PRD — jasonwoltje.com Professional Website
Status: Active
Milestone: 0.0.1 (pre-MVP bootstrap)
Owner: Jason Woltje
Last updated: 2026-04-13
1. Purpose
Build Jason Woltje's professional personal website (jasonwoltje.com) as a Payload CMS–backed portfolio + writing platform. The site anchors Jason's transition from IT Director to software engineer and showcases technical work, writing, and maker/music interests.
2. Goals
| Goal |
Success criterion |
| Establish a credible engineer-executive brand |
"Technical Editorial" design system from design-samples/ rendered with high fidelity |
| Serve as portfolio for projects |
CMS-authored projects collection rendered at /projects + /projects/[slug] |
| Publish long-form writing |
posts collection + /writing and /writing/[slug] routes |
| Let Jason edit content without code |
Payload admin at /admin, sole admin user |
| Deploy on existing homelab |
Portainer stack on w-docker0 behind Traefik; no new infra dependencies |
| Respect immutable-tag hard rule |
Deployments reference sha-<short>; never latest |
3. Non-goals (v0.0.x)
- Multi-author CMS or role-based ACL
- Comments / community features
- E-commerce / subscriptions
- Search (Algolia, Meilisearch) — defer until content volume warrants
- Internationalization
- Native mobile app
4. Users
| Role |
Scope |
| Public visitor |
Read all published content, submit contact form, subscribe to newsletter (once Mautic is live) |
| Admin (Jason) |
Full Payload admin: create/edit/publish content, manage media, view contact submissions |
5. Content model
Collections
| Name |
Purpose |
Key fields |
users |
Payload auth. Jason only for v0.0.x |
email, password, role (admin) |
media |
All uploads. Sharp-generated sizes: thumb (400), card (800), hero (1600), og (1200×630). alt required. |
url, alt, credit, sizes |
categories |
Project/post taxonomy |
name, slug, accent (primary / secondary / tertiary — maps to design tokens) |
projects |
Portfolio entries |
title, slug, role, category (rel), summary, body (richText), stack (string[]), heroImage, gallery, externalUrl, featured (bool), order, status (draft/published), publishedAt, seo (group) |
posts |
Blog / long-form writing |
title, slug, excerpt, body (richText + blocks: code, callout, image), category (rel), heroImage, tags (string[]), status, publishedAt, seo (group) |
gear |
Music/Making items (decorative only for v0.0.x) |
name, type, notes, image, accent |
contactSubmissions |
Inbound form entries; admin-read only |
name, email, brief, source, submittedAt, ipHash, status (new/replied/spam) |
Globals
| Name |
Fields |
home |
heroHeadline (richText), heroSub, statusTerminal (baked at build time), featuredProjects (rel[]), ctas |
about |
introBlock, makerMindsetBlock, soundtrackBlock, gearRefs (rel[]), timeline (year/title/note[]) |
contact |
availabilityBadge, timezoneLabel, directEmail, socialLinks (label, url, icon[]), newsletterEnabled (bool) |
resume |
summary, experience[] (company, role, dates, bullets), skills[], education[], pdfExport (auto-generated) |
navigation |
primaryLinks (label, href[]), footerStatusText |
seo |
siteTitle, defaultDescription, defaultOgImage, twitterHandle, jsonLdPerson |
6. Page routing (Next.js App Router)
| Route |
Source |
Rendering |
/ |
globals.home + featured projects |
ISR (revalidate 60s, on-demand via Payload afterChange hook) |
/about |
globals.about + gear |
ISR |
/projects |
projects where status=published |
ISR |
/projects/[slug] |
projects by slug |
SSG + ISR fallback |
/writing |
posts where status=published |
ISR |
/writing/[slug] |
posts by slug |
SSG + ISR fallback |
/contact |
globals.contact |
Static shell, client form |
/resume |
globals.resume |
SSR, HTML |
/resume.pdf |
globals.resume |
Dynamic, server-rendered PDF (@react-pdf/renderer or Puppeteer) |
/admin/** |
Payload admin |
Dynamic, auth-gated |
/api/contact |
Custom endpoint |
Dynamic (Turnstile verify + honeypot + Payload write + email) |
/api/health |
Healthcheck |
Dynamic |
/sitemap.xml, /robots.txt, /rss.xml |
Generated |
Dynamic w/ cache |
7. Design system
Source: design-samples/stitch_jasonwoltje.com/silicon_ethos/DESIGN.md
Ported as-is to production:
| Token family |
Source |
| Colors (M3 tokens) |
Inline tailwind.config in stitch HTMLs → tailwind.config.ts extend.colors |
| Typography |
Space Grotesk (display/labels) + Inter (body), next/font/google self-hosted, CSS vars --font-headline / --font-body / --font-label |
| Icons |
lucide-react (replaces Material Symbols CDN) |
| Tailwind plugins |
@tailwindcss/forms, @tailwindcss/container-queries |
| Utilities |
.ghost-border (outline-variant/15), .glass-card (backdrop-blur + surface-bright/60), .neon-cta (primary→primary-container gradient + cyan glow) |
Hard design rules (from DESIGN.md):
- No 1px solid borders at 100% opacity — tonal surfaces only
- No 50/50 splits — 60/40 asymmetric layouts
- Status Terminal on header/footer (
LOC / STATUS / REV) — baked from build SHA
- Dark mode always-on (
<html class="dark"> permanent for v0.0.x)
Implementation approach: fixed Next pages composed of typed React section components (HeroHeadline, ProjectBentoGrid, MakerMindsetCard, AudioSignalPath, StatusTerminal, ContactForm, NewsletterBand, SocialBento). Content comes from Payload globals/collections. No generic drag-and-drop page builder — protects editorial asymmetry.
8. Services & integrations
| Service |
Purpose |
Status |
| Cloudflare Turnstile |
Contact form CAPTCHA |
To integrate (site key + secret) |
| Umami (self-hosted) |
Analytics |
To deploy separately (stack 2); site loads Umami tracker once URL known |
| Mautic (self-hosted) |
Newsletter |
NOT YET DEPLOYED — newsletter UI shows "Coming soon" or disabled state until Mautic endpoint exists |
| Resend / SMTP relay |
Contact form notifications, Payload email |
ASSUMPTION: SMTP credentials from existing homelab relay |
| Cloudflare DNS |
jasonwoltje.com A record → w-docker0 edge (10.1.1.43) |
DNS records to be added |
9. Infrastructure
| Element |
Value |
| Deploy host |
w-docker0 (10.1.1.45), single-node Docker Swarm |
| Orchestration |
Portainer (https://10.1.1.43:9443), endpoint ID 7 |
| Ingress |
Edge Traefik on 10.1.1.43 (TLS termination, Let's Encrypt) → per-swarm Traefik on w-docker0 (HTTP, entrypoints=web) |
| External overlay |
traefik-public (pre-existing) |
| Registry |
git.mosaicstack.dev/jason.woltje/professional-website (Gitea container packages) |
| CI |
Woodpecker CI (ci.mosaicstack.dev), Kaniko builder |
| Image tags |
sha-<8-char> (always), branch tag (main/develop), vX.Y.Z (on git tag). latest exists but is NEVER referenced by compose. |
Traefik labels (canonical pattern)
Mirrors mosaic-stack-website/docker-compose.swarm.yml. Labels live under deploy.labels (Swarm). Router uses entrypoints=web; TLS handled at edge. Middleware: www→apex 301 redirect.
Environment variables
| Var |
Required |
Notes |
WEB_IMAGE_TAG |
yes |
Always sha-<short> or vX.Y.Z; CI sets, manual deploy updates |
PAYLOAD_SECRET |
yes |
32+ char random, Portainer-stored |
PAYLOAD_POSTGRES_USER / _PASSWORD / _DB |
yes |
Portainer |
DATABASE_URI |
yes |
Composed in compose from above |
PAYLOAD_PUBLIC_SERVER_URL |
yes |
https://jasonwoltje.com |
NEXT_PUBLIC_SITE_URL |
yes |
https://jasonwoltje.com |
TURNSTILE_SITE_KEY / _SECRET_KEY |
yes |
Cloudflare Turnstile |
NEXT_PUBLIC_UMAMI_SRC / _UMAMI_WEBSITE_ID |
optional |
Analytics; empty disables tracker |
SMTP_* or RESEND_API_KEY |
yes |
Contact form notifications |
NEXT_PUBLIC_BUILD_SHA / _BUILD_REV |
yes |
Baked at build for Status Terminal |
10. CI pipeline (Woodpecker, mirrors website-web.yml)
Stages (all depend via depends_on):
install — pnpm install --frozen-lockfile
lint — pnpm lint
typecheck — pnpm typecheck
build — pnpm build (validates Next)
security-audit — pnpm audit --prod --audit-level=high
docker-build — Kaniko → push sha-<short> + branch tag + latest-on-main
security-trivy — fail on HIGH/CRITICAL in built image
link-package — POST Gitea package→repo link
deploy (Phase 2, not v0.0.1) — Portainer API redeploy
health-check (Phase 2) — poll /api/health
For v0.0.1, ship through step 8. Initial deploy and subsequent redeploys are manual via Portainer UI until the auto-deploy step is wired.
11. Acceptance criteria (v0.0.1 → v0.1.0 MVP)
12. Assumptions
- ASSUMPTION: SMTP relay credentials available from an existing homelab service or a new Resend free-tier key.
- ASSUMPTION: Cloudflare Turnstile site/secret keys will be provisioned on
jason@diversecanvas.com's CF account.
- ASSUMPTION: Umami will be deployed as a separate Portainer stack; if not ready by MVP, tracker loads only when
NEXT_PUBLIC_UMAMI_WEBSITE_ID is set.
- ASSUMPTION: Mautic deployment is out of scope for this repo; newsletter UI is present but wired as "Coming soon" until a Mautic endpoint is provided.
- ASSUMPTION: Payload 3 is compatible with Next.js 16 at build time. If incompatibility emerges, fall back to Next.js 15 LTS — flagged as a rollback path, not an approval gate.
- ASSUMPTION: PostgreSQL 17 (alpine) for the
postgres service. Payload supports pg 15+; no concerns expected.
- ASSUMPTION: Headshots in
images/ are pre-upload originals. They are imported into the Payload media collection as part of content seeding; originals may stay in repo or be removed at Jason's preference post-seed.
13. Escalations (require Jason's decision)
| ID |
Topic |
Impact |
| ESC-01 |
Edge Traefik TLS config for jasonwoltje.com (new domain, not under *.woltje.com wildcard). Does Jason apply the config change, or delegate via runbook? |
Blocks HTTPS |
| ESC-02 |
Mautic deployment timeline — if it slips, newsletter UI remains stub for MVP |
Soft; affects contact page polish |
| ESC-03 |
SMTP credentials source — reuse existing relay, or provision Resend? |
Blocks contact form email notifications |
| ESC-04 |
Turnstile keys — Jason to provision at cloudflare.com/dashboard and drop values in Portainer env |
Blocks spam-resistant contact form |
| ESC-05 |
Analytics (Umami) stack — deploy alongside this site or as separate milestone? |
Affects tracker wiring |
14. Risks
| Risk |
Mitigation |
| Next.js 16 + Payload 3 version drift |
Pin exact versions; fallback to Next 15 documented |
Gitea package path normalization (jason.woltje dot) |
Dry-run Kaniko push early; adjust image path if Gitea normalizes to jason-woltje |
| Single-node host loss |
Volume backups (pg_dump + media tar) to offsite (B2/R2) — defer to v0.1.x |
| Contact form spam |
Turnstile + honeypot + rate-limit on /api/contact |
| Design drift from stitch |
Section components mirror stitch HTML structure; design review before v0.1.0 cut |
15. Out of scope / deferred
- Full page builder (Payload
blocks) — consider only if writing volume justifies
- MinIO migration — triggered by >20 GB media or multi-replica frontend
- Authentik SSO — single-admin site doesn't need it
- Auto-deploy via Portainer API — Phase 2 after stable manual deploys
- Offsite backups — Phase 2
- Spotify / Last.fm integrations — decorative only per Jason's call
Source of truth for tasks: see docs/TASKS.md.
Design source: design-samples/stitch_jasonwoltje.com/ (HTML mockups + silicon_ethos/DESIGN.md).