Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c760e11c4c | ||
|
|
d841cf98b5 | ||
|
|
5a5b2b42d2 | ||
|
|
24a59c6e93 | ||
|
|
8c9afb65c3 | ||
|
|
dc2bda8c07 | ||
|
|
6bb56e6035 | ||
|
|
1b24607e02 | ||
|
|
85bd49b0c3 | ||
|
|
7207e351d4 | ||
|
|
03a70efcb6 | ||
|
|
15560445ce | ||
|
|
1e921dfe37 | ||
|
|
4f1b468f75 | ||
|
|
206d31d9c2 | ||
|
|
f452d6d237 | ||
|
|
30e385ec7f | ||
|
|
ce5676f552 | ||
|
|
31d4e540ce | ||
|
|
3f02b323a3 | ||
|
|
e3c511b5d5 | ||
|
|
ae51f41fdf | ||
|
|
d875ae70d5 | ||
|
|
841d831486 | ||
|
|
ce93f90250 | ||
|
|
59383f703f | ||
|
|
0d3da3d102 | ||
|
|
c5d4d16596 | ||
|
|
1b2f26e7eb | ||
|
|
e78ac3a4c6 | ||
|
|
d2de8d6c7e | ||
|
|
ab6da0d049 | ||
|
|
35b45c5bcb | ||
|
|
0f438c7557 | ||
|
|
b5fbcdfb27 | ||
|
|
29f3f1a448 | ||
|
|
d45f2e1cd3 | ||
|
|
a857c5bf2e | ||
|
|
c0a989e088 | ||
|
|
dde6155304 | ||
|
|
c128093f46 | ||
|
|
bafad2c69e | ||
|
|
fa3a953a96 | ||
|
|
fb80bd0f2b | ||
|
|
336ac26e64 | ||
|
|
2591b46616 | ||
|
|
6e88cf4cd4 | ||
|
|
650221b74b | ||
|
|
23880f746b | ||
|
|
57767a112e | ||
|
|
11c75caaad | ||
|
|
3f5fb4b7fd | ||
|
|
71ea7c7ce9 | ||
|
|
ece7693ad5 | ||
|
|
d6d062a3b2 | ||
|
|
a71883215b | ||
|
|
539b251287 | ||
|
|
af9a71a37e | ||
|
|
7bad958a38 | ||
|
|
33c9ea4157 | ||
|
|
3e8a4ddc40 | ||
|
|
3ae9add947 | ||
|
|
1b07a1119a | ||
|
|
b3d8e927b0 | ||
|
|
4002546700 | ||
|
|
678fb96be5 | ||
|
|
2e9408bdb8 | ||
|
|
25732fcfc8 | ||
|
|
70faed6c9b |
@@ -0,0 +1,11 @@
|
|||||||
|
.git
|
||||||
|
.github
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.astro
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
README.md
|
||||||
|
LICENSE
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
# For most projects, this workflow file will not need changing; you simply need
|
|
||||||
# to commit it to your repository.
|
|
||||||
#
|
|
||||||
# You may wish to alter this file to override the set of languages analyzed,
|
|
||||||
# or to provide custom queries or build logic.
|
|
||||||
#
|
|
||||||
# ******** NOTE ********
|
|
||||||
# We have attempted to detect the languages in your repository. Please check
|
|
||||||
# the `language` matrix defined below to confirm you have the correct set of
|
|
||||||
# supported CodeQL languages.
|
|
||||||
#
|
|
||||||
name: "CodeQL Advanced"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "main" ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ "main" ]
|
|
||||||
schedule:
|
|
||||||
- cron: '23 16 * * 1'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze (${{ matrix.language }})
|
|
||||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
|
||||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
|
||||||
# - https://gh.io/supported-runners-and-hardware-resources
|
|
||||||
# - https://gh.io/using-larger-runners (GitHub.com only)
|
|
||||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
|
||||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
|
||||||
permissions:
|
|
||||||
# required for all workflows
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
# required to fetch internal or private CodeQL packs
|
|
||||||
packages: read
|
|
||||||
|
|
||||||
# only required for workflows in private repositories
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
|
|
||||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
|
||||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
|
||||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
|
||||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
|
||||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
|
||||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
|
||||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
# Add any setup steps before running the `github/codeql-action/init` action.
|
|
||||||
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
|
||||||
# or others). This is typically only required for manual builds.
|
|
||||||
# - name: Setup runtime (example)
|
|
||||||
# uses: actions/setup-example@v1
|
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v4
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
build-mode: ${{ matrix.build-mode }}
|
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
||||||
# By default, queries listed here will override any specified in a config file.
|
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
|
||||||
|
|
||||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
|
||||||
# queries: security-extended,security-and-quality
|
|
||||||
|
|
||||||
# If the analyze step fails for one of the languages you are analyzing with
|
|
||||||
# "We were unable to automatically build your code", modify the matrix above
|
|
||||||
# to set the build mode to "manual" for that language. Then modify this step
|
|
||||||
# to build your code.
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
|
||||||
- name: Run manual build steps
|
|
||||||
if: matrix.build-mode == 'manual'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo 'If you are using a "manual" build mode for one or more of the' \
|
|
||||||
'languages you are analyzing, replace this with the commands to build' \
|
|
||||||
'your code, for example:'
|
|
||||||
echo ' make bootstrap'
|
|
||||||
echo ' make release'
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v4
|
|
||||||
with:
|
|
||||||
category: "/language:${{matrix.language}}"
|
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
name: Portfolio-synk (Immich → portfolio)
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 3 * * *' # dygnsvis 03:00 UTC
|
||||||
|
workflow_dispatch: # "på begäran" — manuell knapp / API-trigger
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write # för att committa nya bilder
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: portfolio-sync # ingen parallell körning (race-skydd)
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
|
||||||
|
- name: Install sharp
|
||||||
|
run: npm install sharp@^0.33.5
|
||||||
|
|
||||||
|
- name: Run portfolio sync
|
||||||
|
id: sync
|
||||||
|
env:
|
||||||
|
IMMICH_API_KEY: ${{ secrets.IMMICH_API_KEY }}
|
||||||
|
run: node scripts/portfolio-sync.mjs
|
||||||
|
|
||||||
|
- name: Commit, PR & auto-merge if changed
|
||||||
|
if: steps.sync.outputs.changed == 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.SYNC_PAT }}
|
||||||
|
SYNC_PAT: ${{ secrets.SYNC_PAT }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
ADDED: ${{ steps.sync.outputs.added }}
|
||||||
|
REMOVED: ${{ steps.sync.outputs.removed }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
BRANCH="sync/portfolio-$(date +%Y%m%d-%H%M%S)"
|
||||||
|
git config user.name "portfolio-sync[bot]"
|
||||||
|
git config user.email "actions@github.com"
|
||||||
|
git checkout -b "$BRANCH"
|
||||||
|
git add src/assets/images/portfolio/
|
||||||
|
git commit -m "chore(portfolio): sync ${ADDED} added, ${REMOVED} removed from Immich"
|
||||||
|
git push "https://x-access-token:${SYNC_PAT}@github.com/${REPO}.git" "$BRANCH"
|
||||||
|
gh pr create --head "$BRANCH" --base main \
|
||||||
|
--title "chore(portfolio): Immich sync (${ADDED} added, ${REMOVED} removed)" \
|
||||||
|
--body "Automated portfolio sync from Immich portfolio tag."
|
||||||
|
gh pr merge "$BRANCH" --squash --admin --delete-branch
|
||||||
|
|
||||||
|
# Coolify auto-deployar på push till main (webhook) — GitHub-runnern når
|
||||||
|
# inte Coolify (Tailscale-IP 100.x), så vi triggar inte härifrån.
|
||||||
|
- name: Wait for Coolify auto-deploy
|
||||||
|
if: steps.sync.outputs.changed == 'true'
|
||||||
|
run: sleep 120
|
||||||
|
|
||||||
|
- name: Purge Cloudflare cache
|
||||||
|
if: steps.sync.outputs.changed == 'true'
|
||||||
|
run: |
|
||||||
|
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/22c7ed24735e7417d814ef66d90e4397/purge_cache" \
|
||||||
|
-H "Authorization: Bearer ${{ secrets.CF_PURGE_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"files":["https://imagesbyolofsson.se/portfolio"]}' || echo "CF-purge misslyckades (icke-kritiskt)"
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
run: |
|
||||||
|
echo "### Portfolio-synk klar" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- Tillagda: ${{ steps.sync.outputs.added }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- Borttagna: ${{ steps.sync.outputs.removed }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- Ändring: ${{ steps.sync.outputs.changed }}" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN --mount=type=cache,target=/root/.npm \
|
||||||
|
if [ -f package-lock.json ]; then npm ci; else npm install --no-audit --no-fund; fi
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:1.27-alpine AS runtime
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
RUN adduser -D -H -u 101 -s /sbin/nologin nginxuser 2>/dev/null || true
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD wget -q --spider http://127.0.0.1/ || exit 1
|
||||||
@@ -2,158 +2,101 @@
|
|||||||
|
|
||||||
A modern, responsive photography portfolio website built with Astro, Tailwind CSS, and advanced animations.
|
A modern, responsive photography portfolio website built with Astro, Tailwind CSS, and advanced animations.
|
||||||
|
|
||||||
[](https://imagesbyolofsson.se)
|
<br>
|
||||||
[](https://astro.build)
|
|
||||||
[](https://tailwindcss.com)
|
|
||||||
|
|
||||||
## Preview
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" width="33%">
|
<td align="center">
|
||||||
<img src="./public/3d-scroll/img1.jpg" width="250" alt="Portfolio Grid"/>
|
<img src="https://raw.githubusercontent.com/Images-by-Olofsson/homepage/main/src/assets/images/3d-scroll/img1.jpg" width="250" alt="Portfolio Grid">
|
||||||
<br>
|
<br><em> More </em>
|
||||||
<em>Portfolio Grid</em>
|
|
||||||
</td>
|
</td>
|
||||||
<td align="center" width="33%">
|
<td align="center">
|
||||||
<img src="./public/3d-scroll/img2.jpg" width="250" alt="3D Scroll Effect"/>
|
<img src="https://raw.githubusercontent.com/Images-by-Olofsson/homepage/main/src/assets/images/3d-scroll/img2.jpg" width="250" alt="3D Scroll Effect">
|
||||||
<br>
|
<br><em> At </em>
|
||||||
<em>3D Scroll Effect</em>
|
|
||||||
</td>
|
</td>
|
||||||
<td align="center" width="33%">
|
<td align="center">
|
||||||
<img src="./public/3d-scroll/img3.jpg" width="250" alt="Hero Section"/>
|
<img src="https://raw.githubusercontent.com/Images-by-Olofsson/homepage/main/src/assets/images/3d-scroll/img3.jpg" width="250" alt="Hero Section">
|
||||||
<br>
|
<br><em>Imagesbyolofsson.se</em>
|
||||||
<em>Hero Section</em>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Modern Design** - Minimalist and elegant design with focus on images
|
Modern design with minimalist aesthetics and focus on imagery. Advanced animations including wave effects, 3D scroll transforms, and smoke particles. Fully responsive across all devices with optimized performance.
|
||||||
- **Advanced Animations** - Wave animations, 3D scroll effects, and smoke particles
|
|
||||||
- **Fully Responsive** - Perfectly adapted for all devices
|
<br>
|
||||||
- **Blazingly Fast** - Built with Astro for maximum performance
|
|
||||||
- **Interactive Elements** - Hover effects, smooth transitions, and dynamic backgrounds
|
|
||||||
- **SEO Optimized** - Semantic HTML and metadata
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### Prerequisites
|
**Prerequisites**
|
||||||
- Node.js 18.0+
|
|
||||||
- npm or yarn
|
|
||||||
|
|
||||||
### Installation
|
Node.js 18.0 or higher
|
||||||
|
|
||||||
|
**Installation**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repo
|
git clone https://github.com/Images-by-Olofsson/homepage.git
|
||||||
git clone https://github.com/LinusOlofsson-maker/imagesbyolofsson.se.git
|
cd homepage
|
||||||
cd imagesbyolofsson.se
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
npm install
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
# Start development server
|
**Development**
|
||||||
|
|
||||||
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
**Build**
|
||||||
|
|
||||||
# Build the project
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# Preview production build
|
|
||||||
npm run preview
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
src/
|
src/
|
||||||
├── assets/ # Static resources (images, SVGs)
|
├── assets/
|
||||||
├── components/ # Reusable Astro components
|
├── components/
|
||||||
│ ├── WaveBackground.astro # Animated wave background
|
│ ├── WaveBackground.astro
|
||||||
│ ├── ThreeDScroll.astro # 3D scroll effect
|
│ ├── ThreeDScroll.astro
|
||||||
│ ├── FloatingOrbs.astro # Floating orb elements
|
│ ├── FloatingOrbs.astro
|
||||||
│ └── Welcome.astro # Welcome component
|
│ └── Welcome.astro
|
||||||
├── layouts/ # Page layouts
|
├── layouts/
|
||||||
│ ├── MainLayout.astro # Main layout
|
│ ├── MainLayout.astro
|
||||||
│ └── Layout.astro # Base layout
|
│ └── Layout.astro
|
||||||
├── pages/ # Page components
|
├── pages/
|
||||||
│ ├── index.astro # Home page
|
│ ├── index.astro
|
||||||
│ ├── portfolio.astro # Portfolio page
|
│ ├── portfolio.astro
|
||||||
│ ├── about.astro # About page
|
│ ├── about.astro
|
||||||
│ └── contact.astro # Contact page
|
│ └── contact.astro
|
||||||
└── styles/ # CSS st
|
└── styles/
|
||||||
src/
|
└── global.css
|
||||||
├── assets/ # Static resources (images, SVGs)
|
```
|
||||||
├── components/ # Reusable Astro components
|
|
||||||
│ ├── WaveBackground.astro # Animated wave background
|
|
||||||
│ ├── ThreeDScroll.astro # 3D scroll effect
|
|
||||||
│ ├── FloatingOrbs.astro # Floating orb elements
|
|
||||||
│ └── Welcome.astro # Welcome component
|
|
||||||
├── layouts/ # Page layouts
|
|
||||||
│ ├── MainLayout.astro # Main layout
|
|
||||||
│ └── Layout.astro # Base layout
|
|
||||||
├── pages/ # Page components
|
|
||||||
│ ├── index.astro # Home page
|
|
||||||
│ ├── portfolio.astro # Portfolio page
|
|
||||||
│ ├── about.astro # About page
|
|
||||||
│ └── contact.astro # Contact page
|
|
||||||
└── styles/ # CSS styles
|
|
||||||
└── global.css # Global styles
|
|
||||||
Technologies
|
|
||||||
Astro - Modern web framework
|
|
||||||
Tailwind CSS - Utility-first CSS framework
|
|
||||||
TypeScript - Type-safe JavaScript
|
|
||||||
Exifr - EXIF data reading
|
|
||||||
Design System
|
|
||||||
Color Palette
|
|
||||||
Primary: Cyan (#06B6D4)
|
|
||||||
Secondary: Purple (#A855F7)
|
|
||||||
Neutral: Grayscale (#1F2937 - #F9FAFB)
|
|
||||||
Typography
|
|
||||||
Headings: Font Black, Tracking Tighter
|
|
||||||
Body Text: Font Light
|
|
||||||
Accent: Uppercase, Tracking Widest
|
|
||||||
Animations
|
|
||||||
Waves: SVG-based with filter effects
|
|
||||||
Smoke: Path animations with gaussian blur
|
|
||||||
3D Scroll: Parallax with transform3d
|
|
||||||
Transitions: Smooth ease-in/out
|
|
||||||
Responsive Design
|
|
||||||
The website is optimized for:
|
|
||||||
|
|
||||||
Mobile (< 768px) - Single column, touch-friendly
|
<br>
|
||||||
Tablet (768px - 1024px) - Medium layout
|
|
||||||
Desktop (> 1024px) - Full experience with all animations
|
|
||||||
Performance
|
|
||||||
Lighthouse Score: 95+ Performance
|
|
||||||
First Contentful Paint: < 1.5s
|
|
||||||
Largest Contentful Paint: < 2.5s
|
|
||||||
Cumulative Layout Shift: < 0.1
|
|
||||||
Contributing
|
|
||||||
All contributions are welcome! Please follow these steps:
|
|
||||||
|
|
||||||
Fork this repo
|
## Technologies
|
||||||
Create a feature branch (git checkout -b feature/AmazingFeature)
|
|
||||||
Commit your changes (git commit -m 'Add some AmazingFeature')
|
|
||||||
Push to the branch (git push origin feature/AmazingFeature)
|
|
||||||
Open a Pull Request
|
|
||||||
License
|
|
||||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
||||||
|
|
||||||
Contact
|
Astro, Tailwind CSS, TypeScript, Exifr
|
||||||
Website: imagesbyolofsson.se
|
|
||||||
Email: info@imagesbyolofsson.se
|
|
||||||
Instagram: @imagesbyolofsson
|
|
||||||
LinkedIn: Linus Olofsson
|
|
||||||
Acknowledgments
|
|
||||||
Thanks to all the amazing open-source projects that made this possible:
|
|
||||||
|
|
||||||
Astro - For the fantastic web framework
|
<br>
|
||||||
Tailwind CSS - For the powerful CSS framework
|
|
||||||
Adobe Lightroom - For image processing
|
|
||||||
<div align="center"> <p>Built by <a href="https://github.com/LinusOlofsson-maker">Linus Olofsson</a></p> <p>© 2024 Images by Olofsson. All rights reserved.</p> </div> ```
|
|
||||||
Please confirm you want Copilot to make this change in the LinusOlofsson-maker/Images-by-Olofsson/homepage repository on branch main.yles
|
|
||||||
└── global.css # Global styles
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see LICENSE file for details
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<p>Built by Linus Olofsson</p>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
// Kanonisk adress: canonical, og:url, sitemap och JSON-LD byggs från denna.
|
||||||
|
site: 'https://imagesbyolofsson.se',
|
||||||
|
// En URL per sida: /about (aldrig /about/). Servern 301:ar /about/ -> /about.
|
||||||
|
trailingSlash: 'never',
|
||||||
|
build: {
|
||||||
|
// about.astro -> dist/about.html (inte dist/about/index.html).
|
||||||
|
// nginx/Caddy serverar /about via try_files $uri.html.
|
||||||
|
format: 'file',
|
||||||
|
},
|
||||||
|
vite: {
|
||||||
|
plugins: [tailwindcss()],
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# deploy/caddy-site.snippet
|
||||||
|
# Caddy-motsvarigheten till nginx.conf för imagesbyolofsson.se (statisk dist/ från `astro build`,
|
||||||
|
# build.format "file": /about -> about.html, 404 -> 404.html).
|
||||||
|
#
|
||||||
|
# Användning i Caddyfile (Caddy >= 2.7):
|
||||||
|
#
|
||||||
|
# import /etc/caddy/snippets/caddy-site.snippet
|
||||||
|
#
|
||||||
|
# www.imagesbyolofsson.se {
|
||||||
|
# import ibo_www_redirect
|
||||||
|
# }
|
||||||
|
# imagesbyolofsson.se {
|
||||||
|
# # Specialregler (på Cleura: handle för /about, /contact, /posters -> kundportalen,
|
||||||
|
# # se kundportalens deploy/cleura/caddy/Caddyfile) ska ligga FÖRE importen.
|
||||||
|
# import ibo_site /srv/imagesbyolofsson/dist
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# Argumentet är sökvägen till dist-katalogen på servern (justera till den riktiga).
|
||||||
|
|
||||||
|
(ibo_www_redirect) {
|
||||||
|
redir https://imagesbyolofsson.se{uri} permanent
|
||||||
|
}
|
||||||
|
|
||||||
|
(ibo_site) {
|
||||||
|
root * {args[0]}
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
# --- Säkerhetsheaders ---
|
||||||
|
# CSP: tillåter Meta Pixel (laddas först efter samtycke), anonym statistik via sendBeacon
|
||||||
|
# till customer.imagesbyolofsson.se och Cloudflares egna skript (/cdn-cgi, ev. Web Analytics).
|
||||||
|
# 'unsafe-inline' behövs för samtyckesskriptet och Astros inlinade skript/stilar.
|
||||||
|
header {
|
||||||
|
Strict-Transport-Security "max-age=31536000"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-Frame-Options "SAMEORIGIN"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()"
|
||||||
|
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://connect.facebook.net https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://www.facebook.com https://*.facebook.com; font-src 'self' data:; connect-src 'self' https://customer.imagesbyolofsson.se https://www.facebook.com https://*.facebook.com https://connect.facebook.net https://cloudflareinsights.com; frame-src https://www.facebook.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests"
|
||||||
|
-Server
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Cache-Control (ömsesidigt uteslutande matchers) ---
|
||||||
|
@ibo_immutable path /_astro/*
|
||||||
|
header @ibo_immutable Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
|
||||||
|
@ibo_media {
|
||||||
|
path *.jpg *.jpeg *.png *.webp *.avif *.gif *.svg *.ico *.woff *.woff2 *.ttf *.otf
|
||||||
|
not path /_astro/*
|
||||||
|
}
|
||||||
|
header @ibo_media Cache-Control "public, max-age=2592000"
|
||||||
|
|
||||||
|
@ibo_textfiles path *.xml *.txt
|
||||||
|
header @ibo_textfiles Cache-Control "public, max-age=3600"
|
||||||
|
|
||||||
|
@ibo_pages {
|
||||||
|
not path /_astro/*
|
||||||
|
not path *.jpg *.jpeg *.png *.webp *.avif *.gif *.svg *.ico *.woff *.woff2 *.ttf *.otf *.xml *.txt
|
||||||
|
}
|
||||||
|
header @ibo_pages Cache-Control "public, max-age=0, must-revalidate"
|
||||||
|
|
||||||
|
# --- 301:or ---
|
||||||
|
# Gamla indexerade adresser -> portfolion.
|
||||||
|
@ibo_old path /my-people /my-people/ /animales-and-nature /animales-and-nature/ /celebrations-with-family /celebrations-with-family/ /northen-lights /northen-lights/
|
||||||
|
redir @ibo_old /portfolio permanent
|
||||||
|
|
||||||
|
# /index.html -> /
|
||||||
|
redir /index.html / permanent
|
||||||
|
|
||||||
|
# /about.html -> /about (404-sidan hanteras separat nedan)
|
||||||
|
@ibo_htmlext {
|
||||||
|
path_regexp ibo_htmlext ^(/.+)\.html$
|
||||||
|
not path /404.html
|
||||||
|
}
|
||||||
|
redir @ibo_htmlext {re.ibo_htmlext.1}{?query} permanent
|
||||||
|
|
||||||
|
# Avslutande snedstreck: /about/ -> /about
|
||||||
|
@ibo_trailing path_regexp ibo_trailing ^(/.+)/$
|
||||||
|
redir @ibo_trailing {re.ibo_trailing.1}{?query} permanent
|
||||||
|
|
||||||
|
# 404-sidan ska bara synas som svar på okända adresser, inte som egen URL med 200.
|
||||||
|
@ibo_404page path /404 /404.html
|
||||||
|
error @ibo_404page 404
|
||||||
|
|
||||||
|
# --- Statiska filer ---
|
||||||
|
try_files {path} {path}.html
|
||||||
|
file_server
|
||||||
|
|
||||||
|
# Riktig 404: 404.html med status 404 (inte startsidan med 200).
|
||||||
|
handle_errors {
|
||||||
|
@ibo_notfound expression {err.status_code} == 404
|
||||||
|
handle @ibo_notfound {
|
||||||
|
root * {args[0]}
|
||||||
|
rewrite * /404.html
|
||||||
|
file_server {
|
||||||
|
status 404
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# deploy/cleura/install-portfolio-sync.sh – installs the portfolio sync on the Cleura server (run as root from a
|
||||||
|
# checkout of this repo). Needs /etc/portal/portfolio-sync.env (root 600) first; see portfolio-sync.sh for the keys.
|
||||||
|
# Undo: systemctl disable --now portfolio-sync.timer && rm /etc/systemd/system/portfolio-sync.{service,timer}
|
||||||
|
set -euo pipefail
|
||||||
|
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
ENVF=/etc/portal/portfolio-sync.env
|
||||||
|
[ -f "$ENVF" ] || { echo "$ENVF is missing - copy IMMICH_URL, PORTFOLIO_TAG_ID, IMMICH_API_KEY and GIT_URL there first"; exit 2; }
|
||||||
|
[ "$(stat -c %a "$ENVF")" = "600" ] || chmod 600 "$ENVF"
|
||||||
|
install -m 755 -o root -g root "$HERE/portfolio-sync.sh" /opt/portal/site/portfolio-sync.sh
|
||||||
|
install -m 644 -o root -g root "$HERE/portfolio-sync.service" /etc/systemd/system/portfolio-sync.service
|
||||||
|
install -m 644 -o root -g root "$HERE/portfolio-sync.timer" /etc/systemd/system/portfolio-sync.timer
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now portfolio-sync.timer
|
||||||
|
systemctl list-timers portfolio-sync.timer --no-pager
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Immich portfolio tag -> imagesbyolofsson-se repo -> live site (deploy/cleura/portfolio-sync.sh)
|
||||||
|
After=network-online.target docker.service
|
||||||
|
Wants=network-online.target
|
||||||
|
Requires=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/opt/portal/site/portfolio-sync.sh
|
||||||
|
TimeoutStartSec=45min
|
||||||
|
Nice=10
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# deploy/cleura/portfolio-sync.sh – Immich "portfolio" tag -> this repo -> imagesbyolofsson.se, on the Cleura server.
|
||||||
|
#
|
||||||
|
# Replaces /opt/immich-sync/run-sync.sh on Nurem (started by n8n over SSH) when imagesbyolofsson.se moved to Cleura
|
||||||
|
# (Linus 2026-11-06: everything on the Cleura server). Same steps: fetch main, run scripts/portfolio-sync.mjs,
|
||||||
|
# commit + push to Gitea main when photos were added or removed, and then rebuild the live site with
|
||||||
|
# /opt/portal/site/build-live.sh (EXIF check included). Nothing is built when nothing changed.
|
||||||
|
#
|
||||||
|
# Installed by deploy/cleura/install-portfolio-sync.sh as /opt/portal/site/portfolio-sync.sh, run by
|
||||||
|
# portfolio-sync.timer every 20 minutes. Secrets: /etc/portal/portfolio-sync.env (root 600) with
|
||||||
|
# IMMICH_URL, PORTFOLIO_TAG_ID, IMMICH_API_KEY (read-only Immich key) and GIT_URL (Gitea URL that may push main,
|
||||||
|
# with user:token@ in it, as on Nurem).
|
||||||
|
#
|
||||||
|
# The Gitea token never lands on disk outside that env file: the clone's remote URL is stored without it, git gets
|
||||||
|
# it from a credential helper at fetch/push time, and the Node container (npm install runs third-party code) gets
|
||||||
|
# an empty tmpfs over .git and only the Immich variables.
|
||||||
|
set -euo pipefail
|
||||||
|
umask 077
|
||||||
|
ENVF=${PORTFOLIO_SYNC_ENV:-/etc/portal/portfolio-sync.env}
|
||||||
|
DIR=${PORTFOLIO_SYNC_DIR:-/opt/portal/site/sync/repo}
|
||||||
|
NODE=node:22.22.0-alpine@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34
|
||||||
|
|
||||||
|
exec 9>/run/lock/portfolio-sync.lock
|
||||||
|
flock -n 9 || { echo "[sync] already running"; exit 0; }
|
||||||
|
|
||||||
|
set -a; . "$ENVF"; set +a
|
||||||
|
export IMMICH_URL="${IMMICH_URL:-https://foto.imagesbyolofsson.se/api}"
|
||||||
|
export PORTFOLIO_TAG_ID="${PORTFOLIO_TAG_ID:-67d16b6c-0dea-42ef-a289-2c9e57fa5c5a}"
|
||||||
|
[ -n "${IMMICH_API_KEY:-}" ] || { echo "FATAL: IMMICH_API_KEY missing in $ENVF"; exit 2; }
|
||||||
|
[ -n "${GIT_URL:-}" ] || { echo "FATAL: GIT_URL missing in $ENVF"; exit 2; }
|
||||||
|
|
||||||
|
# Split GIT_URL into the clean URL (stored in .git/config) and user/token (kept in this process only).
|
||||||
|
read -r CLEAN_URL GIT_USER GIT_TOKEN < <(python3 - "$GIT_URL" <<'PY'
|
||||||
|
import sys, urllib.parse as u
|
||||||
|
p = u.urlsplit(sys.argv[1])
|
||||||
|
host = p.hostname + (":%d" % p.port if p.port else "")
|
||||||
|
print(u.urlunsplit((p.scheme, host, p.path, "", "")), u.unquote(p.username or "-"), u.unquote(p.password or "-"))
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
unset GIT_URL
|
||||||
|
export GIT_USER GIT_TOKEN
|
||||||
|
# shellcheck disable=SC2016 # $GIT_USER/$GIT_TOKEN are expanded by the helper's shell, not here.
|
||||||
|
G=(git -c credential.helper= -c 'credential.helper=!f() { echo "username=$GIT_USER"; echo "password=$GIT_TOKEN"; }; f')
|
||||||
|
|
||||||
|
if [ ! -d "$DIR/.git" ]; then
|
||||||
|
mkdir -p "$(dirname "$DIR")"
|
||||||
|
"${G[@]}" clone -q "$CLEAN_URL" "$DIR"
|
||||||
|
fi
|
||||||
|
chmod 700 "$(dirname "$DIR")" "$DIR"
|
||||||
|
git -C "$DIR" remote set-url origin "$CLEAN_URL"
|
||||||
|
echo "[sync] fetch + reset main"
|
||||||
|
"${G[@]}" -C "$DIR" fetch -q origin main
|
||||||
|
git -C "$DIR" checkout -q -B main origin/main
|
||||||
|
git -C "$DIR" reset -q --hard origin/main
|
||||||
|
|
||||||
|
echo "[sync] scripts/portfolio-sync.mjs"
|
||||||
|
# portfolio-sync.mjs exits 1 when a single photo fails but still writes the others; those are committed below, and the
|
||||||
|
# run ends with exit 1 so systemd shows that something needs a look (journalctl -u portfolio-sync).
|
||||||
|
SYNC_RC=0
|
||||||
|
docker run --rm --memory 3g --cpus 2 --network bridge \
|
||||||
|
-e IMMICH_URL -e IMMICH_API_KEY -e PORTFOLIO_TAG_ID \
|
||||||
|
-v "$DIR":/app --mount type=tmpfs,destination=/app/.git -w /app "$NODE" \
|
||||||
|
sh -c 'npm install --no-audit --no-fund --loglevel=error >/dev/null && node scripts/portfolio-sync.mjs' || SYNC_RC=$?
|
||||||
|
|
||||||
|
if git -C "$DIR" status --porcelain -- src/assets/images/portfolio | grep -q .; then
|
||||||
|
git -C "$DIR" add -A src/assets/images/portfolio
|
||||||
|
git -C "$DIR" -c user.name="portfolio-sync (Cleura)" -c user.email="Admin@imagesbyolofsson.se" \
|
||||||
|
commit -q -m "chore(portfolio): sync $(date -u +%Y-%m-%d)"
|
||||||
|
"${G[@]}" -C "$DIR" push -q origin main
|
||||||
|
echo "[sync] pushed $(git -C "$DIR" rev-parse --short HEAD), rebuilding the live site"
|
||||||
|
/opt/portal/site/build-live.sh main
|
||||||
|
else
|
||||||
|
echo "[sync] no changes"
|
||||||
|
fi
|
||||||
|
if [ "$SYNC_RC" != 0 ]; then
|
||||||
|
echo "[sync] WARNING: portfolio-sync.mjs exited $SYNC_RC - some photos failed (see the lines with '!' above)"
|
||||||
|
exit "$SYNC_RC"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Portfolio sync every 20 minutes (Immich has no event when a photo is tagged, so it polls)
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*:0/20
|
||||||
|
RandomizedDelaySec=60
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# nginx för imagesbyolofsson.se (Docker-image: dist/ i /usr/share/nginx/html).
|
||||||
|
# Bygget använder build.format "file": /about -> about.html, 404 -> 404.html.
|
||||||
|
# Samma regler som Caddy-varianten i deploy/caddy-site.snippet.
|
||||||
|
|
||||||
|
# Cache-Control per sökväg (inte per Content-Type, så att /_astro/*.jpg också blir immutable).
|
||||||
|
map $uri $ibo_cache_control {
|
||||||
|
default "public, max-age=0, must-revalidate";
|
||||||
|
~^/_astro/ "public, max-age=31536000, immutable";
|
||||||
|
~*\.(?:jpe?g|png|webp|avif|gif|svg|ico|woff2?|ttf|otf)$ "public, max-age=2592000";
|
||||||
|
~*\.(?:xml|txt)$ "public, max-age=3600";
|
||||||
|
}
|
||||||
|
|
||||||
|
# www -> apex (om www-trafik når den här containern).
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name www.imagesbyolofsson.se;
|
||||||
|
return 301 https://imagesbyolofsson.se$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Relativa Location-headers: bakom Cloudflare/proxy blir det aldrig http://host:80/...
|
||||||
|
absolute_redirect off;
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
# Säkerhetsheaders. OBS: inga add_header i location-blocken nedan, annars ärvs inte dessa.
|
||||||
|
# CSP: tillåter Meta Pixel (connect.facebook.net/facebook.com, laddas först efter samtycke),
|
||||||
|
# den anonyma statistiken (sendBeacon till customer.imagesbyolofsson.se) och Cloudflares
|
||||||
|
# egna skript (/cdn-cgi, ev. Web Analytics). 'unsafe-inline' behövs för samtyckesskriptet
|
||||||
|
# och Astros inlinade skript/stilar; byt mot hashar (Astro experimental.csp) i ett senare steg.
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()" always;
|
||||||
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://connect.facebook.net https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://www.facebook.com https://*.facebook.com; font-src 'self' data:; connect-src 'self' https://customer.imagesbyolofsson.se https://www.facebook.com https://*.facebook.com https://connect.facebook.net https://cloudflareinsights.com; frame-src https://www.facebook.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests" always;
|
||||||
|
add_header Cache-Control $ibo_cache_control always;
|
||||||
|
|
||||||
|
# Gamla indexerade adresser -> närmaste nya sida.
|
||||||
|
location ~ ^/(?:my-people|animales-and-nature|celebrations-with-family|northen-lights)/?$ {
|
||||||
|
return 301 /portfolio;
|
||||||
|
}
|
||||||
|
|
||||||
|
# /index.html och /about.html -> rena URL:er (bara externa anrop; try_files nedan
|
||||||
|
# serverar filerna internt utan att gå hit).
|
||||||
|
location = /index.html {
|
||||||
|
return 301 /;
|
||||||
|
}
|
||||||
|
location ~ ^(?<ibo_noext>/.+)\.html$ {
|
||||||
|
return 301 $ibo_noext$is_args$args;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Inga dubbletter med avslutande snedstreck: /about/ -> /about.
|
||||||
|
location ~ ^(?<ibo_noslash>/.+)/$ {
|
||||||
|
return 301 $ibo_noslash$is_args$args;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Startsidan serveras direkt (ingen intern omdirigering till /index.html).
|
||||||
|
location = / {
|
||||||
|
try_files /index.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hashade Astro-filer: cachas ett år (immutable sätts via map ovan).
|
||||||
|
location ^~ /_astro/ {
|
||||||
|
access_log off;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Originalnedladdning (/download/original) proxas till Immich någon annanstans
|
||||||
|
# (Cloudflare/Caddy). Finns den regeln i den här nginx: lägg den här ovanför.
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Riktig 404: 404.html med status 404 (inte startsidan med 200).
|
||||||
|
error_page 404 /404.html;
|
||||||
|
location = /404.html {
|
||||||
|
internal;
|
||||||
|
}
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_types text/plain text/css text/xml application/xml application/json application/javascript application/xml+rss image/svg+xml;
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 700 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 542 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 119 KiB |
@@ -0,0 +1,38 @@
|
|||||||
|
# robots.txt – https://imagesbyolofsson.se
|
||||||
|
#
|
||||||
|
# Policy (beslut av Linus 2026-10-27): ALLA botar är välkomna, även AI-crawlers
|
||||||
|
# och träningsbotar. Innehållet får användas av AI-assistenter med hänvisning
|
||||||
|
# till Images by Olofsson (se /llms.txt). Cloudflare-sidan (AI Crawl Control /
|
||||||
|
# "Block AI bots" / hanterad robots.txt) ställs in separat så att den inte
|
||||||
|
# blockerar eller skriver över detta.
|
||||||
|
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
# AI-crawlers och träningsbotar: uttryckligen tillåtna.
|
||||||
|
User-agent: GPTBot
|
||||||
|
User-agent: OAI-SearchBot
|
||||||
|
User-agent: ChatGPT-User
|
||||||
|
User-agent: ClaudeBot
|
||||||
|
User-agent: Claude-User
|
||||||
|
User-agent: Claude-SearchBot
|
||||||
|
User-agent: anthropic-ai
|
||||||
|
User-agent: CCBot
|
||||||
|
User-agent: Google-Extended
|
||||||
|
User-agent: Applebot
|
||||||
|
User-agent: Applebot-Extended
|
||||||
|
User-agent: Bytespider
|
||||||
|
User-agent: Amazonbot
|
||||||
|
User-agent: meta-externalagent
|
||||||
|
User-agent: meta-externalfetcher
|
||||||
|
User-agent: FacebookBot
|
||||||
|
User-agent: PerplexityBot
|
||||||
|
User-agent: Perplexity-User
|
||||||
|
User-agent: cohere-ai
|
||||||
|
User-agent: Diffbot
|
||||||
|
User-agent: YouBot
|
||||||
|
User-agent: DuckAssistBot
|
||||||
|
User-agent: MistralAI-User
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://imagesbyolofsson.se/sitemap-index.xml
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Engangsjobb: skapa -s (800px) och -m (1600px) for redan synkade portfolio-bilder.
|
||||||
|
// Kallan ar den befintliga 3000px-filen i repot - ingen ny nedladdning fran Immich.
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import sharp from "sharp";
|
||||||
|
|
||||||
|
const DIR = "src/assets/images/portfolio";
|
||||||
|
const SIZES = [{ suffix: "-s", dim: 800 }, { suffix: "-m", dim: 1600 }];
|
||||||
|
const QUALITY = 88;
|
||||||
|
|
||||||
|
const files = fs.readdirSync(DIR).filter(
|
||||||
|
(f) => /^immich-[0-9a-f-]{36}-/i.test(f) && /\.jpg$/i.test(f) && !/-[sm]\.jpg$/i.test(f)
|
||||||
|
);
|
||||||
|
console.log(`hittade ${files.length} large-bilder`);
|
||||||
|
|
||||||
|
let skapade = 0, hoppade = 0;
|
||||||
|
for (const f of files) {
|
||||||
|
const src = path.join(DIR, f);
|
||||||
|
const base = src.replace(/\.jpg$/i, "");
|
||||||
|
for (const { suffix, dim } of SIZES) {
|
||||||
|
const dest = `${base}${suffix}.jpg`;
|
||||||
|
if (fs.existsSync(dest)) { hoppade++; continue; }
|
||||||
|
await sharp(src)
|
||||||
|
.rotate()
|
||||||
|
.resize(dim, dim, { fit: "inside", withoutEnlargement: true })
|
||||||
|
.withMetadata()
|
||||||
|
.jpeg({ quality: QUALITY })
|
||||||
|
.toFile(dest);
|
||||||
|
skapade++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`klart: ${skapade} skapade, ${hoppade} fanns redan`);
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Portfolio-synk: speglar Immich "portfolio"-taggade bilder till src/assets/images/portfolio/
|
||||||
|
// Självläkande: härleder synkat tillstånd från immich-<assetId>-*-filnamn i repot.
|
||||||
|
// Bevarar EXIF (portfolion läser DateTimeOriginal/Model/ISO för år/månad-filter + lightbox).
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import sharp from "sharp";
|
||||||
|
|
||||||
|
const IMMICH = process.env.IMMICH_URL || "https://foto.imagesbyolofsson.se/api";
|
||||||
|
const KEY = process.env.IMMICH_API_KEY;
|
||||||
|
const TAGID = process.env.PORTFOLIO_TAG_ID || "67d16b6c-0dea-42ef-a289-2c9e57fa5c5a";
|
||||||
|
const PORTFOLIO_DIR = "src/assets/images/portfolio";
|
||||||
|
const MAX_DIM = 3000;
|
||||||
|
const JPEG_QUALITY = 88;
|
||||||
|
// RAW-format som sharp/libvips inte kan konvertera — hoppas över elegant.
|
||||||
|
// Tagga JPEG-versionen (redigera RAW → exportera JPEG → tagga den) istället.
|
||||||
|
const RAW_EXTENSIONS = [".cr2", ".cr3", ".arw", ".nef", ".dng", ".raf", ".orf", ".rw2", ".pef", ".srw"];
|
||||||
|
|
||||||
|
function isRaw(name) {
|
||||||
|
const lower = name.toLowerCase();
|
||||||
|
return RAW_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!KEY) { console.error("IMMICH_API_KEY saknas"); process.exit(1); }
|
||||||
|
|
||||||
|
async function api(method, pathname, body) {
|
||||||
|
const res = await fetch(IMMICH + pathname, {
|
||||||
|
method,
|
||||||
|
headers: { "x-api-key": KEY, "Content-Type": "application/json" },
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`${method} ${pathname} → ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hämta alla portfolio-taggade assets (paginerat, unikt på id — search/metadata ger dubbletter över sidgräns)
|
||||||
|
async function fetchTagged() {
|
||||||
|
const map = new Map();
|
||||||
|
let page = 1;
|
||||||
|
while (true) {
|
||||||
|
const res = await api("POST", "/search/metadata", { tagIds: [TAGID], size: 250, page });
|
||||||
|
const d = await res.json();
|
||||||
|
const items = d.assets.items;
|
||||||
|
if (!items.length) break;
|
||||||
|
for (const a of items) {
|
||||||
|
const date = a.exifInfo?.dateTimeOriginal || a.fileCreatedAt;
|
||||||
|
map.set(a.id, { id: a.id, name: a.originalFileName, date });
|
||||||
|
}
|
||||||
|
const next = d.assets.nextPage;
|
||||||
|
if (!next) break;
|
||||||
|
page = Number(next);
|
||||||
|
}
|
||||||
|
return [...map.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vilka immich-<assetId>-*.jpg finns redan i repot? → { assetId: filename }
|
||||||
|
function existingSynced() {
|
||||||
|
if (!fs.existsSync(PORTFOLIO_DIR)) return {};
|
||||||
|
const out = {};
|
||||||
|
for (const f of fs.readdirSync(PORTFOLIO_DIR)) {
|
||||||
|
const m = f.match(/^immich-([0-9a-f-]{36})-/i);
|
||||||
|
if (m) out[m[1]] = f;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bygg destinationsfilnamn: immich-<assetId>-<ÅÅÅÅMMDD>-<safeOriginalName>.jpg
|
||||||
|
function destName(asset) {
|
||||||
|
const d = asset.date ? new Date(asset.date) : new Date();
|
||||||
|
const ymd = `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, "0")}${String(d.getDate()).padStart(2, "0")}`;
|
||||||
|
// strippa ev. "<assetid>__"-prefix från originalnamnet (från migreringen), behåll basnamn utan ändelse
|
||||||
|
let base = asset.name.replace(/^[0-9a-f-]{36}__/i, "").replace(/\.[^.]+$/, "");
|
||||||
|
base = base.replace(/[^\w.\-]/g, "_").slice(0, 60);
|
||||||
|
return `immich-${asset.id}-${ymd}-${base}.jpg`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadAndResize(asset, destPath) {
|
||||||
|
const res = await api("GET", `/assets/${asset.id}/original`);
|
||||||
|
const buf = Buffer.from(await res.arrayBuffer());
|
||||||
|
// Pixel shift and the 15000 px Instagram squares can exceed sharp's default limit (268 MP). JPEGs are shrunk while
|
||||||
|
// they are decoded, so memory stays low; the sync container still has a memory cap.
|
||||||
|
await sharp(buf, { limitInputPixels: 1_000_000_000 })
|
||||||
|
.rotate() // respektera EXIF-orientering
|
||||||
|
.resize(MAX_DIM, MAX_DIM, { fit: "inside", withoutEnlargement: true })
|
||||||
|
.withMetadata() // BEVARA EXIF (kritiskt för portfolions datum/kamera)
|
||||||
|
.jpeg({ quality: JPEG_QUALITY })
|
||||||
|
.toFile(destPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const taggedAll = await fetchTagged();
|
||||||
|
// Hoppa över RAW-filer elegant — de räknas inte mot synk (tagga JPEG-versionen istället)
|
||||||
|
const skippedRaw = taggedAll.filter((a) => isRaw(a.name));
|
||||||
|
const tagged = taggedAll.filter((a) => !isRaw(a.name));
|
||||||
|
|
||||||
|
const synced = existingSynced();
|
||||||
|
const taggedIds = new Set(tagged.map((a) => a.id));
|
||||||
|
|
||||||
|
const toAdd = tagged.filter((a) => !synced[a.id]);
|
||||||
|
const toRemove = Object.entries(synced).filter(([id]) => !taggedIds.has(id)).map(([id, file]) => ({ id, file }));
|
||||||
|
|
||||||
|
console.log(`portfolio-synk: ${taggedAll.length} taggade (${tagged.length} bilder + ${skippedRaw.length} RAW), ${Object.keys(synced).length} redan synkade`);
|
||||||
|
console.log(`→ lägg till: ${toAdd.length}, ta bort: ${toRemove.length}`);
|
||||||
|
if (skippedRaw.length) {
|
||||||
|
console.log(`⏭ Hoppade över ${skippedRaw.length} RAW-fil(er) — tagga JPEG-versionen istället:`);
|
||||||
|
for (const r of skippedRaw) console.log(` ${r.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdirSync(PORTFOLIO_DIR, { recursive: true });
|
||||||
|
|
||||||
|
for (const a of toAdd) {
|
||||||
|
const fname = destName(a);
|
||||||
|
const dest = path.join(PORTFOLIO_DIR, fname);
|
||||||
|
try {
|
||||||
|
await downloadAndResize(a, dest);
|
||||||
|
console.log(` + ${fname}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(` ! misslyckades ${a.name}: ${e.message}`);
|
||||||
|
process.exitCode = 1; // signalera fel men fortsätt med resten
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const r of toRemove) {
|
||||||
|
fs.rmSync(path.join(PORTFOLIO_DIR, r.file), { force: true });
|
||||||
|
console.log(` - ${r.file}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// skriv ut om något ändrats (workflow läser detta för att avgöra commit)
|
||||||
|
const changed = toAdd.length > 0 || toRemove.length > 0;
|
||||||
|
fs.writeFileSync(process.env.GITHUB_OUTPUT || "/dev/stdout",
|
||||||
|
`changed=${changed}\nadded=${toAdd.length}\nremoved=${toRemove.length}\n`, { flag: "a" });
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => { console.error("FEL:", e.message); process.exit(1); });
|
||||||
|
After Width: | Height: | Size: 312 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 1010 KiB |
|
After Width: | Height: | Size: 391 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 323 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 1000 KiB |
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 861 KiB |
|
After Width: | Height: | Size: 937 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 283 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 705 KiB |
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 802 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 606 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 299 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 354 KiB |
|
After Width: | Height: | Size: 700 KiB |
|
After Width: | Height: | Size: 205 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 521 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 728 KiB |
|
After Width: | Height: | Size: 297 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 721 KiB |
|
After Width: | Height: | Size: 329 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 986 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 742 KiB |
|
After Width: | Height: | Size: 413 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 952 KiB |
|
After Width: | Height: | Size: 432 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 991 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 364 KiB |
|
After Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 424 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 464 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 895 KiB |
|
After Width: | Height: | Size: 302 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 836 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 525 KiB |
|
After Width: | Height: | Size: 354 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.1 MiB |