# qumo.io Website Rebuild — Architecture & Setup Plan

_From Webflow to self-hosted Astro, behind Caddy, managed with Claude Code + OpenSpec._

---

## 1. What we're building

A complete rebuild of qumo.io as an Astro 5 static site, served from an nginx container on the existing VPS, proxied through Caddy, with a self-hosted form handler for the contact page. The current Webflow site stays live as a visual reference until the new site is ready to swap in.

### Target stack

```
Internet → Cloudflare (orange cloud, Full strict)
              │
              ▼
           Caddy (existing, on VPS)
              │
              ├── qumo.io ──────────► qumo-website (nginx:stable-alpine-slim, port 80)
              │                         └── serves /usr/share/nginx/html/ (Astro dist/)
              │
              ├── qumo.io/api/contact ► qumo-form-handler (Go container, port 8080)
              │                         └── validates + sends email via SMTP relay
              │
              └── analytics.qumo.io ──► umami (optional, later)
```

### Pages to build (EN + NL)

| Route (EN)    | Route (NL)        | Source on current site         |
|---------------|-------------------|--------------------------------|
| `/`           | `/nl`             | Homepage — hero, services, stats ticker, approach, CTA |
| `/about`      | `/nl/about`       | Mission, values, team, expertise |
| `/contact`    | `/nl/contact`     | Contact form + meeting booking link |
| `/privacy`    | `/nl/privacy`     | Privacy policy (new — currently missing) |

Plus: 404 page, `robots.txt`, `sitemap.xml` (auto-generated by Astro), `_redirects` or Caddy-level redirects.

### What is NOT in scope for v1

- Blog / CMS content
- Client portal or authenticated pages
- E-commerce
- AI product page (exists in SharePoint as draft content — can be added as a later OpenSpec change)

---

## 2. Repository structure

The repo lives on Gitea at `gitea.qumo.io`. The project is a monorepo containing the Astro site, the form handler, and the Docker Compose stack for deployment.

```
qumo-website/
├── .claude/                     # Claude Code project configuration (committed to git)
│   ├── CLAUDE.md                # Project context — loaded automatically every session
│   ├── settings.json            # Hooks, permissions, env vars (shared with team)
│   ├── settings.local.json      # Personal overrides (gitignored automatically)
│   ├── skills/                  # Auto-generated by `openspec init` + `openspec update`
│   │   └── (openspec skills)    # OpenSpec slash commands (/opsx:propose, etc.)
│   └── agents/                  # Custom subagents (optional, add later if needed)
│
├── .mcp.json                    # MCP server config — Astro docs (committed to git)
│
├── openspec/                    # OpenSpec SDD configuration
│   ├── config.yaml              # Project context + rules
│   ├── specs/                   # Living specifications (grows over time)
│   └── changes/                 # Active change proposals
│       └── archive/             # Completed changes
│
├── website/                     # Astro project root
│   ├── astro.config.mjs
│   ├── tailwind.config.mjs
│   ├── tsconfig.json
│   ├── package.json
│   ├── src/
│   │   ├── layouts/
│   │   │   └── BaseLayout.astro       # HTML shell, <head>, nav, footer
│   │   ├── components/
│   │   │   ├── Nav.astro
│   │   │   ├── Footer.astro
│   │   │   ├── Hero.astro
│   │   │   ├── ServiceCard.astro
│   │   │   ├── StatsTicker.astro
│   │   │   ├── TeamMember.astro
│   │   │   ├── ContactForm.astro
│   │   │   ├── LanguageSwitcher.astro
│   │   │   └── ...
│   │   ├── pages/
│   │   │   ├── index.astro            # EN homepage
│   │   │   ├── about.astro
│   │   │   ├── contact.astro
│   │   │   ├── privacy.astro
│   │   │   ├── 404.astro
│   │   │   └── nl/
│   │   │       ├── index.astro        # NL homepage
│   │   │       ├── about.astro
│   │   │       ├── contact.astro
│   │   │       └── privacy.astro
│   │   ├── content/                   # i18n content strings
│   │   │   ├── en.json
│   │   │   └── nl.json
│   │   ├── styles/
│   │   │   └── global.css             # Tailwind base + Archia font-face
│   │   └── assets/                    # Images, SVGs, fonts (processed by Astro)
│   │       ├── fonts/
│   │       │   └── archia/            # Archia font files (woff2)
│   │       ├── images/
│   │       └── icons/                 # Phosphor Icons (SVG)
│   ├── public/
│   │   ├── robots.txt
│   │   ├── favicon.svg
│   │   └── og-image.png
│   └── Dockerfile                     # Multi-stage: Node build → nginx serve
│
├── form-handler/                # Contact form backend
│   ├── main.go                  # HTTP server: POST /api/contact
│   ├── go.mod
│   ├── go.sum
│   ├── Dockerfile
│   └── README.md
│
├── docker-compose.yml           # Production stack (website + form-handler)
├── .env.example                 # Template for secrets
├── .gitignore
└── README.md
```

