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

228 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. `install``pnpm install --frozen-lockfile`
2. `lint``pnpm lint`
3. `typecheck``pnpm typecheck`
4. `build``pnpm build` (validates Next)
5. `security-audit``pnpm 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`](TASKS.md).
**Design source:** `design-samples/stitch_jasonwoltje.com/` (HTML mockups + `silicon_ethos/DESIGN.md`).