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>
This commit is contained in:
2026-04-13 21:05:06 -05:00
commit c800bef739
21 changed files with 1851 additions and 0 deletions

227
docs/PRD.md Normal file
View File

@@ -0,0 +1,227 @@
# 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`).

121
docs/TASKS.md Normal file
View File

@@ -0,0 +1,121 @@
# TASKS — jasonwoltje.com
**Milestone:** 0.0.1 — Bootstrap (repo, PRD, local scaffold)
**Next:** 0.0.2 — Design system port + collections
**MVP target:** 0.1.0 — Full site deployed on w-docker0, DNS live, all routes
Source of truth for scope: [`PRD.md`](PRD.md).
---
## Legend
| Status | Meaning |
|---|---|
| ☐ | pending |
| ◐ | in progress |
| ✅ | done |
| 🚫 | blocked — see `blocker` column |
| ⏭ | deferred |
---
## Milestone 0.0.1 — Bootstrap
| ID | Task | Status | Owner | Issue | Blocker |
|---|---|---|---|---|---|
| BOOT-01 | Create Gitea repo `jason.woltje/professional-website` | ✅ | orchestrator | — | — |
| BOOT-02 | Commit PRD + TASKS + README + LICENSE + `.gitignore` | ◐ | orchestrator | — | — |
| BOOT-03 | Push bootstrap commit to `main` | ☐ | orchestrator | — | — |
| BOOT-04 | Create first feature branch `feat/scaffold` | ☐ | worker | — | BOOT-03 |
## Milestone 0.0.2 — Scaffold + design system
| ID | Task | Status | Owner |
|---|---|---|---|
| SCAFF-01 | Initialize Next.js 16 + Payload 3 app in `src/` | ☐ | worker |
| SCAFF-02 | Configure pnpm workspace, TS strict, ESLint, Prettier | ☐ | worker |
| SCAFF-03 | Tailwind v3 config: port M3 tokens from stitch HTML | ☐ | worker |
| SCAFF-04 | `next/font`: Space Grotesk + Inter, CSS vars | ☐ | worker |
| SCAFF-05 | Install `lucide-react`, `@tailwindcss/forms`, `@tailwindcss/container-queries` | ☐ | worker |
| SCAFF-06 | Global utility classes: `.ghost-border`, `.glass-card`, `.neon-cta`, StatusTerminal baked vars | ☐ | worker |
| SCAFF-07 | Payload Postgres adapter + local `docker compose` for dev DB | ☐ | worker |
| SCAFF-08 | Health endpoint `/api/health` | ☐ | worker |
## Milestone 0.0.3 — Content model
| ID | Task | Status | Owner |
|---|---|---|---|
| CMS-01 | Collections: `users`, `media`, `categories`, `projects`, `posts`, `gear`, `contactSubmissions` | ☐ | worker |
| CMS-02 | Globals: `home`, `about`, `contact`, `resume`, `navigation`, `seo` | ☐ | worker |
| CMS-03 | Payload hooks: `revalidatePath` on publish | ☐ | worker |
| CMS-04 | Access control: admin-only for sensitive collections | ☐ | worker |
| CMS-05 | Seed script: placeholder content for all collections/globals | ☐ | worker |
## Milestone 0.0.4 — Pages + sections
| ID | Task | Status | Owner |
|---|---|---|---|
| UI-01 | Section components: HeroHeadline, ProjectBentoGrid, MakerMindsetCard, AudioSignalPath, StatusTerminal, ContactForm, NewsletterBand, SocialBento | ☐ | worker |
| UI-02 | `/` page composition | ☐ | worker |
| UI-03 | `/about` page composition | ☐ | worker |
| UI-04 | `/projects` + `/projects/[slug]` | ☐ | worker |
| UI-05 | `/writing` + `/writing/[slug]` | ☐ | worker |
| UI-06 | `/contact` (form + Turnstile + honeypot + rate-limit) | ☐ | worker |
| UI-07 | `/resume` + `/resume.pdf` (react-pdf or puppeteer) | ☐ | worker |
| UI-08 | Sitemap, robots, RSS | ☐ | worker |
## Milestone 0.0.5 — CI + container
| ID | Task | Status | Owner |
|---|---|---|---|
| CI-01 | Multi-stage `Dockerfile` (deps → build → runner, non-root, standalone Next output) | ☐ | worker |
| CI-02 | `.woodpecker/web.yml` — lint/typecheck/build/audit/Kaniko/Trivy/link-package | ☐ | worker |
| CI-03 | First CI run → image pushed to Gitea registry | ☐ | orchestrator |
| CI-04 | Verify image path: `jason.woltje` dot handling | ☐ | orchestrator |
## Milestone 0.0.6 — Deploy
| ID | Task | Status | Owner |
|---|---|---|---|
| DEP-01 | `docker-compose.swarm.yml` — web + postgres, traefik-public external, labels mirror mosaicstack-website | ☐ | worker |
| DEP-02 | `.env.example` committed | ☐ | worker |
| DEP-03 | **ESC-01:** Edge Traefik TLS config for `jasonwoltje.com` | 🚫 | Jason | ESC-01 |
| DEP-04 | Cloudflare DNS: A/CNAME for apex + www, proxied | ☐ | Jason | — |
| DEP-05 | Portainer stack creation on w-docker0 (endpoint 7), env vars populated | ☐ | orchestrator | DEP-03 |
| DEP-06 | First deploy smoke test: `/`, `/admin`, `/api/health`, www→apex 301 | ☐ | orchestrator | DEP-05 |
| DEP-07 | Payload admin bootstrap: Jason creates first admin user | ☐ | Jason | DEP-06 |
## Milestone 0.1.0 — MVP acceptance
See `PRD.md` §11 for full acceptance criteria. Gate conditions:
- All routes render
- Lighthouse thresholds met (Perf ≥85, A11y ≥95, SEO ≥95)
- Contact form end-to-end (submit → Payload → email)
- Headshots uploaded to media collection
- At least 1 project + 1 post published
- Release tag `v0.1.0` pushed; CI tagged build deployed
## Deferred (Phase 2)
| Task | Rationale |
|---|---|
| Auto-deploy via Portainer API | Defer until manual deploys are stable |
| Health-check gate in CI | Same |
| Umami analytics stack | Separate repo/stack |
| Mautic newsletter integration | Separate deploy; newsletter UI is stub until live |
| Offsite backups (B2/R2) | v0.1.x concern |
| MinIO migration | Triggered by volume growth |
---
## Active blockers / escalations
| ID | Description | Unblocks |
|---|---|---|
| ESC-01 | Edge Traefik TLS config for new domain `jasonwoltje.com` | DEP-03 → DEP-05+ |
| ESC-03 | SMTP/Resend credentials for contact form notifications | UI-06 email path |
| ESC-04 | Turnstile site + secret keys | UI-06 CAPTCHA |
Unblocking these does not gate scaffold work — only deploy and contact form completion.