+ {project.name} +
+ {project.years} +{project.meta}
+{project.description}
+diff --git a/.env-sample b/.env-sample deleted file mode 100644 index 2004362..0000000 --- a/.env-sample +++ /dev/null @@ -1 +0,0 @@ -GITHUB_OAUTH_CLIENT_ID=your-client-id \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0e734d..d6ce15a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,11 +6,17 @@ on: pull_request: branches: [master] +# Every job here only reads the repo and runs the build/test tooling. +permissions: + contents: read + jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: @@ -23,6 +29,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: @@ -35,6 +43,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: @@ -47,6 +57,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index db49f91..8dc84ed 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,9 +3,6 @@ name: Deploy on: push: branches: [master] - # Refresh the deployed GitHub stats on the 1st of each month, even without a push. - schedule: - - cron: '0 7 1 * *' workflow_dispatch: concurrency: @@ -27,13 +24,6 @@ jobs: node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile - # Fetch the latest public GitHub stats at build time. The committed - # data/github-stats.json is only a fallback, so a transient API failure - # degrades to slightly stale stats instead of breaking the deploy. - - name: Fetch GitHub stats - env: - GH_STATS_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: pnpm run fetch-github || echo "Stats fetch failed; building with committed data/github-stats.json" - run: pnpm run build - uses: cloudflare/wrangler-action@v3 with: diff --git a/.gitignore b/.gitignore index 3a8a09c..277fab7 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,10 @@ package-lock.json /.svelte-kit /build +# Local tooling scratch (playwright-cli sessions, static-analysis reports) +.playwright-cli +.static-analysis + # OS .DS_Store Thumbs.db diff --git a/.prettierignore b/.prettierignore index 5df8b82..15a4bce 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,5 +3,8 @@ package-lock.json pnpm-lock.yaml yarn.lock -# Generated data -data/github-stats.json +# Generated by the static-analysis skill (git-ignored globally) +.static-analysis/ + +# Machine-local Claude Code settings (git-ignored globally, never reaches CI) +.claude/settings.local.json diff --git a/WARP.md b/WARP.md index 1a4af06..9d22c0e 100644 --- a/WARP.md +++ b/WARP.md @@ -4,239 +4,128 @@ This file provides guidance to WARP (warp.dev) when working with code in this re ## Overview -Nathan Arthur's personal website built with SvelteKit as a static site generator. Features dynamic GitHub statistics integration, project showcase, and responsive design with Tailwind CSS. +Nathan Arthur's personal website: a small static SvelteKit site — a home page, +`/writing`, `/uses`, and one case study — deployed to Cloudflare Workers assets +at nathanarthur.com. See `knowledge.md` for the design and content rules; they +are load-bearing, not decoration. ## Development Commands -### Core Development - ```bash -# Install dependencies (project uses pnpm) -pnpm install - -# Start development server (avoid in WARP to prevent blocking) -pnpm dev - -# Build static site -pnpm build - -# Preview production build locally -pnpm preview - -# Type checking -pnpm check - -# Watch mode type checking +pnpm install # project uses pnpm +pnpm dev # dev server (avoid in WARP to prevent blocking) +pnpm build # static build into ./build +pnpm preview # preview the production build +pnpm check # svelte-check pnpm check:watch +pnpm test # vitest, single run +pnpm lint # prettier --check + eslint +pnpm format # prettier --write ``` -### Testing & Quality - -```bash -# Run tests (use --run to avoid watch mode in WARP) -pnpm test - -# Run Vitest directly with no-watch -pnpm vitest --run - -# Lint code (ESLint + Prettier) -pnpm lint - -# Format code -pnpm format -``` - -### GitHub Statistics Integration - -```bash -# Install dependencies for GitHub stats fetching -pnpm prefetch-github +## Architecture -# Fetch GitHub stats (requires GITHUB_OAUTH_CLIENT_ID in .env) -pnpm fetch-github -``` +### Static site generation -## High-Level Architecture +- **SvelteKit 5** with `@sveltejs/adapter-static` +- Pre-rendering enabled via `export const prerender = true` in `src/routes/+layout.ts` +- **No runtime data fetching at all.** `/uses` reads `src/routes/uses/uses.yaml` + at build time in `+page.ts` (Vite `?raw` import, parsed with js-yaml), so it + prerenders to static HTML and renders with JS disabled -### Static Site Generation +### Technology stack -- **SvelteKit** with `@sveltejs/adapter-static` for GitHub Pages deployment -- **Pre-rendering** enabled via `export const prerender = true` in root layout -- **Build-time data fetching** using static JSON files rather than runtime API calls +- SvelteKit 5, TypeScript, Tailwind CSS (no plugins), Vite, Vitest, ESLint + Prettier -### GitHub Integration Flow +### Project structure -1. **OAuth Device Flow**: `scripts/fetch-github-stats.ts` handles authentication -2. **Data Fetching**: Pulls user profile, repositories, and language statistics -3. **Static Generation**: Data stored in `data/github-stats.json` for build-time consumption -4. **Display**: `src/services/github/api.ts` processes static data for components - -### Technology Stack - -- **SvelteKit 5** for framework and routing -- **TypeScript** for type safety throughout -- **Tailwind CSS** with plugins for forms, typography, container queries -- **Vite** for build tooling and dev server -- **Vitest** for unit testing -- **ESLint + Prettier** for code quality - -## Project Structure - -### Key Directories - -``` +```text src/ -├── routes/ # SvelteKit file-based routing -│ ├── +layout.svelte # Root layout with dark mode support -│ ├── +layout.ts # Layout load function (prerender: true) -│ ├── +page.svelte # Home page with project showcase -│ └── uses/ # "/uses" page route -├── components/ # Reusable Svelte components -│ ├── GithubStats.svelte # GitHub statistics display -│ ├── ProjectList.svelte # Searchable project showcase -│ └── SubscribeForm.svelte -├── services/ # Business logic layer -│ └── github/ # GitHub API integration -│ ├── api.ts # Data processing from static JSON -│ ├── types.ts # TypeScript interfaces -│ └── colors.ts # Language color mapping -└── types/ # Global type definitions -``` - -### Data Flow - -1. **Build Time**: Static JSON (`data/github-stats.json`) consumed by services layer -2. **Component Layer**: `GithubStats.svelte` calls `fetchGithubStats()` from `services/github/api.ts` -3. **Data Processing**: API service calculates statistics and language percentages -4. **Rendering**: Components receive processed data for display - -## GitHub Statistics System - -### Authentication Setup - -1. Create GitHub OAuth app and get client ID -2. Copy `.env-sample` to `.env` and set `GITHUB_OAUTH_CLIENT_ID` -3. Run `pnpm prefetch-github` to install required dependencies - -### Data Fetching Process - -```bash -# Authenticate via OAuth device flow (opens browser) -pnpm fetch-github +├── app.html #
and the Supascribe loader script +├── app.css # Tailwind entry point +└── routes/ + ├── +layout.svelte # page column, footer, global link/focus styles + ├── +layout.ts # prerender: true + ├── +page.svelte # home page (content lives in this file) + ├── +error.svelte # 404 page (emitted as build/404.html) + ├── audioverse/+page.svelte + ├── writing/+page.svelte # newsletter + Beeminder articles + └── uses/ + ├── +page.ts # parses uses.yaml at build time + ├── +page.svelte # renders it; owns only the tag-filter state + ├── filter.ts # tag/category logic — the only tested code + ├── filter.spec.ts + └── uses.yaml ``` -The script: +There is no `src/components/`. The footer lives in the layout, its only consumer. -1. Initiates OAuth device flow authentication -2. Fetches user profile data for 'narthur' -3. Retrieves all public repositories (up to 100) -4. Collects language statistics for each repository -5. Saves complete dataset to `data/github-stats.json` +Home page content — the positioning line, featured work, "also built" — is plain +data at the top of `src/routes/+page.svelte`. Editing the site's content means +editing those arrays; there is no CMS. -### Data Processing +## Development patterns -- **Stars**: Aggregated across all repositories -- **Languages**: Byte counts summed and converted to percentages -- **Top Languages**: Shows top 5, with remainder grouped as "Other" -- **Caching**: No runtime caching needed - data is static at build time +### SvelteKit conventions -## Development Patterns +- `+page.svelte` for pages, `+layout.svelte` for layouts +- PascalCase for `.svelte` component filenames -### SvelteKit Conventions +### Styling -- **File Routes**: `+page.svelte` for pages, `+layout.svelte` for layouts -- **Load Functions**: `+layout.ts` with `prerender: true` for static generation -- **Component Naming**: PascalCase for `.svelte` components +- **Tailwind first** — utility classes directly in markup +- **Dark only.** There is no light theme, no `dark:` variants, and no toggle. + The palette is six tokens in `tailwind.config.js`: `bg`, `ink`, `mute`, + `faint`, `rule`, `accent`. Don't add colors outside them. `accent` resolves to + the CSS variable `--accent`, declared on `:root` in `+layout.svelte` — change + the accent there, not in the Tailwind config. +- **No `@apply`** in Svelte ` diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 18dc6d6..0279f37 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,71 +1,106 @@ -+ {positioning} +
-Full-stack web developer
+{project.meta}
+{project.description}
+- Technology Director, 2019–2025 -
- -- AudioVerse is a non-profit media platform hosting a large library of audio and video - recordings. I led the rebuild of its public frontend on Next.js, working from designs by an - outside design firm, and directed the work on the GraphQL backend and admin dashboard behind - it. -
-- The rebuild shipped with an accessibility backlog too large to clear in one pass, which is why - I built Pa11y Ratchet. The CI action only fails on an increase in number of accessibility issues, allowing a team - to whittle an existing backlog down over time. -
-- These are captures from 2024. The live audioverse.org has changed since, and no longer represents my work. -
-+ ← Nathan Arthur +
-technology director · 2019—2025 · next.js · graphql
-- ← Back home +
+ AudioVerse is a non-profit media platform hosting a large library of audio and video recordings. + I led the rebuild of its public frontend on Next.js, working from designs by an outside design + firm, and directed the work on the GraphQL backend and admin dashboard behind it. +
++ The rebuild shipped with an accessibility backlog too large to clear in one pass, which is why I + built Pa11y Ratchet. The CI action only fails on an increase in number of accessibility issues, allowing a team to + whittle an existing backlog down over time.
++ These are captures from 2024. The live audioverse.org has changed since, and no longer represents my work. +
+- Here's a list of hardware, software, and tools I use on a daily basis for work and personal - projects. This page is inspired by uses.tech. -
- - {#if isLoading} -- Sorry, there was a problem loading the tools and equipment data. Please try again later. -
-- Try selecting different tags or clear your filters to see all items. -
- -+ ← Nathan Arthur +
+ ++ Hardware, software, and tools I use day to day for work and personal projects. Inspired by + uses.tech. +
+ ++ Nothing matches those tags. + . +
+{:else} + {#each visible as group (group.category)} +{item.description}
+ {#if item.tags?.length} +Last updated: {meta?.lastUpdated}
- {#if meta?.affiliateDisclaimer} -{meta.affiliateDisclaimer}
- {/if} -Last updated: {data.meta.lastUpdated}
+ {#if data.meta.affiliateDisclaimer} +{data.meta.affiliateDisclaimer}
+ {/if}+ ← Nathan Arthur +
+ ++ I write a mostly-weekly newsletter, and I've written for the Beeminder blog. +
+ ++ Where most of my writing goes. Tools I'm using, things I've built, and what I'm figuring out + about running a one-person software business. Sign up at the bottom of any page. +
+{post.blurb}
+