diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9b4a0f7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,225 @@ +# CLAUDE.md + +This file provides context for AI assistants working in this repository. + +## Project overview + +Personal website and portfolio for [fmeyer.dev](https://fmeyer.dev), built with Nuxt 4, Nuxt UI, and Nuxt Content. The site is statically generated and deployed via GitHub Actions. + +## Tech stack + +- **Framework**: Nuxt 4 (with `app/` directory convention) +- **UI**: `@nuxt/ui` v4 (Tailwind CSS v4, component library) +- **Content**: `@nuxt/content` v3 (YAML-based content collections) +- **Images**: `@nuxt/image`, `nuxt-og-image` +- **Animations**: `motion-v` +- **Icons**: `@iconify-json/lucide` (prefix `i-lucide-`) and `@iconify-json/simple-icons` (prefix `i-simple-icons-`) +- **Language**: TypeScript +- **Package manager**: pnpm 10 (required — do not use npm or yarn) +- **Node**: 22 + +## Repository structure + +``` +fmeyer.dev/ +├── app/ # Nuxt application code +│ ├── app.vue # Root app component (global head, SEO, canonical URL) +│ ├── app.config.ts # Runtime app config (profile pic, email, footer links, UI theme) +│ ├── assets/css/main.css # Global CSS +│ ├── composables/ +│ │ └── usePageSeo.ts # Sets useSeoMeta from a content page's seo/title/description +│ ├── components/ +│ │ ├── AppHeader.vue # Floating pill navigation bar +│ │ ├── AppFooter.vue # Footer with social links +│ │ ├── ColorModeButton.vue # Dark/light mode toggle +│ │ ├── LabCard.vue # Card for a lab entry in the grid +│ │ ├── landing/ # Landing page section components +│ │ │ ├── Hero.vue +│ │ │ ├── Focus.vue +│ │ │ ├── WorkExperience.vue +│ │ │ ├── LabsTeaser.vue +│ │ │ └── SpeakingTeaser.vue +│ │ └── talks/ +│ │ └── TalkPreviewCard.vue +│ ├── layouts/ +│ │ └── default.vue # Wraps every page: UContainer + AppHeader + slot + AppFooter +│ ├── pages/ +│ │ ├── index.vue # Homepage (queries index, labs, talks collections) +│ │ ├── labs/ +│ │ │ ├── index.vue # Labs listing page +│ │ │ └── [slug].vue # Individual lab detail page +│ │ └── speaking/ +│ │ ├── index.vue # Speaking listing page +│ │ └── [slug].vue # Individual talk detail page +│ ├── utils/ +│ │ ├── date.ts # getTimestamp(value) helper +│ │ ├── clipboard.ts # Clipboard utilities +│ │ ├── labs.ts # LabEntry type, status maps, sortLabs, getLabSlug/Path, formatLabDate +│ │ ├── speaking.ts # TalkEntry/Resource types, sortTalks, resolveTalkEntry/Resource, getTalkSlug/Path +│ │ ├── talkAssets.ts # Registry mapping asset keys to bundled PDF paths +│ │ ├── types.ts # ContentButton shared type +│ │ └── links.ts # navLinks array (Home, Labs, Speaking) +│ └── error.vue # Error page +├── content/ # YAML content files (Nuxt Content collections) +│ ├── index.yml # Homepage data: hero, focus, experience, labs teaser, speaking teaser +│ ├── labs.yml # Labs listing page metadata +│ ├── labs/ # One .yml file per lab project +│ ├── speaking.yml # Speaking listing page metadata +│ └── speaking/ # One .yml file per talk +├── public/ # Static assets served as-is +│ ├── favicon.ico +│ ├── hero/ # Hero images (random-1.avif … random-9.avif) +│ ├── profile/ # Profile photo +│ └── robots.txt +├── content.config.ts # Nuxt Content collection schemas (Zod) +├── nuxt.config.ts # Nuxt config (modules, site URL, Nitro prerender, ESLint stylistic) +├── eslint.config.mjs # ESLint config (extends Nuxt's generated config) +├── tsconfig.json # TypeScript config +├── renovate.json # Renovate dependency update config +└── pnpm-workspace.yaml # pnpm workspace config +``` + +## Development commands + +```bash +pnpm install # Install dependencies +pnpm dev # Start development server at http://localhost:3000 +pnpm build # Production build +pnpm generate # Static site generation (used for deployment) +pnpm preview # Serve the generated output locally +pnpm lint # Run ESLint +pnpm lint:fix # Auto-fix ESLint issues +pnpm typecheck # Run Nuxt type checking (vue-tsc) +``` + +## Environment variables + +Copy `.env.example` to `.env` for local overrides. The only variable is: + +``` +NUXT_PUBLIC_SITE_URL=https://fmeyer.dev +``` + +This is used by `nuxt-og-image` during static generation. For `pnpm dev` it defaults correctly without a `.env` file. + +## Content collections + +Schemas are defined in `content.config.ts` using Zod. All content files are YAML. + +| Collection | Source | Type | Purpose | +|---|---|---|---| +| `index` | `content/index.yml` | page | Homepage sections (hero, focus, experience, teasers) | +| `pages` | `content/labs.yml` | page | Labs listing page metadata and links | +| `labs` | `content/labs/*.yml` | data | Individual lab project entries | +| `speaking` | `content/speaking.yml` | page | Speaking page metadata | +| `talks` | `content/speaking/*.yml` | data | Individual talk entries | + +### Adding a lab entry + +Create `content/labs/.yml` with these fields: + +```yaml +title: My Project # required +description: ... # required +challenge: ... # required +approach: ... # required +nextSteps: # required, at least one item + - Step one +status: wip # required: wip | prototype | paused +tags: + - Vue +date: 2025-01-01T00:00:00Z # required +icon: i-lucide-code # optional Iconify icon +image: /path/to/image.jpg # optional, goes in public/ +url: https://... # optional live demo URL +repoUrl: https://... # optional GitHub repo URL +note: ... # optional note shown on detail page +``` + +The slug used for the URL is derived from the filename (e.g., `my-project.yml` → `/labs/my-project`). + +### Adding a talk entry + +Create `content/speaking/.yml` with these fields: + +```yaml +title: Talk Title # required +summary: Short summary # required +description: Full description # required +event: Event Name # required +location: City, Country # required +topic: Topic name # required +dateLabel: "March 1, 2026" # required display string +date: 2026-03-01 # optional ISO date for sorting +organizerTitle: ... # optional title as submitted to the organiser +time: "10:00" # optional +room: Room Name # optional +duration: 25 min # optional +format: Full Talk # optional +level: Advanced # optional +language: English # optional +venueName: ... # optional +venueAddress: ... # optional +url: https://... # optional session URL +eventUrl: https://... # optional event URL +placeholder: true # optional, marks a not-yet-confirmed talk +resources: # optional list of linked assets + - kind: slides # slides | recording | handout | link + title: Slides + asset: my-talk-slides # key in app/utils/talkAssets.ts (for bundled files) + # OR + url: https://... # external URL — exactly one of asset or url is required + format: PDF # optional + pages: 18 # optional +``` + +### Bundling a talk asset (PDF) + +1. Place the file in `app/assets/` (e.g., `app/assets/My Talk.pdf`). +2. Add a key to the `talkAssetRegistry` in `app/utils/talkAssets.ts`. +3. Reference that key as `asset:` in the talk's YAML `resources` entry. + +## Code conventions + +### Vue components + +- Use `