Adding a new font to FontAlternatives used to take 30+ minutes:
- Research the font (foundry, classification, traits)
- Find free alternatives with similarity scores
- Write the markdown file with proper frontmatter
- Create bidirectional links to alternatives
- Download specimen images
- Run validation checks
- Create PR and deploy
Now it takes 3 minutes. And I don’t do any of it manually.
The problem
Manual content creation doesn’t scale. With 6,198 fonts in my “unavailable” index generating demand signals, I needed a way to process font requests faster than I could type. The font requests that Claude Code processes come from the demand tracking pipeline.
Options I considered:
- Hire writers: Expensive, training overhead, inconsistent quality
- Template generators: Fast but generic, no research capability
- AI with prompts: Flexible but requires manual invocation
What I needed: an AI agent that could research fonts, write content, and handle the full PR workflow autonomously.
The solution: Claude Code in GitHub Actions
Claude Code is Anthropic’s CLI agent that can read files, write code, run commands, and interact with APIs. I wired it into my GitHub Actions workflow.
flowchart TD
A[Issue labeled 'font-request'] --> B[process-font-request.yml]
subgraph workflow[GitHub Actions Workflow]
B --> C[Create branch]
C --> D[Run Claude Code]
D --> E[Research font & write files]
E --> F[Download specimen images]
F --> G[Run guardrails]
G --> H[Create PR]
H --> I[Trigger preview deployment]
end
I --> J[Preview deployed]
J --> K[PR ready for review]
style A fill:#fff3e0
style K fill:#c8e6c9
The workflow
Step 1: Issue trigger
When an issue gets the font-request label (either manually or from my demand bot):
on:
issues:
types: [labeled]
jobs:
process:
if: github.event.label.name == 'font-request'
Step 2: Branch creation
- name: Create branch
run: |
SLUG=$(echo "$ISSUE_TITLE" | sed 's/Font Request: //' | \
tr '[:upper:]' '[:lower:]' | tr ' ' '-')
git checkout -b "font-request/$SLUG"
Step 3: Claude Code invocation
This is the magic. I pass the issue body as context:
- name: Process with Claude Code
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
claude --print "
Process this font request issue:
${{ github.event.issue.body }}
Follow these steps:
1. Research the font (foundry, classification, traits, use cases)
2. Find 2-3 free alternatives on Google Fonts
3. Calculate similarity scores based on visual characteristics
4. Create the premium font markdown file
5. Update free font files with back-references
6. Remove from unavailable-fonts.json if present
Use the content guide at docs/content-guide.md for formatting.
This is a Tier 2 font (standard content depth).
"
Step 4: Image download
Claude Code can run shell commands, so it triggers the image orchestrator:
npx tsx scripts/download-font-images.ts --slug avenir
The orchestrator tries foundry-specific scrapers first, then MyFonts, then generic fallback.
Step 5: Guardrails
Before creating the PR, validate everything:
- name: Run guardrails
run: ./guardrails.sh --level 1
Level 1 checks:
- Biome lint (formatting)
- Schema validation (frontmatter matches Zod schemas)
- Link validation (bidirectional links work)
- Content checks (word counts, FAQ presence)
Step 6: PR creation
- name: Create PR
env:
GH_TOKEN: ${{ secrets.PAT_TOKEN }} # Not GITHUB_TOKEN - see below
run: |
gh pr create \
--title "feat: add $FONT_NAME font" \
--body "Closes #$ISSUE_NUMBER ..."
Important: I use a Personal Access Token, not GITHUB_TOKEN. Why? GitHub Actions workflows triggered by GITHUB_TOKEN don’t trigger other workflows. I need this PR to trigger my preview deployment workflow.
Step 7: Preview deployment
The PR creation triggers preview.yml, which:
- Builds the site with the new font
- Uploads assets to R2
- Deploys to preview Workers environment
- Updates the PR description with preview URLs
The <!-- PREVIEW_URL_PLACEHOLDER --> comment gets replaced with:
| Status | Preview | Font Page |
|--------|---------|-----------|
| Ready | [Site](https://preview...) | [/alternatives/avenir/](https://preview.../alternatives/avenir/) |
The Claude Code prompt
The full prompt I use:
You are processing a font request for FontAlternatives.com.
## Issue Details
$ISSUE_BODY
## Instructions
1. **Research the font**
- Identify the foundry (check MyFonts, official site)
- Determine classification: sans-serif | serif | display | mono | handwritten
- List traits: geometric, humanist, condensed, rounded, etc.
- Note use cases: branding, editorial, ui, code, etc.
- Check if it's a variable font
2. **Find free alternatives**
- Search Google Fonts for similar fonts
- Look for matching: classification, x-height, width, contrast
- Calculate similarity scores (0-100) based on visual characteristics
- Select 2-3 best matches
3. **Create premium font file**
Location: src/content/premiumFonts/{slug}.md
Required frontmatter:
- name, slug, tier: "2", classification
- foundry, traits, useCases
- variableFont (boolean)
- alternatives array with slug + similarity
Content: 100+ words describing the font and alternatives
4. **Link free alternatives**
- For each alternative in the premium font
- Open src/content/freeFonts/{alt-slug}.md
- Add premium slug to alternativeFor array
5. **Clean up**
- Remove font from src/data/unavailable-fonts.json if present
6. **Download images**
Run: npx tsx scripts/download-font-images.ts --slug {slug}
7. **Validate**
Run: npm run check:fonts
Follow docs/content-guide.md for style and formatting.
Do not add emojis to files.
Use imperative commit messages.
Tradeoffs
What I gained:
- 10x faster than manual (30 min to 3 min)
- Consistent quality (follows my content guide exactly)
- Scales with demand (can process 10 fonts/day)
- I review PRs, not write content
What I lost:
- Claude Code costs ~$0.50 per font (API calls)
- Occasional errors require PR fixes
- Less “voice” in content (template-y)
Quality control:
- Guardrails catch schema/link errors before PR
- Preview deployment lets me visually verify
- I review every PR before merge (1-2 min review)
The PAT token trick
This was a gotcha that took hours to debug.
GitHub Actions workflows using GITHUB_TOKEN:
- Can create PRs
- Can push commits
- Cannot trigger other workflows
From GitHub docs:
When you use the repository’s GITHUB_TOKEN to perform tasks, events triggered by the GITHUB_TOKEN will not create a new workflow run.
My solution: use a Personal Access Token (PAT) for the PR creation step:
- name: Create PR
env:
GH_TOKEN: ${{ secrets.PAT_TOKEN }} # Not GITHUB_TOKEN
The PAT needs repo scope. Store it as a repository secret.
Cost analysis
| Component | Cost per Font |
|---|---|
| Claude Code API | ~$0.30-0.50 |
| GitHub Actions | Free (within limits) |
| Cloudflare Workers | Free tier |
| R2 Storage | Free tier |
At 3 fonts/day (my rate limit), worst case is ~$45/month in API costs. For context, that’s less than one hour of my time.
Results
Since implementing this system:
| Metric | Before | After |
|---|---|---|
| Time per font | 30-45 min | 3-5 min (my review time) |
| Fonts added per week | 2-3 | 10-15 |
| Consistency | Variable | High |
| My involvement | Full process | Review only |
The pipeline has processed 89 font requests with a 94% first-pass success rate (guardrails catch most issues).
Complete workflow example
Here’s the essence of process-font-request.yml:
name: Process Font Request
on:
issues:
types: [labeled]
jobs:
process:
if: github.event.label.name == 'font-request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- run: npm ci
- name: Extract font info
id: font
run: |
FONT_NAME=$(echo "${{ github.event.issue.title }}" | \
sed 's/Font Request: //')
SLUG=$(echo "$FONT_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
echo "name=$FONT_NAME" >> $GITHUB_OUTPUT
echo "slug=$SLUG" >> $GITHUB_OUTPUT
- name: Create branch
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "font-request/${{ steps.font.outputs.slug }}"
- name: Process with Claude Code
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
npx @anthropic-ai/claude-code --print "
Process font request for: ${{ steps.font.outputs.name }}
Issue body: ${{ github.event.issue.body }}
Follow docs/content-guide.md.
"
- name: Run guardrails
run: ./guardrails.sh --level 1
- name: Commit and push
run: |
git add -A
git commit -m "feat: add ${{ steps.font.outputs.name }}"
git push -u origin "font-request/${{ steps.font.outputs.slug }}"
- name: Create PR
env:
GH_TOKEN: ${{ secrets.PAT_TOKEN }}
run: |
gh pr create \
--title "feat: add ${{ steps.font.outputs.name }}" \
--body "Closes #${{ github.event.issue.number }}" \
--label "automated"
This automation is a key part of the content flywheel.
What’s next
I’m considering:
- Confidence scoring: Have Claude rate its own work, flag low-confidence PRs
- Image verification: Computer vision to verify specimen images are correct
- Auto-merge: If guardrails pass and preview looks good, merge automatically
But for now, the human review step (2 minutes) catches edge cases and gives me confidence in what ships.