---

## 3. Key architectural decisions

### 3.1 Localization approach

Astro does not have built-in i18n routing, but its file-based routing makes it straightforward. Each locale gets its own page files under `pages/nl/`. Shared content strings live in JSON files (`content/en.json`, `content/nl.json`). A helper function loads the right strings based on the current URL prefix.

Each page imports content like:

```astro
---
import { getStrings } from '../content/i18n';
const t = getStrings('nl');  // or 'en'
---
<h1>{t.hero.title}</h1>
```

`<link rel="alternate" hreflang="...">` tags are added in the base layout for SEO. The language switcher component reads the current path and swaps the locale prefix.

### 3.2 Styling: Tailwind CSS + Archia font

Tailwind is configured with the Qumo brand tokens as custom theme values. No component library — just utility classes. The Archia font is self-hosted (woff2 files in `src/assets/fonts/`).

Tailwind config extends with:

```js
colors: {
  midnight: '#102022',
  snow: '#F3F3F3',
  'brand-blue': '#5257E4',
  'brand-red': '#F71E3E',
},
fontFamily: {
  archia: ['Archia', 'ui-sans-serif', 'system-ui', 'sans-serif'],
},
```

The gradient is applied via a CSS class: `bg-gradient-to-br from-brand-blue to-brand-red`.

### 3.3 Animations

The current Webflow site uses scroll-triggered animations (fade-ins, slide-ups) and a horizontal stats ticker. In Astro, these are implemented with:

- **Scroll animations**: CSS `@keyframes` + `IntersectionObserver` in a small inline `<script>` (no framework needed). Astro's `client:visible` directive can also lazy-load interactive islands.
- **Stats ticker**: A CSS-only infinite horizontal scroll using `@keyframes` and `animation: scroll linear infinite`. No JavaScript required.
- **Page transitions**: Astro's built-in View Transitions API (`transition:animate`).

### 3.4 Contact form

The form HTML lives in the Astro component. On submit, JavaScript POSTs to `/api/contact` which Caddy routes to the form-handler container.

**Form handler** (Go, ~100 lines):
- Accepts POST with JSON body: `name`, `email`, `company`, `message`
- Validates required fields, email format
- Checks honeypot field (hidden input, must be empty)
- Checks a JS-generated timestamp token (rejects submissions < 3 seconds after page load)
- Sends email via SMTP relay (Resend or Brevo)
- Returns JSON response (success/error)
- Rate-limited: max 5 submissions per IP per hour (in-memory counter, acceptable for low traffic)

