A modern, accessible portfolio and blog built with Next.js, React, and MDX. This site showcases my projects, technical talks, and blog posts, with a focus on technology, user experience, and performance.
This repo uses pnpm pinned via Corepack (see package.json "packageManager").
- β‘ Built with Next.js 16 (App Router), React 19, and Tailwind CSS
- π Custom blog with MDX support and syntax highlighting
- π» Portfolio page (
/portfolio) with detailed tech stack and features - π€ Talks & sermons archive
- β‘ Lightning Network integration for tips and donations
- π Responsive design with dark mode support
- π¨ Subtle UI animations and clean, modern design
- βΏ Accessibility and performance best practices
- π All content managed locally with Git
- π± Mobile-first, responsive layout
- π‘ RSS feed and sitemap generation
personal-site/
βββ .github/workflows/ # CI, devotion broadcast, lighthouse
βββ docs/ # Architecture map, runbooks, upgrade matrices
βββ public/ # Static assets
β βββ images/blog/ # Blog post images
βββ scripts/ # Content validators, broadcast, doc gen
βββ src/
β βββ app/ # Next.js App Router (api, blog, portfolio, ...)
β βββ components/ # Reusable UI (LinkCard, Section, SocialBar, ...)
β βββ posts/ # MDX blog posts (frontmatter parsed by gray-matter)
β βββ types/ # Shared TypeScript types
β βββ utils/ # posts.ts, security.ts, constants.ts
β βββ proxy.ts # CSP/security headers (Next.js file convention)
βββ tests/ # Playwright E2E specs
βββ AGENTS.md # Codebase facts (canonical agent doc)
βββ CLAUDE.md # Claude-specific guide (writing rules, gotchas)
βββ next.config.js
βββ package.json
βββ README.md
- Framework: Next.js 16 (App Router, Turbopack)
- Language: TypeScript 6
- UI: React 19, Tailwind CSS 4,
@tailwindcss/typography - Content: MDX for blog posts (
next-mdx-remote,gray-matter) - Animations: Framer Motion
- Icons: Heroicons
- Payments: Lightning Network integration (Alby SDK / NWC)
- Testing: Jest + React Testing Library, Playwright (E2E)
- Deployment: Vercel
- Linting: ESLint 9 + Prettier (Husky + lint-staged pre-commit)
-
Clone the repository:
git clone https://github.com/saucy-tech/personal-site.git cd personal-site -
Install dependencies:
corepack enable pnpm install -
Run the development server:
pnpm dev
-
Open http://localhost:3000 in your browser.
Daily Word devotions are the primary content pipeline. The lifecycle:
- Brandon drafts the lesson in his Sunday School Obsidian vault.
- The
the-daily-wordCowork skill reads today's entry, generates an MDX post undersrc/posts/YYYY-MM-DD-slug.mdx, and opens a draft PR on this repo labeleddevotion. - On merge to
main,.github/workflows/devotion-broadcast.ymlfires and sends the post to ConvertKit (the "The Daily Word" email list) usingKIT_API_KEY. - Vercel deploys the post to production.
Writing rules, frontmatter schema, and agent gotchas live in CLAUDE.md. Codebase architecture, security, and full env var reference live in AGENTS.md.
pnpm broadcast is retained as a manual fallback only and is not the
primary publishing path.
Contributions are welcome β open an issue or PR. Run pnpm quality:gate before
pushing; CI runs the same gate plus build and E2E smoke tests.
Create a .env.local file with the following variables:
# Required for Lightning Network functionality
NOSTR_WALLET_CONNECT_URL=your_nwc_url_here
# Optional LNURL-p configuration
LNURL_MIN_SENDABLE=1000
LNURL_MAX_SENDABLE=1000000000
LNURL_COMMENT_ALLOWED=280
LNURL_METADATA_TEXT="Tip to brandon"
LNURL_METADATA_DESC="Lightning tip jar for brandon"
# Next.js App URL (for OpenGraph)
NEXT_PUBLIC_APP_URL=https://your-domain.com
# ConvertKit API configuration for email subscriptions (optional)
CONVERTKIT_API_KEY=
CONVERTKIT_FORM_ID=
# ConvertKit v4 API β optional manual fallback for `pnpm broadcast`
CK_SECRET_KEY= # Settings β Advanced β API Secret
CK_PUBLISHER_ID= # numeric account ID from Settings β AdvancedThe devotion broadcast workflow is the primary production path. Configure these in GitHub β Settings β Secrets and variables β Actions:
| Name | Type | Required by | Description |
|---|---|---|---|
KIT_API_KEY |
Secret | devotion-broadcast.yml |
ConvertKit API key for the merge-based broadcast workflow |
NEXT_PUBLIC_APP_URL |
Variable | devotion-broadcast.yml |
Your production site URL |
pnpm broadcast is retained as a manual fallback. It creates a draft broadcast from the latest post by frontmatter date and should not be treated as the primary publishing path.
The app uses a consistent vertical spacing system:
- Main sections are spaced using Tailwind's
space-y-sectionutility - Each section maintains its own internal spacing
- Link cards within sections use consistent padding and margins
- The profile section sits at the top with proper spacing to content below
- Social bar and sections maintain visual hierarchy through spacing
The snowflake animation (falling snow with a toggle button) is a winter-only feature. It was removed from the active layout because it is distracting outside of winter, especially on mobile.
To re-enable it for the winter season:
- Open
src/app/layout.tsx - Add the import:
import ClientSnowflakes from '@/components/ClientSnowflakes'; - Add the component inside the background
<div>, after<ClientGalaxyBackground />:{/* Winter snowflakes with toggle */} <ClientSnowflakes />
The component files are preserved at:
src/components/Snowflakes.tsxβ canvas-based snowflake animationsrc/components/ClientSnowflakes.tsxβ client-side toggle with reduced-motion support
- Content Updates: Most content is defined in
src/app/page.tsxas JavaScript objects - Adding Links: Add new
<LinkCard>components within appropriate<Section>components - Profile Changes: Update the
profileDataobject with your information - Styling: Use Tailwind CSS classes for styling - avoid custom CSS where possible
- Images: Place images in the
publicdirectory and reference them in components - Spacing: Use the built-in spacing utilities for consistent layout
Run pnpm content:validate before opening a PR that adds or updates posts.
This command validates all MDX post metadata and quality constraints:
- title length target (
20-72chars) - excerpt length target (
90-180chars) - duplicate tag detection (warning)
- missing tags detection (warning)
The command exits non-zero for quality errors and is enforced in CI.
Run pnpm content:check-links to verify internal blog links and static image/icon references from MDX content.
This check:
- fails for broken
/blog/<slug>links - fails for missing
/images/*or/icons/*assets - warns on relative links and posts with no inbound links from other posts
Run pnpm content:check-images to validate MDX image hygiene.
This check:
- fails on missing alt text in markdown image syntax
- fails when a local image reference is missing in
public/ - warns on large images and fails for oversized images
Run pnpm docs:architecture to regenerate docs/architecture-map.md after adding routes, API endpoints, or major utility/component files.
Run pnpm quality:gate as the local "definition of done" check before pushing. It runs:
pnpm lintpnpm testpnpm content:validatepnpm content:check-linkspnpm content:check-imagespnpm security:drift
CI now uses this same quality:gate command before build and E2E smoke checks.
For framework/runtime modernization work, use the upgrade contract matrix:
docs/testing/framework-upgrade-test-matrix.md
Operational runbooks:
docs/runbooks/api-incident-response.mddocs/runbooks/content-guardrails.md
The easiest way to deploy your app is to use the Vercel Platform.
Ensure all environment variables are configured in your deployment platform.
This project is licensed under the MIT License - see the LICENSE file for details.
- Design inspired by modern link aggregation services
- Galaxy animation adapted from various open source implementations
- Built with the Next.js framework
- Lightning Network integration via Alby SDK
This project uses a Content Security Policy (CSP) applied via Next.js proxy (formerly middleware) to work consistently in both local development and Vercel production/preview deployments.
Key files:
What was fixed:
- Environment-aware CSP: production on Vercel vs local development are handled explicitly in src/utils/security.ts.
- Canvas-safe directives: allows 2D canvas animations and blobs for the animated galaxy background.
- Worker allowances: permits blob/data workers sometimes needed for canvas ops.
- Removed unsafe-eval in production, retained only where needed in development.
- Removed X-Powered-By in Next.js config to avoid leaking server info via next.config.js.
Effective CSP highlights (production):
- default-src 'self'
- script-src 'self' 'unsafe-inline' blob: (no 'unsafe-eval' in production)
- style-src 'self' 'unsafe-inline' data:
- img-src 'self' data: blob: https: (required for canvas pixel operations and assets)
- media-src 'self' data: blob:
- worker-src 'self' blob: data:
- child-src 'self' blob: data:
- connect-src 'self' https://api.coingecko.com https: wss:
- font-src 'self' data: https:
- object-src 'none', frame-src 'none', frame-ancestors 'none'
- base-uri 'self', form-action 'self'
- upgrade-insecure-requests (production only)
Why proxy (not meta tags):
- Vercel's edge/runtime headers are stricter than local; setting CSP at the edge ensures consistent behavior across environments.
- Proxy allows environment-aware CSP that differs between dev and production.
Verification steps:
- Local: verify headers
- curl -I http://localhost:3000
- Confirm presence of:
- content-security-policy header
- img-src includes data:, blob:, https:
- worker-src and child-src include blob: data:
- In development only: script-src includes 'unsafe-eval'
- Vercel preview/prod:
- Deploy to Vercel (preview)
- Open devtools Network tab on first load (not client-side navigated page)
- Check Response Headers on the document:
- content-security-policy is present (no duplicates)
- script-src has no 'unsafe-eval' in prod
- img-src has data: blob: https:
- worker-src/child-src allow blob: data:
- Confirm the animated galaxy background renders immediately on first load and interactive UI works.
Operational notes:
- Do not add another CSP header via next.config.js; CSP is centralized in src/proxy.ts using src/utils/security.ts.
- Avoid adding meta http-equiv="Content-Security-Policy" tags; headers beat meta and Vercel may enforce more strictly.
- If adding new external APIs or CDNs, extend connect-src, font-src, img-src, etc., explicitly in src/utils/security.ts.
- If adding WebAssembly or specialized workers, ensure script-src and worker-src account for those needs without broadening to unsafe directives in production.
Troubleshooting:
- If the galaxy canvas is blank only on first navigation in Vercel:
- Ensure the page load (not client-routed) response has the CSP header with the directives above.
- Check that no second CSP header is present (duplicates can override each other). Keep CSP only in proxy.
- If fonts or images fail in Vercel but work locally, verify the corresponding src directives (font-src/img-src) include https: and data:/blob: as applicable.
- If you see blocked scripts in production, verify that no 'unsafe-eval' is required.
Security posture:
- Production avoids 'unsafe-eval' (removed).
- Uses 'unsafe-inline' for scripts/styles (required for Next.js compatibility).
- No frames or objects allowed.
- Permissions-Policy is set with conservative defaults in next.config.js headers; CSP and related security headers are set in proxy.
Change log (CSP):
- Centralized and hardened CSP in src/utils/security.ts
- Ensured environment-aware behavior for Vercel deployments
- Set poweredByHeader: false in next.config.js to remove X-Powered-By