Initial project scaffold: Astro + OpenSpec + Claude Code config

This commit is contained in:
2026-04-10 12:41:18 +02:00
commit ed946c03d7
46 changed files with 9304 additions and 0 deletions

View File

@@ -0,0 +1,830 @@
# 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. |