**Spam protection layers** (no CAPTCHA needed):
1. Honeypot hidden field
2. JavaScript timestamp token (bots that don't execute JS fail)
3. Server-side rate limiting per IP

### 3.5 Docker setup

**`website/Dockerfile`** (multi-stage):

```dockerfile
# Stage 1: Build
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Serve
FROM nginx:stable-alpine-slim
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
```

**`form-handler/Dockerfile`**:

```dockerfile
FROM golang:1.22-alpine AS build
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o form-handler .

FROM alpine:3.20
COPY --from=build /app/form-handler /usr/local/bin/
EXPOSE 8080
CMD ["form-handler"]
```

**`docker-compose.yml`** (production):

```yaml
services:
  website:
    build: ./website
    container_name: qumo-website
    restart: unless-stopped
    expose:
      - "80"
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost/"]
      interval: 30s
      timeout: 3s
      retries: 3
    networks:
      - qumo_services_proxy_network

  form-handler:
    build: ./form-handler
    container_name: qumo-form-handler
    restart: unless-stopped
    expose:
      - "8080"
    env_file:
      - .env
    networks:
      - qumo_services_proxy_network

networks:
  qumo_services_proxy_network:
    external: true
```

### 3.6 Caddy configuration

Add to the existing Caddyfile on the VPS:

```caddyfile
# ============================================================
# qumo.io — company website
# ============================================================
qumo.io, www.qumo.io {
    encode gzip

    # Security headers (better than Webflow standard plans)
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "strict-origin-when-cross-origin"
        Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()"
        Cross-Origin-Opener-Policy "same-origin"
        -Server
        -X-Powered-By
    }

    # Caching: immutable for hashed assets, revalidate for HTML
    @immutable path /assets/* /_astro/*
    header @immutable Cache-Control "public, max-age=31536000, immutable"
    @html path *.html /
    header @html Cache-Control "public, max-age=0, must-revalidate"

    # Contact form API → form handler container
    handle /api/contact {
        reverse_proxy qumo-form-handler:8080
    }

    # Everything else → website container
    handle {
        reverse_proxy qumo-website:80
    }

    # www → apex redirect
    @www host www.qumo.io
    handle @www {
        redir https://qumo.io{uri} permanent
    }
}
```

Note: This block does NOT use `import authentik` — the public website should be unauthenticated.

### 3.7 SEO and structured data

Every page includes via the base layout:

- `<title>` and `<meta name="description">` from the i18n content files
- `<link rel="canonical">` pointing to the current URL
- `<link rel="alternate" hreflang="en">` and `hreflang="nl"` pointing to both versions
- Open Graph tags (`og:title`, `og:description`, `og:image`, `og:url`, `og:type`)
- Twitter card tags
- JSON-LD structured data:
  - **Homepage**: `ProfessionalService` schema (organization info, contact, social links)
  - **About page**: `Organization` + `Person` schemas for each team member
  - **Contact page**: `ContactPage` schema
  - **All pages**: `BreadcrumbList` schema

`robots.txt` allows all crawlers including AI bots (GPTBot, ClaudeBot, PerplexityBot).

`sitemap.xml` is auto-generated by `@astrojs/sitemap` at build time with both EN and NL URLs.

### 3.8 Image optimization

Astro's built-in `<Image>` component handles everything at build time:
- Converts to AVIF (primary) with WebP fallback
- Generates responsive `srcset` at 400, 800, 1200, 1920px widths
- Adds `width`, `height`, `loading="lazy"`, `decoding="async"` automatically
- Output goes to `/_astro/` with content hashes (immutable caching)

Team photos, SVG illustrations, and the logo are stored in `src/assets/`. The `public/` directory is only for files that must not be processed (favicon, og-image).

---

## 4. .claude/ directory — Claude Code project configuration

### `.claude/CLAUDE.md` — project context

This file is loaded automatically at the start of every Claude Code session. It lives inside `.claude/` so all project configuration is in one place.

```markdown
# qumo.io Website

## What this is

The company website for Qumo (qumo.io), a data analytics consultancy targeting Dutch MKB companies. Built with Astro 5, Tailwind CSS, and served from Docker containers behind Caddy.

## Execution environment

Claude Code runs inside a nono sandbox (`nono run --profile claude-openspec -- claude`).
- The sandbox grants read+write to the working directory and `~/.claude`
- Network access is enabled (Anthropic API, npm registry, Astro MCP server)
- All filesystem and network boundaries are kernel-enforced by nono (Landlock on Linux)
- Do NOT attempt to access files outside the project directory or `~/.claude`

## Architecture

- **Framework**: Astro 5 (static output, zero JS by default)
- **Styling**: Tailwind CSS with custom Qumo brand tokens
- **Font**: Archia (self-hosted woff2, licensed from Atipo Foundry)
- **Localization**: EN (default) + NL, file-based routing with JSON content strings
- **Contact form**: Go backend at /api/contact, honeypot + timestamp spam protection
- **Deployment**: Docker multi-stage build → nginx:stable-alpine-slim → Caddy reverse proxy
- **Hosting**: Self-hosted VPS behind Cloudflare (orange cloud, Full strict TLS)
- **Git**: Gitea at gitea.qumo.io

## Brand guidelines (must follow)

- **Colors**: Midnight #102022 (primary dark), Snow #F3F3F3 (light bg), Gradient #5257E4 → #F71E3E (accent only, never as flat standalone colors)
- **Font**: Archia — Bold/SemiBold for headings (uppercase), Regular for body (sentence case)
- **Icons**: Phosphor Icons, light weight, 32px baseline, SVG format
- **Logo**: Q icon can stand alone. Wordmark must always pair with Q icon. Never modify.
- **Brand element**: Q-corner motif, can be used as corner accent or tiled pattern

## Code conventions

- Astro components use `.astro` extension
- All text content comes from `src/content/en.json` and `src/content/nl.json` — never hardcode user-visible strings in components
- CSS uses Tailwind utilities. Custom CSS only for animations and font-face declarations
- Images go in `src/assets/` and use Astro's `<Image>` component (never raw `<img>` tags for raster images)
- SVGs can be inlined directly in components
- No client-side JavaScript frameworks (React, Vue, etc.) — use vanilla JS in `<script>` tags when interactivity is needed
- Keep components small and single-purpose
- File naming: kebab-case for files, PascalCase for components

## Content rules (from Qumo company guidelines)

- Never mention team size, company size, or founding date
- Never name clients in public content (use descriptive labels: "a shipping company")
- The team section shows 5 co-founders: Luigi, Matthijs, Jelle, Jorn, Stijn
- Engagement models should not be listed as a menu — mention naturally
- Proof points are for calls/follow-ups, not the website

## SEO requirements

- Every page needs: unique <title>, meta description, canonical URL, hreflang alternates, OG tags
- JSON-LD structured data on every page (ProfessionalService, BreadcrumbList, etc.)
- All images need alt text
- Heading hierarchy must be correct (single h1 per page, logical h2→h3 nesting)
- sitemap.xml auto-generated by @astrojs/sitemap

## Testing before committing

- `cd website && npm run build` must succeed (catches broken links, missing imports)
- Check the built output in `website/dist/` for correct HTML structure
- Validate JSON-LD with Google Rich Results Test after deployment

## Current reference

The live Webflow site at https://www.qumo.io is the visual reference. Match the design, layout, and content — but improve the code quality, performance, and SEO.
```

### `.claude/settings.json` — hooks and permissions

This file is committed to git and shared with anyone working on the project. It defines hooks that run automatically during Claude Code sessions.

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "if echo \"$CLAUDE_FILE_PATHS\" | grep -qE '\\.(astro|ts|tsx|js|jsx|css|json)$'; then cd $CLAUDE_PROJECT_DIR/website && npx prettier --write \"$CLAUDE_FILE_PATHS\" 2>/dev/null; fi",
            "timeout": 10
          }
        ]
      }
    ]
  },
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)"
    ]
  }
}
```

This auto-formats Astro, TypeScript, CSS, and JSON files with Prettier after every edit, and prevents Claude from reading `.env` files containing secrets.

### Nono profile: `claude-openspec`

You run Claude Code inside a nono sandbox with a custom profile that extends the built-in `claude-code` profile. This profile lives at `~/.config/nono/profiles/claude-openspec.json` on your local machine (not in the repo). It was set up in a previous session.

The launch command for every development session is:

```bash
cd ~/path/to/qumo-website
nono run --profile claude-openspec -- claude
```

The nono sandbox provides kernel-enforced boundaries (Landlock on Ubuntu 24.04):
- Read+write access to the working directory (the repo)
- Read+write access to `~/.claude` (Claude Code state, hooks, skills)
- Network access enabled (Anthropic API, npm, MCP servers)
- Automatic hook that tells Claude about sandbox restrictions when file operations fail

Because nono is the sandbox layer, Claude Code's built-in sandbox should be disabled to avoid double-sandboxing. The `claude-openspec` profile handles this.

---

## 5. OpenSpec configuration

### `openspec/config.yaml`

```yaml
schema: spec-driven

