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:
---
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:
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+IntersectionObserverin a small inline<script>(no framework needed). Astro'sclient:visibledirective can also lazy-load interactive islands. - Stats ticker: A CSS-only infinite horizontal scroll using
@keyframesandanimation: 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):
- Honeypot hidden field
- JavaScript timestamp token (bots that don't execute JS fail)
- Server-side rate limiting per IP
3.5 Docker setup
website/Dockerfile (multi-stage):
# 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:
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):
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:
# ============================================================
# 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">andhreflang="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:
ProfessionalServiceschema (organization info, contact, social links) - About page:
Organization+Personschemas for each team member - Contact page:
ContactPageschema - All pages:
BreadcrumbListschema
- Homepage:
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
srcsetat 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.
# 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.
{
"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:
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
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)
{
"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:
# 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:
git clone git@gitea.qumo.io:qumo/qumo-website.git
cd qumo-website
Step 3: Initialize the Astro project
# 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
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.
# 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
# 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/:
ls .claude/skills/
# Should show OpenSpec skill files
Step 7: Create the initial directory structure
# 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
# 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
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
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
# 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:
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
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. |