Personal portfolio and case-study site for Christopher Harley, a Design Engineer building websites for agencies and small businesses. Deployed to Netlify.
- Astro 7 — server-rendered (
output: 'server') via the Netlify adapter - React 19 — interactive islands: mobile nav, contact form, toasts
- Tailwind CSS v4 — styling via the Vite plugin
- shadcn/ui — accessible UI primitives (button, field, input, label, textarea) with
class-variance-authority,tailwind-merge, andclsx - react-hook-form + zod — contact form validation
- sonner — toast notifications
Requirements: Node.js >= 22.12.0 (see engines in package.json).
npm install
npm run devThe dev server runs at http://localhost:4321.
| Command | Action |
|---|---|
npm install |
Install dependencies |
npm run dev |
Start local dev server at localhost:4321 |
npm run build |
Build production site to ./dist/ |
npm run preview |
Preview the production build locally |
npm run check |
Run Astro type/lint checks |
npm run format |
Format source with Prettier |
npm run format:check |
Verify formatting without writing |
ℹ️ The package has no lockfile committed; installs use npm by default.
/
├── public/
│ ├── christopher_harley_resume.pdf
│ └── favicon.*
├── src/
│ ├── components/ # Astro + React components
│ │ ├── ui/ # shadcn/ui primitives
│ │ ├── Nav.astro # Desktop navigation
│ │ ├── MobileNav.tsx # Mobile navigation drawer
│ │ ├── Footer.astro
│ │ ├── ProjectCard.astro
│ │ ├── contact-form.tsx # Contact form (React Server Action)
│ │ └── ToasterWrapper.tsx
│ ├── data/
│ │ ├── navigation.ts # Nav link config
│ │ └── projects.ts # All project + case-study content
│ ├── images/ # Case-study imagery (imported, Astro-optimized)
│ │ ├── bs-case-study/ # BASSMENT
│ │ ├── mk-case-study/ # Merge Konflict
│ │ └── wl-case-study/ # Walnut Lawn
│ ├── layouts/Layout.astro # Global layout (head, fonts, nav, footer)
│ ├── lib/
│ │ ├── contact-schema.ts # Zod schema + error parsing for the form
│ │ └── utils.ts # cn() helper
│ ├── pages/
│ │ ├── index.astro # Home / projects grid
│ │ ├── about.astro
│ │ ├── contact.astro
│ │ ├── case-study/[slug].astro # Dynamic case-study pages
│ │ └── 404.astro
│ └── styles/global.css # Tailwind entry + design tokens
└── astro.config.mjs
All project and case-study content lives in src/data/projects.ts. The home page renders projects in array order, so the first entry appears first.
Each project includes:
- Card metadata (
title,description,tags,imageUrl,imageAlt,slug) - A full case study: subtitle, hero image, industry, category, tech stack, live link, introduction, design description + images, development body (multi-paragraph array), and a concluding quote + body.
To add a project:
- Drop images in
src/images/<name>-case-study/and import them at the top ofprojects.ts. - Append a new entry to the
projectsarray (or insert at the position you want). - Run
npm run checkandnpm run buildto verify.
src/pages/case-study/[slug].astro generates a page per project. Prev/next navigation across projects is derived from the array order, so slugs must be unique.
The contact form posts to a Netlify server action, validates with a Zod schema, and shows success/error feedback via sonner toasts. No environment variables are required for local development; check .env for any Netlify-specific settings (never commit it).
Continuous deployment via Netlify (the @astrojs/netlify adapter emits a serverless function with _redirects). Push to master to trigger a production deploy; the repo's pull-request flow handles preview builds.
Before pushing, run:
npm run check # Astro type checking
npm run format:check # Prettier formatting
npm run build # Production build (SSR function output)