context: |
  Project: qumo.io company website rebuild
  Stack: Astro 5, Tailwind CSS, TypeScript (minimal), Go (form handler)
  Deployment: Docker (nginx:stable-alpine-slim) behind Caddy reverse proxy on VPS
  Localization: English (default) + Dutch, file-based routing, JSON content strings
  Font: Archia (self-hosted woff2, licensed). Fallback: geometric sans-serif
  Brand colors: Midnight #102022, Snow #F3F3F3, Gradient #5257E4 → #F71E3E
  Icons: Phosphor Icons, light weight, SVG, 32px
  Form: Go backend, honeypot + JS timestamp + rate limiting. SMTP via Resend or Brevo.
  Git: Gitea (self-hosted). No CI/CD pipeline yet — manual docker compose build + up.
  AI tooling: Claude Code with Astro Docs MCP server.
  Visual reference: https://www.qumo.io (live Webflow site, match design but improve code)

  Constraints:
  - Zero JavaScript shipped by default (Astro islands only where needed)
  - All user-visible text in JSON content files, never hardcoded
  - No client-side frameworks (React, Vue, etc.)
  - Images processed by Astro <Image> component, not raw <img>
  - Security headers configured in Caddy, not in the app

rules:
  proposal:
    - Reference the visual design at qumo.io when describing UI changes
    - Note which content strings (en.json / nl.json) are affected
    - Identify if the change affects SEO (structured data, meta tags, sitemap)
  specs:
    - Include both EN and NL content in requirements where applicable
    - Specify responsive behavior (mobile, tablet, desktop breakpoints)
  design:
    - Use Astro component patterns (no React/Vue)
    - Reference Tailwind utility classes, not custom CSS where possible
    - Specify which Phosphor icon names to use
  tasks:
    - Each task should be completable in a single Claude Code session
    - Build verification: npm run build must pass after each task
    - Tasks that add new pages must include structured data and meta tags
