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>
228 lines
13 KiB
Markdown
228 lines
13 KiB
Markdown
# 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`):
|
||
|
||
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`).
|