Personal homepage and blog of Josh Finnie built with Astro 5.
- Framework: Astro 5 with React, MDX, and Alpine.js
- Styling: Tailwind CSS 4
- Images: Cloudinary for CDN and automatic optimization
- Linting/Formatting: Biome
- Type Checking: TypeScript (strict mode)
- Deployment: Netlify
- CI/CD: GitHub Actions
- Node.js 24+
- pnpm 10.8.1+
# Clone the repository
git clone https://github.com/joshfinnie/joshfinnie.com.git
cd joshfinnie.com
# Install dependencies
pnpm install# Start dev server (http://localhost:3333)
pnpm dev
# Run linting and formatting checks
pnpm format:check
# Auto-fix linting and formatting issues
pnpm format
# Type check
pnpm check
# Build for production
pnpm build
# Preview production build
pnpm previewThis site uses Cloudinary for all images. Images are automatically optimized (format, quality, and size) by Cloudinary's CDN.
-
Upload to Cloudinary:
# Place your image in src/assets/ (temporarily) # Example: src/assets/blog/my-new-image.jpg # Run the upload script pnpm cloudinary:upload
-
Reference in Content:
For blog post hero images, use the Cloudinary public ID in frontmatter:
--- title: "My Post" heroImage: "blog/my-new-image" # No extension, just the public ID ---
-
Inline Images in MDX:
<!-- Use direct Cloudinary URLs --> <img src="https://res.cloudinary.com/dgd9cw3gu/image/upload/f_auto,q_auto/blog/my-image" alt="Description" />
-
Using the CloudinaryImage Component:
--- import CloudinaryImage from '@components/CloudinaryImage.astro'; --- <CloudinaryImage publicId="blog/my-image" alt="Description" width={1024} />
Cloudinary automatically applies:
- Format optimization (
f_auto): Serves WebP/AVIF to supported browsers - Quality optimization (
q_auto): Adjusts quality based on content - Responsive images: CloudinaryImage component generates srcset automatically
Blog posts are located in src/collections/blog/ and support both .md and .mdx formats.
---
title: "Your Post Title"
date: "2024-01-01"
description: "A concise SEO-friendly description (max 160 characters)"
tags:
- "tag1"
- "tag2"
slug: "custom-url-slug" # Optional
heroImage: "blog/hero-image" # Optional, Cloudinary public ID
unsplash: "Photographer Name" # Optional, if using Unsplash
unsplashURL: "photographer-handle" # Optional
draft: false # Optional, set to true to hide from production
---
Your content here...title: Post titledate: Publication date (YYYY-MM-DD format)description: SEO meta description (keep under 160 characters)tags: Array of relevant tags
slug: Custom URL (defaults to filename)heroImage: Cloudinary public ID for hero imageunsplash/unsplashURL: Credit for Unsplash photosdraft: Hide from production if trueexpires: Mark as time-sensitive content
This project uses Husky for git hooks:
- Pre-commit: Runs Biome formatting and linting checks
- Commits are blocked if code quality checks fail
GitHub Actions runs on every push and PR:
- Linting & Formatting: Biome checks
- Type Checking: TypeScript validation
- Build: Production build verification
# Check formatting and linting
pnpm format:check
# Auto-fix issues
pnpm format
# Lint only
pnpm lintjoshfinnie.com/
├── .github/
│ └── workflows/
│ └── ci.yml # GitHub Actions CI
├── src/
│ ├── collections/
│ │ ├── blog/ # Blog posts (.md, .mdx)
│ │ ├── projects/ # Project pages
│ │ └── talks/ # Speaking engagements
│ ├── components/ # Reusable Astro components
│ │ └── CloudinaryImage.astro
│ ├── layouts/ # Page layouts
│ ├── lib/
│ │ └── cloudinary.ts # Cloudinary helper functions
│ ├── pages/ # Route pages
│ └── content.config.ts # Content collections schema
├── scripts/
│ ├── upload_to_cloudinary.js # Image upload script
│ └── migrate_frontmatter.js # Migration utilities
├── astro.config.mjs # Astro configuration
├── biome.json # Biome linter/formatter config
├── tailwind.config.mjs # Tailwind CSS configuration
└── tsconfig.json # TypeScript configuration
Required for local development:
# .env
CLOUDINARY_CLOUD_NAME=dgd9cw3gu
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secretNote: The cloud name has a fallback in code, but API credentials are required for uploading images.
The site automatically deploys to Netlify on pushes to main:
- Build Command:
pnpm build - Publish Directory:
dist - Environment Variables: Set
CLOUDINARY_CLOUD_NAMEin Netlify settings (optional, has fallback)
- Create a feature branch
- Make your changes
- Ensure all checks pass:
pnpm format:check && pnpm check && pnpm build - Commit (pre-commit hooks will run automatically)
- Push and create a PR
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License
- Website: joshfinnie.com
- GitHub: @joshfinnie