```

---

## 6. MCP server configuration

### `.mcp.json` (project-level, for Claude Code)

```json
{
  "mcpServers": {
    "Astro docs": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://mcp.docs.astro.build/mcp"]
    }
  }
}
```

This gives Claude Code access to the latest Astro documentation while generating code. It is loaded automatically when Claude Code starts in the project directory.

---

## 7. Setup steps — getting started

Follow these steps in order on your local development machine (Ubuntu 24.04).

### Step 1: Prerequisites

Verify that your `claude-openspec` nono profile is working:

```bash
# Verify nono is installed
nono --version

# Verify the claude-openspec profile exists
nono policy show claude-openspec

# Verify Node.js 20+ (provided by the profile or your system)
node --version   # must be >= 20.19.0

# Verify OpenSpec is installed
openspec --version

# Verify Claude Code is installed
claude --version
```

If any of these are missing, install them. Node.js 20+ is required for both OpenSpec and Astro. Go 1.22+ is needed for the form handler but can be deferred.

### Step 2: Create the repository on Gitea

Create a new repository `qumo-website` on your Gitea instance. Then clone it locally:

```bash
git clone git@gitea.qumo.io:qumo/qumo-website.git
cd qumo-website
```

### Step 3: Initialize the Astro project

```bash
# Create the Astro project inside the website/ directory
npm create astro@latest website -- --template minimal --no-install --typescript strict

