Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions .github/actions/build-site/action.yml
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
# Based on https://www.specialcase.dev/blog/sharing-steps-in-github-action-workflows/

name: "Shared steups to build the site"
name: "Shared steps to build the site"
description: "Used to share steps between jobs that build the static site."
runs:
using: "composite"
steps:
- name: Cache next build cache
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache Cargo registry and build artifacts
uses: actions/cache@v4
with:
path: |
${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-nextjs-cache-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
~/.cargo/registry
~/.cargo/git
${{ github.workspace }}/ssg/target
key: ${{ runner.os }}-cargo-${{ hashFiles('ssg/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-nextjs-cache-${{ hashFiles('**/yarn.lock') }}-
${{ runner.os }}-cargo-

- name: Cache next build
id: cache-next-build
- name: Cache site build output
id: cache-site-build
uses: actions/cache@v4
with:
path: |
${{ github.workspace }}/public
${{ github.workspace }}/.next/
${{ github.workspace }}/dist
key: ${{ github.run_id }}

- name: Build
if: ${{ steps.cache-next-build.outputs.cache-hit != 'true' }}
if: ${{ steps.cache-site-build.outputs.cache-hit != 'true' }}
env:
CI: "true"
shell: bash
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ tsconfig.tsbuildinfo
/.next/
/out/

# Rust build artifacts
/ssg/target/

# Static site output
/dist/

# We generate the following files via the prebuild script
public/sitemap.xml
public/blog/index.xml
66 changes: 66 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# AI Agents Guide

This document provides essential context, architectural understanding, and operational instructions for AI agents (such as Claude Code, GitHub Copilot, or Cursor) working within the `hockeybuggy.com` repository.

## 🎯 Project Overview
`hockeybuggy.com` is a personal website and blog for Douglas Anderson. It uses a static-site-generation-like approach within Next.js, where Markdown content is processed via custom scripts to generate RSS feeds and sitemates during the build process.

## 🏗️ Architecture & Data Flow

### Content Pipeline
1. **Source:** Blog posts are authored as `.md` files (typically in a `content/` or similar directory, though check project structure for exact location).
2. **Processing:** Custom scripts in `scripts/` (e.g., `build_blog_feed.ts`) parse these Markdown files.
3. **Services Layer:** The `services/` directory contains the logic for:
* Markdown to HTML conversion (`markdownToHtml`).
* Data retrieval and presentation (`BlogPresentor`).
4. **Output:** Generated content (like `public/blog/index.xml`) is updated during the build process.

### Frontend
* **Framework:** Next.js (React) with TypeScript.
* **Styling:** Sass (`.scss`) is used for component and global styling.
* **Routing:** Uses the Next.js file-system based router.

## 🛠️ Development Workflow

### Essential Commands
| Task | Command |
| :--- | :--- |
| **Development Server** | `yarn dev` |
| **Production Build** | `yarn build` |
| **Linting** | `yarn lint` |
| **Type Checking** | `yarn typecheck` |
| **Formatting** | `yarn format` |
| **Pre-build Content Generation** | `yarn test:prebuild` |
| **Unit/Integration Tests** | `yarn test` |
| **E2E Tests (Puppeteer)** | `JEST_PUPPETEER_CONFIG=e2e_tests/jest-puppeteer.config.js jest --runInBand -c 'e2e_tests/jest.config.js'` |

### Adding a New Blog Post
1. Create a new `.md` file in the content directory.
2. Ensure it follows the required frontmatter structure (Title, Date, etc.).
3. Run `yarn test:prebuild` to regenerate the RSS feed and sitemap locally.
4. Verify the changes in the development server.

## 📜 Coding Standards & Rules

### TypeScript & JavaScript
* **Strict Typing:** Always use explicit types; avoid `any`.
* **Naming:** Use `camelCase` for functions/variables, `PascalCase` for React components, and `kebab-case` for filenames.
* **Arrows:** Prefer arrow functions for React components.

### Styling
* Use `.scss` files for styles.
* Follow existing patterns for component-scoped or global Sass.

### Quality Assurance (The "Agent Manifesto")
1. **Don't break the build:** Before submitting any change, ensure `yarn lint` and `yarn typecheck` pass.
2. **Update Tests:** If you modify functionality, you **must** update/add corresponding tests in `test/` or `e2e_tests/`.
3. **Accessibility (a11y):** Always write semantic HTML and ensure components are accessible to screen readers.
4. **Content Integrity:** When modifying scripts that handle Markdown, always verify the output (RSS/Sitemap) is still valid.

## 📂 Directory Map (Key Folders)
* `scripts/`: Build automation and content processing logic.
* `services/`: Business logic, parsing, and data presentation.
* `components/`: React UI components.
* `public/`: Static assets and build-generated files.
* `test/`: Unit and integration tests.
* `e2e_tests/`: End-to-end browser testing configuration.
25 changes: 25 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Build/Test Commands
- Development: `yarn dev`
- Build: `yarn build`
- Lint: `yarn lint`
- Type check: `yarn typecheck`
- Format code: `yarn format`
- Prebuild tests: `yarn test:prebuild`
- E2E tests: `yarn test:e2e`
- Run single E2E test: `JEST_PUPPETEER_CONFIG=e2e_tests/jest-puppeteer.config.js jest --runInBand -c 'e2e_tests/jest.config.js' -t 'test name'`

## Code Style Guidelines
- TypeScript with strict type checking
- React for UI components
- Next.js for routing and build system
- Use absolute imports; types should be explicit when not inferrable
- Files use kebab-case, components use PascalCase, functions use camelCase
- Arrow functions preferred for React components
- Four-space indentation with Prettier for formatting
- ESLint for code quality (run `yarn lint` before commits)
- Semantic HTML and accessible components (JSX a11y)
- Update tests when modifying functionality
3 changes: 1 addition & 2 deletions netlify.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[build]
command = "yarn build"
functions = "lambda"
publish = ".next"
publish = "dist"

[dev]
autoLaunch = false
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
"license": "MIT",
"private": true,
"scripts": {
"build": "yarn prebuild && next build",
"prebuild": "ts-node ./scripts/build_blog_feed.ts && ts-node ./scripts/build_sitemap.ts",
"start": "next start -p 4000",
"dev": "next",
"build": "cargo run --release --manifest-path ssg/Cargo.toml",
"prebuild": "echo 'prebuild is handled by cargo build'",
"start": "node serve_static.js",
"dev": "cargo run --manifest-path ssg/Cargo.toml",
"lint": "eslint \"**/*.ts*\"",
"typecheck": "tsc -p . --skipLibCheck && rm tsconfig.tsbuildinfo",
"format": "prettier --write \"**/*.ts*\"",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 80 additions & 0 deletions serve_static.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env node
// Simple static file server that serves index.html from directories WITHOUT
// redirecting. Required because the e2e tests check page.url() === original URL.

"use strict";

const http = require("http");
const fs = require("fs");
const path = require("path");

const PORT = 4000;
const ROOT = path.join(__dirname, "dist");

const MIME = {
".html": "text/html; charset=utf-8",
".css": "text/css",
".js": "application/javascript",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".webp": "image/webp",
".svg": "image/svg+xml",
".xml": "application/xml; charset=utf-8",
".ico": "image/x-icon",
".txt": "text/plain",
".woff": "font/woff",
".woff2": "font/woff2",
".json": "application/json",
};

function resolve(urlPath) {
// Strip query string and decode
const clean = decodeURIComponent(urlPath.split("?")[0]);

// Candidate paths in order of preference
const candidates = [
path.join(ROOT, clean),
path.join(ROOT, clean, "index.html"),
path.join(ROOT, clean.replace(/\/$/, ""), "index.html"),
];

for (const candidate of candidates) {
try {
const stat = fs.statSync(candidate);
if (stat.isFile()) return candidate;
if (stat.isDirectory()) {
const idx = path.join(candidate, "index.html");
if (fs.existsSync(idx)) return idx;
}
} catch {}
}
return null;
}

const server = http.createServer((req, res) => {
const filePath = resolve(req.url);

if (!filePath) {
// Serve 404.html if present, otherwise plain text
const notFound = path.join(ROOT, "404.html");
if (fs.existsSync(notFound)) {
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
fs.createReadStream(notFound).pipe(res);
} else {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("404 Not Found");
}
return;
}

const ext = path.extname(filePath).toLowerCase();
const contentType = MIME[ext] || "application/octet-stream";

res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "no-cache" });
fs.createReadStream(filePath).pipe(res);
});

server.listen(PORT, () => {
console.log(`Serving dist/ at http://localhost:${PORT}/`);
});
Loading
Loading