Personal website with blog and interactive resume.
Live: robbiepalmer.me
- Framework: Next.js 15 (App Router, Static Export)
- Language: TypeScript (strict mode)
- Styling: Tailwind CSS 4
- Components: shadcn/ui
- UI Library: React 19
- Format: MDX with strict frontmatter validation
- Blog Posts: Located in
ui/content/blog/(authoring guide) - Projects: Located in
ui/content/projects/(authoring guide) - Images: Cloudflare Images with CalVer versioning (YYYY-MM-DD)
- Hosting: Cloudflare Pages
- CDN & Images: Cloudflare Images (automatic WebP/AVIF conversion, responsive)
- DNS: Cloudflare DNS
- IaC: Terraform with Terraform Cloud remote state (workspace)
- Task Runner: mise with monorepo tasks (see
.mise.toml) - Linting: Biome (TypeScript/JavaScript), markdownlint (Markdown), remark (MDX), yamllint (YAML)
- Formatting: Biome
- Type Checking: TypeScript
- Pre-commit: Husky + lint-staged
- Performance QA: Lighthouse (via
mise //ui:lighthouse:serve) - Dependency Management: Renovate (automated PRs)
- Code Review: CodeRabbit (AI-powered PR reviews)
- Project Management: Shortcut (see ADR 034)
- AI Pair Programming: Claude (see AGENTS.md for agent guide)
-
Install mise (one-time)
curl https://mise.run | sh -
Add mise to your shell config (one-time)
echo 'eval "$(mise activate bash)"' >> ~/.bashrc # or ~/.zshrc for zsh source ~/.bashrc # reload your shell
-
Clone and enter the project directory
cd personal-site -
Install dependencies
mise //ui:install
-
Start dev server
mise //ui:dev
Visit http://localhost:3000
To run the frontend together with the recipe API Worker for OAuth development,
configure workers/recipe-api/.dev.vars as described in the
recipe API setup guide, then run:
mise run //:devpersonal-site/
├── .github/ # CI/CD workflows, issue templates
│ └── workflows/ # UI CI, Infra CI/CD
├── .claude/ # AI agent config
│ └── commands/ # Custom Claude slash commands
├── infra/ # Infrastructure as Code
│ └── cloudflare/ # Terraform configs for Cloudflare Pages, DNS
├── ui/ # Next.js application
│ ├── app/ # Next.js App Router pages
│ ├── components/ # React components
│ ├── content/ # Content files
│ │ └── blog/ # Blog posts in MDX (see authoring guide)
│ ├── lib/ # Shared utilities and config
│ ├── public/ # Static assets (fonts, tech icons, etc. - blog post images use Cloudflare Images)
│ ├── scripts/ # Utility scripts (image sync, health checks)
│ └── tests/ # Vitest tests
├── .mise.toml # Root mise config (repo-wide tasks)
├── package.json # Root package (repo-wide dev tooling)
└── docs/ # Private technical documentation
This project uses mise's monorepo tasks feature for managing tasks across multiple projects.
mise //ui:dev # Start dev server
mise //ui:build # Build for production
mise //ui:check # Run all checks (lint, format, typecheck, test)
mise //ui:test # Run tests
mise //ui:test:watch # Run tests in watch modemise //:lint # Lint all files (Markdown, MDX, YAML, TypeScript)
mise //:lint:markdown # Lint Markdown files
mise //:lint:mdx # Lint MDX files
mise //:lint:yaml # Lint YAML files
mise //ui:lint # Lint TypeScript/JavaScript
mise //ui:format # Format and fix with Biomemise //infra:format # Format Terraform files
mise //infra:lint # Lint Terraform filesmise //ui:images:sync # Upload blog images to Cloudflare Images
mise //ui:images:health-check # Validate Cloudflare Images setupList all tasks:
mise tasks --allLearn more: mise monorepo tasks documentation
This project has multiple levels of testing, from local development to production-like previews:
Run the dev server with hot module replacement for rapid iteration:
mise //ui:devVisit http://localhost:3000 in your browser. Changes to code will hot-reload automatically.
Test on real mobile devices connected to the same network:
mise //ui:dev:qrThis displays a QR code you can scan with your phone to open the dev server. Useful for testing responsive layouts, touch interactions, and mobile-specific behavior on actual devices.
When you open a pull request, GitHub Actions automatically runs:
- Build:
next build- ensures the static export compiles - Lint: Biome, markdownlint, remark, yamllint
- Type Check: TypeScript strict mode
- Tests: Vitest unit and integration tests
All checks must pass before merging.
Every PR automatically gets a preview deployment on Cloudflare Pages. This lets you test on real production infrastructure before merging:
- Full static build deployed to Cloudflare's edge network
- Production-like environment (CDN, headers, routing)
- Shareable URL for team review
Preview URLs appear as a comment on the PR once the deployment completes.
Run Lighthouse audits to catch performance regressions:
# Against local build (builds, serves, and runs Lighthouse)
mise //ui:lighthouse:serve
# Against preview deployment
LIGHTHOUSE_URL=https://preview-abc123.personal-site.pages.dev mise //ui:lighthouse
# Against production (as baseline for comparison)
LIGHTHOUSE_URL=https://robbiepalmer.me mise //ui:lighthouseCompare scores across environments to catch regressions before they reach production.
Automatic linting and formatting runs on commit. See:
.husky/pre-commit- Hook entry pointpackage.json(lint-staged) - File patterns and commands.mise.toml- Task definitions (single source of truth for all tooling).markdownlint.json,.yamllint- Linter configurations
New blog post authors should read the Blog Authoring Guide which covers:
- Frontmatter format specification
- Image sourcing and optimization guidelines
- How to sync images to Cloudflare Images
- Alt text best practices
- Local testing workflow
- Platform: Cloudflare Pages
- Trigger: Automatic on push to
main - Preview Deployments: Automatic on pull requests
- Build: GitHub Actions builds and Cloudflare Pages deploys
- Secrets: Loaded from GitHub environment variables (configured in Terraform)
- Dashboard: Cloudflare Pages Dashboard → Your Account → Workers & Pages → personal-site
- Tool: Terraform with Terraform Cloud remote state
- Secrets: Loaded from GitHub environment variables (Cloudflare API tokens, etc.)
- Workflow:
- PR opened → CI runs
terraform plan, posts plan as PR comment - PR merged to
main→ CD automatically runsterraform apply
- PR opened → CI runs
- Workspace: Terraform Cloud
Blog posts include OpenGraph and Twitter Card metadata for rich social media previews. These require manual testing since preview generation happens on external platforms:
Testing Tools:
- Open Graph Debugger: https://www.opengraph.xyz/ (shows previews for Twitter/X, LinkedIn, Facebook, WhatsApp, Discord)
- Alternative: Post URL in a draft tweet/post to see live preview on the actual platform
What to verify:
- Images display correctly (1200x630px for OG)
- Title, description, and alt text appear
- Correct card type (summary_large_image for Twitter)
- No broken images or missing metadata
When to test:
- After updating blog post featured images
- After changing site metadata configuration
- Cloudflare Pages → Workers & Pages → personal-site
- Cloudflare Images → Images
- Cloudflare DNS → Websites → robbiepalmer.me → DNS
- Terraform Cloud Workspace
- GitHub Repository
- Renovate Dashboard
- CLA Assistant
- mise documentation
- mise monorepo tasks
- Next.js documentation
- Cloudflare Pages documentation
- Cloudflare Images documentation
- Terraform documentation
- Lighthouse documentation
- CodeRabbit documentation
If you're an AI coding agent (like Claude), read AGENTS.md for:
- Build system details (mise usage)
- Project structure and conventions
- Coding patterns and best practices
All PRs must pass:
- Linting (Biome, markdownlint, remark, yamllint)
- Type checking (TypeScript)
- Tests (Vitest)
- Build (Next.js static export)
- Terraform validation (if infra changed)
- CodeRabbit: Automated AI reviews on all PRs
- Manual Review: By repository owner