commit 4420dd51ac687c4cea41a44a226170bac3ef246d Author: Jerod Hodgkin Date: Sat Jan 17 10:35:55 2026 -0700 Initial commit: LisiLou Photography Portfolio Features: - Immich integration with automatic album image carousel - Dynamic configuration via JSON files - Nginx proxy for Immich API access - Docker deployment ready - Gitea Actions CI/CD workflow Co-Authored-By: Claude Opus 4.5 diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..4e20884 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(docker compose:*)", + "Bash(curl:*)", + "WebFetch(domain:photography.lisilou.com)", + "WebFetch(domain:immich.app)", + "WebFetch(domain:docs.immich.app)", + "WebFetch(domain:api.immich.app)" + ] + } +} diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..e73fecd --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,68 @@ +name: Build and Deploy + +on: + push: + branches: + - main + pull_request: + branches: + - main + +env: + REGISTRY: your-registry.com # Update with your registry + IMAGE_NAME: lisilou-portfolio + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=sha,prefix= + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + needs: build + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + steps: + - name: Deploy to server + uses: appleboy/ssh-action@v1.0.0 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_KEY }} + script: | + cd /opt/lisilou-portfolio + docker compose pull + docker compose up -d + docker image prune -f diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b40d76 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Dependencies +node_modules/ + +# Build outputs +dist/ +build/ + +# Environment files +.env +.env.local +.env.*.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# Local config overrides (keep default configs in git) +config/local.json + +# Docker +docker-compose.override.yml diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..abc7dfa --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,71 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +LisiLou Photography Portfolio is a lightweight, zero-framework photography portfolio website designed to integrate with Immich for image hosting and client gallery access. It's a single-page application with all code in one HTML file, using vanilla JavaScript and inline CSS. + +## Technology Stack + +- **Frontend:** Vanilla HTML5, CSS3, JavaScript (no framework) +- **Server:** Nginx (Alpine-based) +- **Deployment:** Docker + Docker Compose +- **CI/CD:** Gitea Actions + +## Development Commands + +```bash +# Start development server (serves at http://localhost:8080) +docker compose up -d + +# Rebuild after code changes to src/index.html +docker compose build && docker compose up -d + +# View container logs +docker logs lisilou-portfolio +``` + +**Note:** Configuration (`config/`) and image (`public/images/`) changes are applied immediately without rebuild since they're mounted as volumes. + +## Architecture + +### Single-File SPA +The entire application lives in `src/index.html` (~865 lines): +- Lines 1-605: Inline CSS with CSS custom properties for theming +- Lines 606-865: HTML structure and vanilla JavaScript + +### Configuration-Driven Content +All site content is loaded dynamically from `config/site.json` at runtime: +- Site branding (title, tagline, logo) +- Theme colors (applied to CSS variables) +- Portfolio categories with Immich album links +- Social media links +- Client gallery access settings + +### Immich Integration +The app serves as a gateway to Immich shared albums: +- Portfolio categories link to Immich albums via `immichAlbumId` +- Client gallery codes are Immich share IDs +- Full URLs constructed as: `{baseUrl}{publicAlbumPrefix}{albumId}` + +### Multi-Tenant Support +Multiple photographers can be supported via `config/profiles.json`, mapping domains to different configuration files. + +## Key Files + +| File | Purpose | +|------|---------| +| `src/index.html` | Complete SPA (CSS + HTML + JS) | +| `config/site.json` | Runtime configuration for all content | +| `config/profiles.json` | Multi-tenant profile routing | +| `nginx.conf` | Caching rules, security headers, SPA routing | +| `Dockerfile` | Multi-stage build (Node Alpine → Nginx Alpine) | +| `.gitea/workflows/deploy.yml` | CI/CD pipeline | + +## Nginx Configuration Highlights + +- **Caching:** Images = 1 year, CSS/JS = 1 month, Config = 5 minutes +- **Health Check:** `/health` endpoint for container monitoring +- **SPA Routing:** Falls back to `index.html` for all unmatched routes +- **Security Headers:** X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1e5cd1f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# Multi-stage build for lightweight production image +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files if they exist (for future npm dependencies) +COPY package*.json ./ +RUN if [ -f package.json ]; then npm ci --only=production; fi + +# Production image using nginx +FROM nginx:alpine + +# Install envsubst for runtime config substitution +RUN apk add --no-cache gettext + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/nginx.conf + +# Copy static files +COPY src/ /usr/share/nginx/html/ +COPY config/ /usr/share/nginx/html/config/ +COPY public/ /usr/share/nginx/html/ + +# Create directories for runtime config +RUN mkdir -p /usr/share/nginx/html/config + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost:80/ || exit 1 + +# Expose port +EXPOSE 80 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..d23a813 --- /dev/null +++ b/README.md @@ -0,0 +1,234 @@ +# LisiLou Photography Portfolio + +A lightweight, configurable photography portfolio website designed to integrate with Immich for image hosting and client gallery access. + +## Features + +- 🎨 **Elegant, modern design** - Clean photography-focused aesthetic +- ⚙️ **Easy configuration** - Update content via JSON files, no code changes needed +- 📱 **Fully responsive** - Looks great on all devices +- 🖼️ **Immich integration** - Direct links to shared albums for portfolio and client galleries +- 👥 **Multi-photographer support** - Configure multiple profiles for different photographers +- 🐳 **Docker ready** - Easy deployment with Docker and Docker Compose +- 🔄 **CI/CD ready** - Gitea Actions workflow included + +## Quick Start + +### Local Development + +1. Clone this repository +2. Customize `config/site.json` with your information +3. Add your images to `public/images/` +4. Run with Docker: + +```bash +docker compose up -d +``` + +5. Visit `http://localhost:8080` + +### Configuration + +All site content is configured through `config/site.json`. Here's what you can customize: + +#### Site Settings +```json +{ + "site": { + "title": "Your Photography Name", + "tagline": "Your tagline here", + "description": "SEO description" + } +} +``` + +#### Social Media +```json +{ + "social": { + "instagram": "your.handle", + "facebook": "yourpage", + "pinterest": "yourprofile", + "tiktok": "yourhandle" + } +} +``` + +#### Immich Integration +```json +{ + "immich": { + "baseUrl": "https://photos.yourdomain.com", + "publicAlbumPrefix": "/share/" + } +} +``` + +#### Portfolio Categories +```json +{ + "portfolio": { + "categories": [ + { + "id": "seniors", + "name": "Senior Portraits", + "description": "Celebrate your milestone", + "coverImage": "/images/portfolio/seniors-cover.jpg", + "immichAlbumId": "your-immich-album-share-id" + } + ] + } +} +``` + +#### Theme Customization +```json +{ + "theme": { + "primaryColor": "#8B7355", + "accentColor": "#D4C5B5", + "textColor": "#2C2C2C", + "backgroundColor": "#FDFBF9" + } +} +``` + +### Adding Portfolio Images + +1. Create cover images for each portfolio category +2. Place them in `public/images/portfolio/` +3. Update the `coverImage` paths in `config/site.json` +4. Get the share IDs from your Immich albums and add them to `immichAlbumId` + +### Client Gallery Access + +Clients can enter their gallery code (Immich share ID) on the website. They'll be redirected to their Immich shared album. + +**Workflow:** +1. Create a shared album in Immich for your client +2. Give them the share ID as their "gallery code" +3. They enter it on your website and are taken directly to their photos + +## Deployment + +### With Docker Compose + +1. Copy files to your server: +```bash +scp -r . user@server:/opt/lisilou-portfolio/ +``` + +2. Create the external network: +```bash +docker network create web +``` + +3. Start the container: +```bash +cd /opt/lisilou-portfolio +docker compose up -d +``` + +### With Reverse Proxy (Traefik/Nginx) + +If using Traefik, add labels to docker-compose.yml: + +```yaml +services: + portfolio: + labels: + - "traefik.enable=true" + - "traefik.http.routers.portfolio.rule=Host(`lisilou.com`)" + - "traefik.http.routers.portfolio.tls=true" + - "traefik.http.routers.portfolio.tls.certresolver=letsencrypt" +``` + +### Gitea Actions CI/CD + +1. Set up Gitea Actions on your Gitea instance +2. Add these secrets to your repository: + - `REGISTRY_USERNAME` - Container registry username + - `REGISTRY_PASSWORD` - Container registry password + - `DEPLOY_HOST` - Your server hostname + - `DEPLOY_USER` - SSH username + - `DEPLOY_KEY` - SSH private key + +3. Update `.gitea/workflows/deploy.yml` with your registry URL + +4. Push to main branch to trigger automatic deployment + +## Multi-Photographer Support + +To support multiple photographers: + +1. Create additional config files (e.g., `config/photographer2.json`) +2. Update `config/profiles.json`: + +```json +{ + "profiles": { + "lisilou": { + "enabled": true, + "domain": "lisilou.com", + "configFile": "site.json" + }, + "photographer2": { + "enabled": true, + "domain": "photographer2.com", + "configFile": "photographer2.json" + } + }, + "defaultProfile": "lisilou", + "multiTenant": true +} +``` + +3. Add multi-tenant routing to nginx.conf (or use separate containers) + +## File Structure + +``` +lisilou-portfolio/ +├── config/ +│ ├── site.json # Main configuration +│ └── profiles.json # Multi-profile config +├── public/ +│ └── images/ +│ ├── portfolio/ # Portfolio cover images +│ ├── logo.png # Site logo +│ └── favicon.ico # Favicon +├── src/ +│ └── index.html # Main HTML file +├── .gitea/ +│ └── workflows/ +│ └── deploy.yml # CI/CD workflow +├── docker-compose.yml +├── Dockerfile +├── nginx.conf +└── README.md +``` + +## Updating Content + +### To update text/settings: +1. Edit `config/site.json` +2. The changes are applied immediately (no rebuild needed) + +### To update images: +1. Add new images to `public/images/` +2. Update paths in `config/site.json` +3. Changes are applied immediately + +### To update code: +1. Edit `src/index.html` +2. Rebuild the Docker image: +```bash +docker compose build +docker compose up -d +``` + +Or push to Git and let CI/CD handle it. + +## License + +MIT License - Feel free to use and modify for your photography business! diff --git a/config/profiles.json b/config/profiles.json new file mode 100644 index 0000000..2ca50df --- /dev/null +++ b/config/profiles.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "lisilou": { + "enabled": true, + "domain": "lisilou.com", + "configFile": "site.json" + } + }, + "defaultProfile": "lisilou", + "multiTenant": false +} diff --git a/config/site.json b/config/site.json new file mode 100644 index 0000000..f830782 --- /dev/null +++ b/config/site.json @@ -0,0 +1,68 @@ +{ + "site": { + "title": "LisiLou Photography", + "tagline": "Capturing life's beautiful moments", + "description": "Senior portraits, engagement sessions, and milestone photography", + "logo": "/images/logo.png", + "favicon": "/images/favicon.ico" + }, + "photographer": { + "name": "LisiLou", + "bio": "Passionate about capturing authentic moments and creating timeless memories.", + "profileImage": "/images/profile.jpg" + }, + "contact": { + "email": "hello@lisilou.com", + "phone": "", + "bookingUrl": "" + }, + "social": { + "instagram": "lisilou.photography", + "facebook": "", + "pinterest": "", + "tiktok": "" + }, + "immich": { + "baseUrl": "https://photography.lisilou.com", + "publicAlbumPrefix": "/s/" + }, + "portfolio": { + "categories": [ + { + "id": "seniors", + "name": "Senior Portraits", + "description": "Celebrate your milestone", + "coverImage": "/images/portfolio/seniors-cover.jpg", + "immichAlbumId": "fake-portraits" + }, + { + "id": "engagements", + "name": "Engagements", + "description": "Love stories captured", + "coverImage": "/images/portfolio/engagements-cover.jpg", + "immichAlbumId": "fake-engagements" + }, + { + "id": "families", + "name": "Families", + "description": "Moments that matter", + "coverImage": "/images/portfolio/families-cover.jpg", + "immichAlbumId": "family-portraits" + } + ] + }, + "clientAccess": { + "enabled": true, + "title": "Client Gallery Access", + "description": "Enter your gallery code to view and download your photos", + "placeholder": "Enter your gallery code" + }, + "theme": { + "primaryColor": "#8B7355", + "accentColor": "#D4C5B5", + "textColor": "#2C2C2C", + "backgroundColor": "#FDFBF9", + "fontDisplay": "Cormorant Garamond", + "fontBody": "Nunito Sans" + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e449d8b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3.8' + +services: + portfolio: + build: + context: . + dockerfile: Dockerfile + container_name: lisilou-portfolio + restart: unless-stopped + ports: + - "8080:80" + volumes: + # Mount config for easy updates without rebuilding + - ./config:/usr/share/nginx/html/config:ro + # Mount images for easy updates + - ./public/images:/usr/share/nginx/html/images:ro + environment: + - TZ=America/Denver + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + networks: + - web + + # Optional: Watchtower for automatic updates + # watchtower: + # image: containrrr/watchtower + # volumes: + # - /var/run/docker.sock:/var/run/docker.sock + # command: --interval 300 lisilou-portfolio + +networks: + web: + external: true diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..ccd8e22 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,102 @@ +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml; + + server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Cache static assets + location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + location ~* \.(css|js)$ { + expires 1M; + add_header Cache-Control "public"; + } + + # Config files should not be cached long + location /config/ { + expires 5m; + add_header Cache-Control "public, must-revalidate"; + } + + # SPA fallback + location / { + try_files $uri $uri/ /index.html; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # Proxy for Immich share pages (to avoid CORS issues) + # Usage: /api/immich-proxy?url=https://photography.lisilou.com/s/albumId + location /api/immich-proxy { + internal; + resolver 8.8.8.8 1.1.1.1 valid=300s; + resolver_timeout 5s; + + set $target_url $arg_url; + proxy_pass $target_url; + proxy_set_header Host $proxy_host; + proxy_set_header Accept "text/html"; + proxy_ssl_server_name on; + } + + # Proxy endpoint for Immich share pages and API + location ~ ^/api/fetch-immich/(.+)$ { + resolver 8.8.8.8 1.1.1.1 valid=300s; + resolver_timeout 5s; + + set $immich_path $1; + set $immich_host photography.lisilou.com; + proxy_pass https://$immich_host/$immich_path$is_args$args; + proxy_set_header Host $immich_host; + proxy_ssl_server_name on; + proxy_ssl_protocols TLSv1.2 TLSv1.3; + } + } +} diff --git a/public/images/.gitkeep b/public/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/images/portfolio/.gitkeep b/public/images/portfolio/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..6225f69 --- /dev/null +++ b/src/index.html @@ -0,0 +1,956 @@ + + + + + + LisiLou Photography + + + + + + + + + + + + +
+
+ +
+
+
+ + + + + + + + +
+
+
+
+

Photography

+

LisiLou Photography

+

Capturing life's beautiful moments

+ +
+
+ Scroll + + + +
+
+ + +
+
+

The Work

+

Portfolio

+
+
+ +
+
+ + +
+
+
+

Clients

+

Gallery Access

+
+

Enter your gallery code to view and download your photos

+
+ + +
+
+
+ + +
+
+

Get in Touch

+

Let's Connect

+
+ +
+ + +
+ + +
+ + + +