Imported from eddyv/personal-website (
AGENTS.md). Install upstream withnpx skills add eddyv/personal-website. Copyright stays with the author.
AGENTS.md
This document provides guidance for AI coding agents working in this repository.
Project Overview
A personal website built with Astro 6.x (SSR mode), React 19.x, TypeScript, and Tailwind CSS 4.x. Deployed to Cloudflare Workers using Bun as the package manager and git with GitHub for version control.
Note: the dev server runs SSR in Cloudflare's workerd runtime (via
@cloudflare/vite-plugin). If dev 500s with "module is not defined" or
"require is not defined", an externalized CJS dependency needs to be added to
vite.ssr.optimizeDeps.include in astro.config.mjs - then clear
node_modules/.vite and .astro before restarting.
Content (EmDash CMS)
Blog content lives in EmDash (database-backed, Portable Text) - NOT in markdown files. Key facts:
- Local dev uses Cloudflare D1/R2 simulated by miniflare; state persists in
.wrangler/state/(gitignored, disposable).rm -rf .wrangler/statefor a fresh database - the committed.emdash/seed.jsonre-applies on the next dev-server first request (collections + settings; content applies via the setup flow). - Admin UI: with the dev server running, visit
http://localhost:4321/_emdash/api/setup/dev-bypass?redirect=/_emdash/adminonce - it completes setup, applies seed content, and logs in a dev admin (dev-only; returns 403 in production builds). - Query content with
getEmDashCollection/getEmDashEntryfromemdashand render with<PortableText>fromemdash/ui(seesrc/pages/index.astro). - Post images are served from
public/blog/and referenced by URL in Portable Text image blocks (asset.url) - they are not in the media library. robots.txtis served by EmDash's injected route fromsettings.seo.robotsTxt(set in the seed). Do not addsrc/pages/robots.txt.ts- it collides with the injected route.scripts/markdown-to-seed/is the one-shot converter that produced.emdash/seed.jsonfrom the original markdown posts (now deleted). Its conversion logic is unit-tested with inline fixtures.
Build/Lint/Test Commands
| Command | Description |
|---|---|
bun run dev |
Start local dev server |
bun run build |
Type check and build (astro check && astro build) |
bun run preview |
Serve the built worker locally (wrangler dev -c dist/server/wrangler.json; build first) |
bun run deploy |
Build and deploy to Cloudflare Workers |
bun run check |
Lint check via Ultracite/Biome |
bun run fix |
Auto-fix lint/format issues |
bun run test |
Run unit and e2e suites |
bun run test:unit |
Run Vitest unit tests |
bun run test:unit:watch |
Run Vitest in watch mode |
bun run test:e2e |
Run Playwright e2e tests (spawns its own dev server) |
bun run clean |
Remove ./dist and ./.astro directories |
bun run cf-typegen |
Generate Cloudflare Worker types |
Testing
- Unit tests live in
test/unit/(Vitest viagetViteConfig, soastro:*virtual modules and tsconfig path aliases resolve). Middleware tests mockastro:env/serverandastro:middlewarefor determinism. - E2e tests live in
test/e2e/(Playwright, chromium only). The config startsbun run devitself withPLAYWRIGHT_TEST=1(disables the Astro dev toolbar, which otherwise intercepts dock clicks) and a raised rate-limit window (every dev request shares the "unknown" client IP). A globalSetup step hits the EmDash dev-bypass endpoint so seed content is applied before the suite runs. If post content looks stale,rm -rf .wrangler/state. test/fixtures/expected-posts.tsis the blog content contract (slugs, titles, ordering, headings, image prefixes). It must keep passing unchanged through framework upgrades and content-system migrations.- E2e assertions are semantic (headings, alt text, counts) - never HTML snapshots, so markdown-renderer internals can change without false alarms.
- IMPORTANT: never run
bun test- bun's built-in runner grabs*.test.tsfiles but cannot handlevi.mock. Always usebun run test:unit.
Running a Single Test
bunx vitest run test/unit/cors.test.ts
bunx playwright test test/e2e/notes.spec.ts
Code Style Guidelines
This project uses Ultracite, a zero-config Biome preset for linting and formatting. Run bun run fix before committing.
Imports
- Use path aliases consistently:
@components/,@hooks/,@utils/,@layouts/,@pages/,@middleware/,@assets/,@icons/ - Prefer named imports over default imports
- Use
typekeyword for type-only imports:import type { APIRoute } from "astro" - Order imports: external packages first, then internal path aliases
import { useState, useCallback } from "react";
import type { APIRoute } from "astro";
import { Terminal } from "@components/terminal";
import { useTerminalCommands } from "@hooks/use-terminal-commands";
Formatting
- Quotes: Double quotes for strings
- Semicolons: Required
- Indentation: 2 spaces
- JSX Attributes: Sorted alphabetically
- Run
bun run fixto auto-format
TypeScript
- Use explicit types for function parameters and return values
- Prefer
unknownoveranywhen type is genuinely unknown - Use const assertions (
as const) for immutable values - Leverage type narrowing instead of type assertions
- Define component props as interface named
Props
interface Props {
title: string;
count?: number;
}
export function Component({ title, count = 0 }: Props): React.ReactElement {
// ...
}
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Files | kebab-case | terminal-utils.tsx, use-terminal-commands.ts |
| Components | PascalCase | Terminal, LoadingDots |
| Hooks | camelCase with use prefix |
useTerminalCommands, useTypingAnimation |
| Functions/Variables | camelCase | renderPrompt, executeCommand |
| Types/Interfaces | PascalCase | CommandOutput, RateLimitConfig |
| Constants | camelCase | defaultTimeout, maxRetries |
React Patterns
- Use function components (no class components)
- Call hooks at the top level only, never conditionally
- Specify all dependencies in hook dependency arrays
- Use
useCallbackfor memoized event handlers - Use
useReffor DOM element references - Default exports for main components, named exports for utilities
export function Terminal({ initialCommand }: Props): React.ReactElement {
const [input, setInput] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = useCallback((e: React.FormEvent) => {
e.preventDefault();
// ...
}, []);
return <form onSubmit={handleSubmit}>...</form>;
}
Error Handling
- Use try-catch blocks in async handlers
- Check error type with
instanceof Error - Return proper HTTP status codes (200, 429, 500)
- Prefer early returns over nested conditionals
- Throw Error objects with descriptive messages
try {
const result = await fetchData();
return new Response(JSON.stringify(result), { status: 200 });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return new Response(JSON.stringify({ error: message }), { status: 500 });
}
Async/Promises
- Always
awaitpromises in async functions - Use
async/awaitsyntax instead of promise chains - Handle errors with try-catch blocks
- Don't use async functions as Promise executors
Security
- Add
rel="noopener"when usingtarget="_blank" - Avoid
dangerouslySetInnerHTMLunless necessary - Never use
eval()or assign directly todocument.cookie - Validate and sanitize user input
Performance
- Avoid spread syntax in accumulators within loops
- Use top-level regex literals instead of creating in loops
- Prefer specific imports over namespace imports
- Avoid barrel files (index files that re-export everything)
Accessibility
- Provide meaningful alt text for images
- Use proper heading hierarchy
- Add labels for form inputs
- Include keyboard event handlers alongside mouse events
- Use semantic HTML (
<button>,<nav>) over divs with roles
Project Structure
src/
├── components/ # UI components (.astro and .tsx)
├── hooks/ # Custom React hooks
├── icons/ # Custom SVG icons
├── layouts/ # Page layout templates
├── middleware/ # Request middleware (CORS, rate limiter)
├── pages/ # Route components and API endpoints
├── styles/ # Global CSS
└── utils/ # Utility functions
Architecture Notes
- Astro components (
.astro) for static UI - React components (
.tsx) for interactive features withclient:loaddirective - Middleware chain uses Astro's
sequence()function - Environment variables typed via Astro's
envFieldschema inastro.config.mjs - API routes use Astro's
APIRoutetype for server-side endpoints
Pre-commit Hook
The Husky pre-commit hook automatically runs ultracite fix on staged files. Ensure your changes pass linting before committing.
When to Use Manual Review
Biome catches most issues automatically. Focus manual attention on:
- Business logic correctness
- Meaningful naming for functions, variables, and types
- Architecture decisions (component structure, data flow)
- Edge cases and error states
- User experience and accessibility
- Documentation for complex logic
Ultracite Code Standards
This project uses Ultracite, a zero-config preset that enforces strict code quality standards through automated formatting and linting.
Quick Reference
- Format code:
bun x ultracite fix - Check for issues:
bun x ultracite check - Diagnose setup:
bun x ultracite doctor
Biome (the underlying engine) provides robust linting and formatting. Most issues are automatically fixable.
Core Principles
Write code that is accessible, performant, type-safe, and maintainable. Focus on clarity and explicit intent over brevity.
Type Safety & Explicitness
- Use explicit types for function parameters and return values when they enhance clarity
- Prefer
unknownoveranywhen the type is genuinely unknown - Use const assertions (
as const) for immutable values and literal types - Leverage TypeScript's type narrowing instead of type assertions
- Use meaningful variable names instead of magic numbers - extract constants with descriptive names
Modern JavaScript/TypeScript
- Use arrow functions for callbacks and short functions
- Prefer
for...ofloops over.forEach()and indexedforloops - Use optional chaining (
?.) and nullish coalescing (??) for safer property access - Prefer template literals over string concatenation
- Use destructuring for object and array assignments
- Use
constby default,letonly when reassignment is needed, nevervar
Async & Promises
- Always
awaitpromises in async functions - don't forget to use the return value - Use
async/awaitsyntax instead of promise chains for better readability - Handle errors appropriately in async code with try-catch blocks
- Don't use async functions as Promise executors
React & JSX
- Use function components over class components
- Call hooks at the top level only, never conditionally
- Specify all dependencies in hook dependency arrays correctly
- Use the
keyprop for elements in iterables (prefer unique IDs over array indices) - Nest children between opening and closing tags instead of passing as props
- Don't define components inside other components
- Use semantic HTML and ARIA attributes for accessibility:
- Provide meaningful alt text for images
- Use proper heading hierarchy
- Add labels for form inputs
- Include keyboard event handlers alongside mouse events
- Use semantic elements (
<button>,<nav>, etc.) instead of divs with roles
Error Handling & Debugging
- Remove
console.log,debugger, andalertstatements from production code - Throw
Errorobjects with descriptive messages, not strings or other values - Use
try-catchblocks meaningfully - don't catch errors just to rethrow them - Prefer early returns over nested conditionals for error cases
Code Organization
- Keep functions focused and under reasonable cognitive complexity limits
- Extract complex conditions into well-named boolean variables
- Use early returns to reduce nesting
- Prefer simple conditionals over nested ternary operators
- Group related code together and separate concerns
Security
- Add
rel="noopener"when usingtarget="_blank"on links - Avoid
dangerouslySetInnerHTMLunless absolutely necessary - Don't use
eval()or assign directly todocument.cookie - Validate and sanitize user input
Performance
- Avoid spread syntax in accumulators within loops
- Use top-level regex literals instead of creating them in loops
- Prefer specific imports over namespace imports
- Avoid barrel files (index files that re-export everything)
- Use proper image components (e.g., Next.js
<Image>) over<img>tags
Framework-Specific Guidance
Next.js:
- Use Next.js
<Image>component for images - Use
next/heador App Router metadata API for head elements - Use Server Components for async data fetching instead of async Client Components
React 19+:
- Use ref as a prop instead of
React.forwardRef
Solid/Svelte/Vue/Qwik:
- Use
classandforattributes (notclassNameorhtmlFor)
Testing
- Write assertions inside
it()ortest()blocks - Avoid done callbacks in async tests - use async/await instead
- Don't use
.onlyor.skipin committed code - Keep test suites reasonably flat - avoid excessive
describenesting
When Biome Can't Help
Biome's linter will catch most issues automatically. Focus your attention on:
- Business logic correctness - Biome can't validate your algorithms
- Meaningful naming - Use descriptive names for functions, variables, and types
- Architecture decisions - Component structure, data flow, and API design
- Edge cases - Handle boundary conditions and error states
- User experience - Accessibility, performance, and usability considerations
- Documentation - Add comments for complex logic, but prefer self-documenting code
Most formatting and common issues are automatically fixed by Biome. Run bun x ultracite fix before committing to ensure compliance.