Step 002 complete. Step 003 needs verification
This commit is contained in:
830
README.md
Normal file
830
README.md
Normal 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. |
|
||||
Reference in New Issue
Block a user