Files
professional-website/docs/PRD.md
Jason Woltje c800bef739 chore: bootstrap repo with PRD, tasks, design samples, and Mosaic scaffolding
Personal professional website for jasonwoltje.com, built on Payload CMS 3 +
Next.js 16 and deployed to w-docker0 (Docker Swarm) behind the existing
MosaicStack edge Traefik. Establishes the delivery contract before any
scaffold work begins:

- docs/PRD.md — stack, content model, routing, design system, CI, infra,
  acceptance criteria, assumptions, and escalation log
- docs/TASKS.md — milestone breakdown 0.0.1 → 0.1.0 MVP
- README.md, LICENSE (All Rights Reserved), .gitignore
- design-samples/ — stitch "Technical Editorial" mockups + DESIGN.md tokens
- images/ — source headshots (to be imported into Payload media on seed)
- .mosaic/ — orchestrator scaffolding (quality rails, repo hooks)

Scaffold (Next.js + Payload init) ships on feat/scaffold in a follow-up PR.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 21:05:06 -05:00

13 KiB
Raw Permalink Blame History

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 CMSbacked 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):

  1. installpnpm install --frozen-lockfile
  2. lintpnpm lint
  3. typecheckpnpm typecheck
  4. buildpnpm build (validates Next)
  5. security-auditpnpm audit --prod --audit-level=high
  6. docker-build — Kaniko → push sha-<short> + branch tag + latest-on-main
  7. security-trivy — fail on HIGH/CRITICAL in built image
  8. link-package — POST Gitea package→repo link
  9. deploy (Phase 2, not v0.0.1) — Portainer API redeploy
  10. 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)

  • Repo exists at git.mosaicstack.dev/jason.woltje/professional-website with PRD + TASKS + README + LICENSE committed
  • Payload 3 + Next.js 16 scaffold boots locally (pnpm dev → admin accessible)
  • Tailwind config extends design tokens from DESIGN.md
  • All collections + globals defined and seeded with placeholder content
  • All routes render without error (even with empty content)
  • Contact form persists to contactSubmissions + emails notification
  • Turnstile gating verified in staging
  • Dockerfile builds reproducibly under Kaniko
  • .woodpecker/web.yml present; first CI run pushes image to Gitea registry
  • Trivy scan passes (no HIGH/CRITICAL)
  • Compose stack (docker-compose.swarm.yml) deploys on w-docker0 via Portainer
  • Edge Traefik TLS config updated for jasonwoltje.com
  • DNS records active at Cloudflare; apex + www resolve
  • Site loads over HTTPS at both jasonwoltje.com and www.jasonwoltje.com (www redirects)
  • /admin loads Payload admin, Jason can log in
  • /api/health returns 200
  • Lighthouse: Performance ≥ 85, Accessibility ≥ 95, SEO ≥ 95 on home page

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).