This repository contains the source code for the personal website, blog, and resume builder: daniel.heene.io.
For AI coding agents and contributors: See
AGENTS.mdfor coding conventions, architecture notes, and known security/style guardrails before making changes.
- Language & Runtime: TypeScript / Node.js (
^26.0.0) - Framework: Next.js 16 (App Router, React 19)
- CMS: Payload CMS 3.x
- Database: MongoDB 8 (via
@payloadcms/db-mongodb) - Cache & Pub/Sub: Redis 8 (
@payloadcms/kv-redis,@trieb.work/nextjs-turbo-redis-cache, and custom SSE pub/sub handler) - Storage: S3-compatible storage (RustFS in local dev via Docker, AWS S3 / Cloudflare R2 in production via
@payloadcms/storage-s3) - Styling: Tailwind CSS 4, PostCSS, Tailwind Variants
- Component Development: Storybook 10
- Linting & Formatting: Oxlint and Oxfmt (no ESLint/Prettier)
- Package Manager: Bun (
^1.4.0, pinnedbun@1.4.2); Node.js still runs Next, Payload and the scripts - Testing: Vitest (Unit), Playwright (E2E)
- Integrations & Services:
- Analytics: Umami (Optional)
- Transactional Email: UseSend / React Email (Optional)
- Error Tracking & Tracing: Sentry (Optional)
- AI Metadata & Alt-Text: OpenAI / Anthropic (Optional)
- Address & Geocoding: Mapbox (Optional)
- Stock Photos: Unsplash API (Optional)
- Icons: Self-hosted Iconify API (
https://icons.heene.io) - Dev Tunnel: Cloudflare Tunnel (Optional)
- Node.js:
^26.0.0 - Bun:
^1.4.0(v1.4.2 recommended) - Docker & Docker Compose: For running local database (MongoDB), cache (Redis), and object storage (RustFS).
- Doppler CLI: For environment configuration and secret management.
Configuration and secrets are managed in Doppler. Install the CLI, link this directory to the project, then write the config out to .env.local:
brew install dopplerhq/cli/doppler # See docs.doppler.com/docs/install-cli for other platforms
doppler login
doppler setup --project website --config <your-config>
bun run load-env # Writes .env.local from active Doppler configdoppler setupstores the project and config against this directory in~/.doppler(one-time step per clone).bun run load-envis the only command that talks to Doppler. Next.js loads.env.localautomatically, so standard commands (bun run dev,bun run build) work without wrappingdoppler run.- Re-run
bun run load-envafter changing variables in Doppler or switching configs withdoppler setup --config <name>. bun run load-env --checkreports whether.env.localis in sync with Doppler and exits non-zero if drift is detected (without writing)..env.localis gitignored and written with owner-only permissions (0600).- To inspect secrets without writing to disk, run
doppler secrets.
Launch the infrastructure services (MongoDB 8, Redis 8, and RustFS storage with automatic bucket creation) using Docker Compose:
docker compose up -dbun installGit hooks (Lefthook, commitlint) are installed automatically during
bun install.
Payload requires generated TypeScript types and an import map for the admin panel:
bun run generateAlways run
bun run generateafter adding or changing collections, globals, blocks, or fields.
Start the development server (runs Next.js dev server and Storybook in parallel):
bun run devOr run services independently:
bun run dev:app # Next.js dev server only (http://localhost:3000)
bun run dev:storybook # Storybook only (http://localhost:6006)
bun run dev:email # React Email preview server (http://localhost:3005)- Frontend: http://localhost:3000
- Admin Panel: http://localhost:3000/admin
- Storybook: http://localhost:6006
- React Email Previews: http://localhost:3005
- Next.js App Router:
app/(frontend)/: Public website pages, layouts, and API routes (/api/preview,/api/sse,/api/heartbeat,/api/health/*, etc.). Dynamic pages are under[slug]/.app/(payload)/: Payload CMS admin panel routes (/admin).
- Payload Configuration:
payload.config.tsin the project root, integrated into Next.js viawithPayloadinnext.config.ts. - Jobs Queue & Worker:
src/jobs-queue/and standalone background worker entrypointscripts/start-worker.mjswithscripts/health-server.ts. - Dev Runner:
scripts/dev.mjs(handles--tunnelCloudflare tunnel argument and orchestrates dev processes). - Environment Loader:
scripts/load-env.mjs(fetches active Doppler secrets into.env.local).
| Script | Description |
|---|---|
bun run dev |
Starts Next.js dev server and Storybook in parallel (supports --tunnel). |
bun run dev:app |
Starts Next.js development server only (localhost:3000). |
bun run dev:storybook |
Starts Storybook development server (localhost:6006). |
bun run build |
Production build (compiles Next.js bundle with Sentry release tagging). |
bun run build:storybook |
Builds static Storybook documentation into dist/. |
bun run start:storybook |
Serves the static Storybook build on port 3020. |
bun run start:app |
Starts the production Next.js application server. |
bun run start:worker |
Runs standalone background jobs worker and health monitoring server. |
bun run load-env |
Writes .env.local from active Doppler config (--check validates drift). |
bun run generate |
Runs generate:types and generate:importmap in parallel. |
bun run generate:types |
Generates TypeScript types for Payload collections and globals (src/types/payload.ts). |
bun run generate:importmap |
Regenerates Payload admin component import map. |
bun run _payload |
Wrapper to execute Payload CLI commands (used by migrate, generate and the other scripts). |
bun run migrate |
Runs database migrations via Payload CLI. |
bun run ci |
Local convenience: runs database migrations and a full production build (CI itself runs the steps separately). |
bun run lint |
Runs oxlint and oxfmt --check (linting and format checks). |
bun run lint:fix |
Runs oxlint --fix and oxfmt to auto-fix what they can. |
bun run format |
Runs oxfmt to fix formatting and import order. |
bun run typecheck |
Runs TypeScript typechecker (tsc --noEmit). |
bun run deps:lint |
Runs Syncpack to check dependency version consistency across packages. |
bun run deps:fix |
Runs Syncpack to automatically align dependency versions. |
bun run deps:update |
Updates dependency versions using Syncpack. |
bun run chore:format |
Formats package.json field order using Syncpack. |
bun run chore:reinstall |
Cleans node_modules and bun.lock, then runs fresh bun install. |
bun run dev:email |
Starts React Email development server on port 3005 (src/emails). |
bun run seed:topics |
Seeds fixture blog topics (--clean to remove). |
bun run seed:posts |
Seeds fixture blog posts and media (--clean to remove, --count <n> to set quantity). |
bun run seed:pages |
Seeds fixture pages (--clean to remove, --count <n> to set quantity). |
bun run seed:resume-documents |
Seeds an older and a newer fixture resume document without PDFs (--clean to remove). |
bun run links:migrate |
Runs database migration for link field naming. |
bun run refs:backfill |
Rebuilds content reference index table. |
bun run test |
Runs unit tests once via Vitest. |
bun run test:watch |
Runs Vitest in interactive watch mode. |
bun run test:coverage |
Runs Vitest with v8 code coverage reporting. |
bun run test:e2e |
Runs Playwright E2E tests against .env.test. |
bun run test:e2e:ui |
Runs Playwright E2E tests with interactive UI. |
bun run test:e2e:docker |
Runs Playwright E2E tests inside Docker Chromium container. |
bun run release |
Runs semantic-release to calculate version, tag, and publish changelog. |
bun run release:dry-run |
Runs semantic-release in dry-run mode. |
The source of truth is declared as a Zod schema in src/types/environment.ts. It is validated by next.config.ts during startup, failing fast if required variables are missing or malformed.
| Variable | Type / Requirement | Purpose |
|---|---|---|
NODE_ENV |
Optional (development | production | test) |
Runtime environment (defaults to development). |
SERVER_HOST |
Required | Host/port used for server-side URL construction. |
SERVER_URL |
Required (URL) | Public URL of the server (inlined into client bundle at build time). |
PAYLOAD_SECRET |
Required | Secret key used to encrypt Payload JWT tokens. |
PREVIEW_SECRET |
Required | Shared secret for draft/preview authentication. |
CRON_SECRET |
Required | Secret token reserved for securing cron/scheduled tasks. |
| Variable | Type / Requirement | Purpose |
|---|---|---|
DATABASE_URL |
Required | MongoDB connection string (e.g. mongodb://127.0.0.1:27017/productiondb). |
REDIS_URL |
Required | Redis connection string (used for KV adapter, cache handler, and SSE pub/sub). |
| Variable | Type / Requirement | Purpose |
|---|---|---|
S3_BUCKET |
Required | S3 bucket name. |
S3_ENDPOINT |
Required | S3 API endpoint URL (e.g. http://127.0.0.1:9000 locally). |
S3_REGION |
Required | S3 region identifier (us-east-1 for local RustFS). |
S3_ACCESS_KEY |
Required | S3 access key ID. |
S3_SECRET_KEY |
Required | S3 secret access key. |
| Variable | Type / Requirement | Purpose |
|---|---|---|
PAYLOAD_JOBS_ENABLE_APP_WORKERS |
Optional (true | false) |
Enables embedded job workers in the main Next.js app process (defaults to false). |
| Variable | Type / Requirement | Purpose |
|---|---|---|
STATUS_PAGE_URL |
Required (URL) | Public status page URL (inlined into client bundle). |
STATUS_PAGE_HEARTBEAT_URL |
Required (URL) | Heartbeat monitoring URL pinged server-side. |
| Variable | Type / Requirement | Purpose |
|---|---|---|
NEXT_PUBLIC_UMAMI_URL |
Required | Umami analytics script URL / rewrite target. |
NEXT_PUBLIC_UMAMI_SITE_ID |
Required (UUID) | Umami tracking site UUID. |
UMAMI_USERNAME |
Required | Umami account username for server-side stats queries. |
UMAMI_PASSWORD |
Required | Umami account password for server-side stats queries. |
USESEND_URL |
Required (URL) | UseSend transactional email endpoint. |
USESEND_API_KEY |
Required | UseSend API authorization key. |
USESEND_DEFAULT_FROM_ADDRESS |
Required (Email) | Default sender email address. |
USESEND_DEFAULT_FROM_NAME |
Required | Default sender display name. |
| Variable | Type / Requirement | Purpose |
|---|---|---|
NEXT_PUBLIC_ICONIFY_API |
Optional (URL) | Self-hosted Iconify API (defaults to https://icons.heene.io). |
OPENAI_API_KEY |
Required | API key for OpenAI (used for automated alt-text and meta descriptions). |
ANTHROPIC_API_KEY |
Required | API key for Anthropic Claude (alternative AI generation provider). |
MAPBOX_API_KEY |
Required | Mapbox access token for address and coordinate lookups. |
UNSPLASH_ACCESS_KEY |
Optional | Unsplash API access key for stock photo search & direct media import. |
| Variable | Type / Requirement | Purpose |
|---|---|---|
SENTRY_DSN |
Optional (URL) | Enables Sentry SDK if provided (inlined at build time). |
SENTRY_ENVIRONMENT |
Optional | Custom environment name reported to Sentry. |
SENTRY_RELEASE |
Optional | Release tag reported to Sentry. |
SENTRY_TRACES_SAMPLE_RATE |
Optional | Performance trace sample rate (0 to 1). |
SENTRY_AUTH_TOKEN |
Optional | Sentry authentication token for source map upload during build. |
SENTRY_ORG |
Optional | Sentry organization slug for source map upload. |
SENTRY_PROJECT |
Optional | Sentry project name for source map upload. |
| Variable | Type / Requirement | Purpose |
|---|---|---|
CLOUDFLARE_TUNNEL_HOST |
Optional | Hostname for local dev tunnel. |
CLOUDFLARE_TUNNEL_URL |
Optional (URL) | Target URL for Cloudflare tunnel. |
CLOUDFLARE_TUNNEL_TOKEN |
Optional | Cloudflare tunnel authentication token. |
Build-Time Inlining Notice:
SERVER_URL,STATUS_PAGE_URL, andSENTRY_DSNare inlined into the client bundle at build time bynext.config.ts. Additionally, route shells rendered at build time withcacheComponents: truecapture server-side values. Ensure correct values are present duringbun run build.
.
├── app/ # Next.js App Router
│ ├── (frontend)/ # Public website routes, layouts, and API routes
│ │ ├── [slug]/ # Dynamic page routes
│ │ ├── api/ # API endpoints (preview, sse, health, heartbeat, icons, etc.)
│ │ └── layout.tsx # Frontend root layout
│ └── (payload)/ # Payload CMS admin routes
│ ├── admin/ # Payload admin UI routes
│ ├── api/ # Payload REST & GraphQL API endpoints
│ └── layout.tsx # Payload admin layout
├── src/ # Application source code
│ ├── access/ # Payload access control policies (anyone, authenticated, etc.)
│ ├── blocks/ # Reusable Payload blocks (Hero, Content, Resume, Media, etc.)
│ ├── collections/ # Payload collections (Media, Pages, Posts, Resume*, Users, etc.)
│ ├── components/ # React UI components & Storybook stories
│ ├── contexts/ # React context providers (theme, locale, etc.)
│ ├── emails/ # React Email templates
│ ├── fields/ # Reusable Payload field factories (Slug, HeroSlides, Link, etc.)
│ ├── fonts/ # Custom font definitions
│ ├── globals/ # Payload globals (SiteSettings, PDFGeneratorSettings, etc.)
│ ├── hooks/ # Custom React hooks
│ ├── jobs-queue/ # Payload background jobs, tasks, workflows, and health checks
│ ├── lib/ # Utilities (RedisHandler, SSE, image optimization, seeding, etc.)
│ ├── migrations/ # Database migration scripts
│ ├── pdf/ # PDF resume generator components (@react-pdf/renderer)
│ ├── plugins/ # Custom Payload plugins
│ ├── stories/ # Global Storybook configurations & documentation
│ ├── styles/ # CSS and Tailwind 4 stylesheets
│ ├── types/ # TypeScript declarations, environment schema, generated types
│ └── widgets/ # Custom Payload dashboard widgets
├── public/ # Static public assets (favicons, icons, robots.txt)
├── scripts/ # Utility scripts (dev runner, worker, seeders, environment loader)
├── docs/ # Specifications, architecture notes, and design plans
├── e2e/ # Playwright end-to-end test specs
├── docker-compose.yml # Local development infrastructure (MongoDB, Redis, RustFS)
├── next.config.ts # Next.js configuration & Payload integration
├── payload.config.ts # Payload CMS master configuration
├── oxlint.config.ts # Oxlint rules
├── oxfmt.config.ts # Oxfmt formatting and import order
├── lefthook.yml # Git hooks
├── bunfig.toml # Bun install settings
├── playwright.config.ts # Playwright E2E configuration
├── vitest.config.ts # Vitest unit test configuration
├── vitest.setup.ts # Vitest environment setup and mocks
└── package.json # Dependencies, scripts, and engine requirements
Unit tests are co-located next to the code under test as *.test.ts / *.test.tsx.
bun run test # Run all unit tests once
bun run test:watch # Run unit tests in interactive watch mode
bun run test:coverage # Generate test coverage report- Configuration:
vitest.config.ts. - Mocks & environment:
vitest.setup.tsforcesTZ=UTCand mockspayload,next/cache, andredis. Real implementations oftailwind-merge,date-fns,slugify,pupa, andneotraverseare tested directly.
E2E tests live in e2e/*.spec.ts and test structural health and smoke flows.
bun run test:e2e # Run Playwright tests headlessly
bun run test:e2e:ui # Open Playwright interactive UI test runner
bun run test:e2e:docker # Run tests in Playwright Docker Chromium containerE2E Prerequisites:
- Run
docker compose up -dso MongoDB, Redis, and RustFS storage are active. - E2E tests execute using
.env.testwith dummy valid credentials. bun run test:e2eautomatically starts or reuses the local dev server. For Docker tests (bun run test:e2e:docker), start the dev server beforehand withbun run dev:app.
Seed fixture content into local MongoDB for development and testing:
# Seed Topics (categories)
bun run seed:topics # Creates fixture blog topics
bun run seed:topics:clean # Removes seeded blog topics
# Seed Blog Posts (with images)
bun run seed:posts # Creates blog topics & 30 posts with downloaded images
bun run seed:posts:clean # Removes seeded posts and associated media
# Pass custom count via: bun run seed:posts -- --count 10
# Seed Pages
bun run seed:pages # Creates standard fixture pages
bun run seed:pages:clean # Removes seeded pages
# Pass custom count via: bun run seed:pages -- --count 5- Seeders are idempotent and match by slug.
- Article prose and layouts are deterministic per title using a seeded pseudo-random generator.
- Images are downloaded from
picsum.photosand uploaded into Payload media.
Everything is built in CI; Docker images only COPY finished output (no package manager, installs or builds inside an image). The checks live in .github/workflows/lint-and-test.yml; branch protection on main/develop requires them to pass before a PR can merge, so pushes (build-and-deploy.yml) do not re-run them.
lint-and-test.yml(pull requests only):- Lint:
commitlint(PRs),oxlint+oxfmt --check,syncpack, a check that the committed generated files matchbun run generate, andactionlint. - Semgrep: static security analysis (Node.js sinks, TypeScript/React patterns, hardcoded secrets).
- Unit Tests:
vitest run --coverage. - E2E (environment
Testing): resets the shared Testing services (Mongo and Redis rebuilt through Dokploy, S3 bucket emptied;scripts/reset-test-services.mjs), migrates, seeds, runs a singlenext buildagainst.env.test, assemblesout/weband runs Playwright againstnode out/web/server.js. Forks and dependabot cannot read the Testing secrets and use local containers (docker compose) instead.
- Lint:
- Pushes to
develop/main(build-and-deploy.yml):- Semantic Versioning: semver bump and tag/changelog via
semantic-release.maincuts releases (vX.Y.Z, images also taggedlatest),developcuts release candidates (vX.Y.Z-rc.N, images also taggededge). After amainrelease,developis fast-forwarded tomainwhen it has no commits of its own; otherwise it has to be brought up to date by hand, sincedeveloponly cuts release candidates while it contains everymainrelease. - Deploy (environment
Productiononmain,Developmentondevelop): runsbun run migrateagainst the target database, runs a singlenext buildwith that environment's real values, assemblesout/{web,worker,storybook}(scripts/assemble-images.mjs), builds the COPY-only images (app,worker,storybookinDockerfile) forlinux/amd64without pushing, starts each one as a real container and polls its health endpoint, then pushes the verified images toghcr.io/danielheene/website/*and triggers the Dokploy redeploy webhooks. A failed boot-check stops the job before anything reaches the registry.
- Semantic Versioning: semver bump and tag/changelog via
The push-triggered deploy build also uploads source maps to the self-hosted Rustrak Sentry backend via a standalone sentry-cli step; the PR-time build does not, since it never ships and there'd be no deployed release to attach them to. See AGENTS.md's "Observability" note for why this isn't done through withSentryConfig.
Lefthook (lefthook.yml) manages Git hooks, installed automatically during bun install:
pre-commit: Runsoxfmtandoxlint --fixon staged files, and syncpack whenpackage.jsonchanges.commit-msg: Enforces Conventional Commits format usingcommitlint.
Example valid commit messages:
feat(admin): add Iconify icon picker field
fix(media): sanitize SVG uploads before rendering
chore: update dependencies
This project is licensed under the MIT License.