# Enter the website directory and install dependencies
cd website
npm install
npm install @astrojs/tailwind @astrojs/sitemap tailwindcss
npm install -D @tailwindcss/typography prettier prettier-plugin-astro

# Verify it works
npm run dev    # should start at localhost:4321
# Ctrl+C to stop

cd ..
```

### Step 4: Create the `.claude/` directory and files

```bash
mkdir -p .claude
```

Copy the contents of `.claude/CLAUDE.md` from Section 4 of this document.

Copy the contents of `.claude/settings.json` from Section 4 of this document.

```bash
# Verify the structure
ls -la .claude/
# Should show: CLAUDE.md  settings.json
```

### Step 5: Create the MCP server config

Copy the contents from Section 6 into `.mcp.json` at the repo root.

### Step 6: Initialize OpenSpec

```bash
# Initialize OpenSpec in the repo root
openspec init

# When prompted, select the 'expanded' workflow profile for access to
# /opsx:new, /opsx:ff, /opsx:continue, /opsx:verify, /opsx:archive

# Then apply the profile
openspec config profile   # select 'expanded'
openspec update           # regenerate skill files in .claude/skills/
```

After init, replace the generated `openspec/config.yaml` with the contents from Section 5 of this document.

Verify that OpenSpec generated its skills inside `.claude/skills/`:

```bash
ls .claude/skills/
# Should show OpenSpec skill files
```

### Step 7: Create the initial directory structure

```bash
# Content and asset directories
mkdir -p website/src/{layouts,components,pages/nl,content,styles}
mkdir -p website/src/assets/{fonts/archia,images,icons}
mkdir -p website/public

# Form handler (scaffold — implementation comes later)
mkdir -p form-handler

# Copy Archia font files (woff2) from SharePoint into:
#   website/src/assets/fonts/archia/
# You need: archia-thin, archia-light, archia-regular,
#           archia-medium, archia-semibold, archia-bold (woff2 format)
```

### Step 8: Initial commit

```bash
# Create .gitignore
cat > .gitignore << 'EOF'
node_modules/
dist/
.env
.DS_Store
*.log
website/.astro/
.claude/settings.local.json
EOF

git add -A
git commit -m "Initial project scaffold: Astro + OpenSpec + Claude Code config"
git push
```

### Step 9: Start Claude Code inside nono

```bash
cd ~/path/to/qumo-website
nono run --profile claude-openspec -- claude
```

Claude Code will automatically read `.claude/CLAUDE.md` for project context and `.mcp.json` to connect to the Astro Docs MCP server. Verify both:

```
> /mcp
```

You should see "Astro docs" listed as a connected server.

```
> /config
```

Verify that hooks from `.claude/settings.json` are loaded (PostToolUse for Prettier formatting).

Nono's automatic hook integration will also inject sandbox context into `~/.claude/CLAUDE.md` (the global one, not the project one) so Claude understands the sandbox boundaries.

---

## 8. Development workflow with OpenSpec

Each feature or page is built as an OpenSpec change. This keeps work organized and gives Claude Code structured context for each task.

### The cycle

```
/opsx:new <change-name>     # Scaffold a new change
/opsx:ff                     # Generate all planning docs (proposal, specs, design, tasks)
                             # ↑ Review these, edit if needed, then:
