Skip to content

saucy-tech/personal-site

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

212 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Personal Portfolio & Blog

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").

Features

  • ⚑ 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

Project Structure

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

Tech Stack

  • 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)

Getting Started

  1. Clone the repository:

    git clone https://github.com/saucy-tech/personal-site.git
    cd personal-site
  2. Install dependencies:

    corepack enable
    pnpm install
  3. Run the development server:

    pnpm dev
  4. Open http://localhost:3000 in your browser.

Daily Devotion Workflow

Daily Word devotions are the primary content pipeline. The lifecycle:

  1. Brandon drafts the lesson in his Sunday School Obsidian vault.
  2. The the-daily-word Cowork skill reads today's entry, generates an MDX post under src/posts/YYYY-MM-DD-slug.mdx, and opens a draft PR on this repo labeled devotion.
  3. On merge to main, .github/workflows/devotion-broadcast.yml fires and sends the post to ConvertKit (the "The Daily Word" email list) using KIT_API_KEY.
  4. 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.

Contributing

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.

Development

Environment Variables

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 β†’ Advanced

GitHub Actions Secrets

The 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.

Component Architecture

The app uses a consistent vertical spacing system:

  • Main sections are spaced using Tailwind's space-y-section utility
  • 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

Seasonal Features

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:

  1. Open src/app/layout.tsx
  2. Add the import: import ClientSnowflakes from '@/components/ClientSnowflakes';
  3. 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 animation
  • src/components/ClientSnowflakes.tsx β€” client-side toggle with reduced-motion support

Development Tips

  1. Content Updates: Most content is defined in src/app/page.tsx as JavaScript objects
  2. Adding Links: Add new <LinkCard> components within appropriate <Section> components
  3. Profile Changes: Update the profileData object with your information
  4. Styling: Use Tailwind CSS classes for styling - avoid custom CSS where possible
  5. Images: Place images in the public directory and reference them in components
  6. Spacing: Use the built-in spacing utilities for consistent layout

Content Quality Validation

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-72 chars)
  • excerpt length target (90-180 chars)
  • 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 lint
  • pnpm test
  • pnpm content:validate
  • pnpm content:check-links
  • pnpm content:check-images
  • pnpm 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.md
  • docs/runbooks/content-guardrails.md

Deployment

The easiest way to deploy your app is to use the Vercel Platform.

Ensure all environment variables are configured in your deployment platform.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgements

  • 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

CSP on Vercel: Production-compatible configuration

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:

  1. Local: verify headers
  2. 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

About

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors