# 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,
, 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' ---