/opsx:apply                  # Implement the tasks
/opsx:verify                 # Check the implementation against specs
/opsx:archive                # Archive completed change, merge specs
```

### Suggested change sequence for the rebuild

Build the site incrementally, one change at a time. Each change should produce a working (if incomplete) site.

| Order | Change name                   | What it produces                                             |
|-------|-------------------------------|--------------------------------------------------------------|
| 1     | `setup-base-layout`           | BaseLayout.astro with `<head>`, meta tags, OG tags, JSON-LD shell, Archia font-face, Tailwind config, global styles |
| 2     | `add-nav-and-footer`          | Nav.astro (responsive, mobile hamburger, language switcher), Footer.astro, both EN + NL |
| 3     | `build-homepage`              | Homepage sections: hero, services cards, stats ticker, approach steps, CTA. Both locales |
| 4     | `build-about-page`            | About page: mission, values, team members, expertise. Both locales |
| 5     | `build-contact-page`          | Contact form component (frontend only, POST to /api/contact). Both locales |
| 6     | `build-form-handler`          | Go backend: validation, honeypot, timestamp, SMTP sending, rate limiting |
| 7     | `add-structured-data`         | JSON-LD for all pages: ProfessionalService, Person, BreadcrumbList, ContactPage |
| 8     | `add-scroll-animations`       | Fade-in/slide-up on scroll for sections. CSS + IntersectionObserver |
| 9     | `add-404-and-privacy`         | 404 page, privacy policy page, both locales |
| 10    | `docker-and-deployment`       | Dockerfiles, docker-compose.yml, nginx.conf, Caddyfile block, .env.example |
| 11    | `seo-final-pass`              | Sitemap config, robots.txt, canonical URLs, hreflang validation, OG image |

### Example: starting the first change

```bash
cd ~/path/to/qumo-website
nono run --profile claude-openspec -- claude

# In Claude Code:
> /opsx:new setup-base-layout
# Claude generates the change scaffold

> /opsx:ff
# Claude generates proposal.md, specs/, design.md, tasks.md
# Review them — edit anything that's wrong

> /opsx:apply
# Claude implements the tasks one by one

# After implementation:
> cd website && npm run build   # verify it compiles
> cd website && npm run dev     # check in browser

> /opsx:verify    # Claude checks implementation against specs
> /opsx:archive   # Done, specs merged
> git add -A && git commit -m "feat: base layout with head, meta, Tailwind, Archia font"
> git push
```

---

## 9. Deployment workflow

Once the site is ready to go live:

### On the VPS

```bash
# Clone the repo
git clone git@gitea.qumo.io:qumo/qumo-website.git ~/apps/qumo-website
cd ~/apps/qumo-website

# Create .env from template
cp .env.example .env
# Edit .env with SMTP credentials (Resend API key or Brevo SMTP)

# Build and start
docker compose up -d --build

# Verify containers are running
docker ps | grep qumo
```

### Update the Caddyfile

Add the `qumo.io` block from Section 3.6 to `~/networking/caddy/Caddyfile`. Reload Caddy:

```bash
cd ~/networking/caddy
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
```

### Update Cloudflare DNS

Change the `qumo.io` A record to point to the VPS IP (orange cloud proxied). If it currently points to Webflow, this is the cutover moment.

### Verify

- Visit `https://qumo.io` — should load the new site
- Visit `https://qumo.io/nl` — Dutch version
- Submit a test contact form
- Check `https://securityheaders.com/?q=qumo.io` — should be A+
- Run PageSpeed Insights on mobile
- Submit sitemap to Google Search Console

### Subsequent deploys

```bash
cd ~/apps/qumo-website
git pull
docker compose up -d --build
```

The nginx container rebuilds with the new static files. Caddy picks up the new container automatically. Downtime is 2-3 seconds (acceptable for a marketing site).

---

## 10. What this plan intentionally defers

These are explicitly out of scope for v1 but can be added as OpenSpec changes later:

| Item | Why deferred |
|------|-------------|
| CI/CD pipeline (Gitea Actions) | Manual deploy is fine for a low-change marketing site. Add when deploy frequency justifies it. |
| Umami analytics | Not critical for launch. Add as a separate Docker service + Caddy block. |
| AI product page | Content exists in SharePoint draft form. Add when the offering is finalized. |
| Blog | No content ready. Astro supports markdown-based blogs natively when needed. |
| CSP report-only → enforced | Start with enforced but may need `report-uri` to catch issues. Adjust after launch. |
| Uptime monitoring (Uptime Kuma) | Already in the infrastructure plan. Connect after launch. |
| Image CDN / edge caching | Cloudflare's proxy already caches static assets. Sufficient for current traffic. |
