This post covers the strategy behind FontAlternatives’ growth engine. For the technical implementation, see Building a Demand-Driven Content Pipeline.
FontAlternatives started with 50 fonts. Now it has 300+, with a pipeline that adds new fonts automatically based on what users search for.
The flywheel: more fonts bring more users, whose searches reveal which fonts to add next.
The flywheel concept
flowchart TD
A[User searches for 'Avenir'] --> B[Search shows 'Coming soon' badge]
B --> C[PostHog tracks demand signal]
C --> D[Daily cron queries demand data]
D --> E[High-demand fonts get GitHub issues]
E --> F[Claude Code processes issue]
F --> G[New font page goes live]
G --> H[Google indexes the page]
H --> I[More users find the site]
I --> A
Each revolution adds fonts that bring more users who reveal more demand.
The demand signal
I index 6,198 fonts in search, but only 303 have pages. When users search for the others, they see results with “Coming soon” badges:
interface SearchResult {
slug: string;
name: string;
available: boolean; // true = has page, false = coming soon
}
These “unavailable” searches are demand signals.
Tracking with PostHog
When a user’s search matches an unavailable font, I fire an event:
function handleSearch(query: string, results: SearchResult[]): void {
const unavailableMatches = results.filter(
r => !r.available && fuzzyScore(query, r.name) > 0.8
);
for (const match of unavailableMatches) {
posthog.capture('unavailable_font_searched', {
font_name: match.name,
query: query,
});
}
}
Over time, this builds a ranking of desired fonts. The PostHog proxy captures events even from users with ad blockers.
The daily cron
A Cloudflare Worker runs daily, querying PostHog for demand:
export default {
async scheduled(event: ScheduledEvent, env: Env): Promise<void> {
// Query PostHog for top searched unavailable fonts
const demand = await queryPostHog(`
SELECT properties.font_name, COUNT(*) as count
FROM events
WHERE event = 'unavailable_font_searched'
AND timestamp > now() - interval 30 day
GROUP BY properties.font_name
HAVING count >= 5
ORDER BY count DESC
LIMIT 10
`);
// Check which fonts don't have issues yet
const existingIssues = await getOpenIssues(env);
const newDemand = demand.filter(
d => !existingIssues.has(d.font_name)
);
// Create issues for top 3
for (const font of newDemand.slice(0, 3)) {
await createGitHubIssue(font, env);
}
},
};
Rate limited to 3 issues/day to prevent spam.
The GitHub issue
Issues follow a template (here’s an example for Avenir):
## Font Request: Avenir
**Demand signal:** 47 searches in the last 30 days
### Research needed:
- [ ] Identify foundry
- [ ] Determine classification
- [ ] Find 2-3 free alternatives
- [ ] Calculate similarity scores
### Auto-detected info:
- Classification: likely sans-serif (based on name patterns)
- Foundry: likely Lineto (based on web search)
The issue contains enough context for automated processing.
Claude Code processing
When an issue gets the font-request label, a GitHub Action triggers:
on:
issues:
types: [labeled]
jobs:
process:
if: github.event.label.name == 'font-request'
steps:
- name: Process with Claude Code
run: |
claude --print "
Process this font request:
${{ github.event.issue.body }}
1. Research the font
2. Find free alternatives
3. Create the markdown file
4. Update bidirectional links
5. Download specimen images
"
Claude researches, writes content, and prepares a PR.
The PR pipeline
flowchart TD
A[Issue created] --> B[Label applied]
B --> C[Claude Code processes]
C --> D[Branch created]
D --> E[Content written]
E --> F[Images downloaded]
F --> G[PR opened]
G --> H[Preview deployed]
H --> I[Human review]
I --> J[Merge + deploy]
style A fill:#e3f2fd
style J fill:#c8e6c9
Human review is the only manual step. Everything else is automated.
The compounding effect
Month 1: 50 fonts, 1,000 searches/month Month 3: 150 fonts, 5,000 searches/month Month 6: 300 fonts, 15,000 searches/month
More fonts means:
- More pages indexed by Google
- More long-tail search queries matched
- More users finding the site
- More demand signals generated
The feedback loop quality
Not all demand is equal. I weight signals:
function calculatePriority(fontName: string, searches: number): number {
let priority = searches;
// Boost fonts from reputable foundries
const foundry = guessFoundry(fontName);
if (TIER_1_FOUNDRIES.includes(foundry)) {
priority *= 1.5;
}
// Boost fonts with high-intent search patterns
// (people searching "free alternative to X" vs just "X")
const intentPatterns = getSearchPatterns(fontName);
if (intentPatterns.some(p => p.includes('alternative'))) {
priority *= 2;
}
return priority;
}
High-intent searches from premium foundry fonts get processed first.
What drives the flywheel
Inputs:
- User searches (demand signals)
- Time (cron runs daily)
- Compute (Claude Code API credits)
Outputs:
- New font pages
- Better search coverage
- More organic traffic
Friction points:
- Rate limiting (3 fonts/day max)
- Review bottleneck (I review every PR)
- Foundry image availability (some fonts have no images)
Optimizing the flywheel
Things that speed it up:
- Lower review friction: Better automated checks mean faster reviews
- Expand unavailable index: More fonts to track demand for
- Improve Claude Code prompts: Higher quality first drafts
Things that slow it down:
- API cost limits: Claude Code costs ~$0.50/font
- Manual review required: I refuse to auto-merge
- Image scraping failures: Need manual intervention
The self-healing aspect
When things break, the flywheel self-corrects:
- Bad content? Users bounce, no backlinks, page falls in rankings
- Missing fonts? Users search for them, creating demand signals
- Broken links? Guardrails catch them before merge
The system optimizes for quality because low-quality pages get deprioritized naturally.
Metrics I track
| Metric | Purpose |
|---|---|
| Searches/day | Overall demand |
| Unavailable searches/day | Unmet demand |
| Issues created/week | Pipeline input |
| PRs merged/week | Pipeline output |
| Fonts added/month | Growth rate |
| Organic traffic/month | SEO success |
The ratio of unavailable searches to total searches tells me how much unmet demand exists.
The end state
Eventually, the flywheel slows:
- Most popular fonts are covered
- Diminishing returns on long-tail
- Demand signals get weaker
At that point, the site transitions from growth mode to maintenance mode. But with 6,000+ fonts in the potential index, that’s a long way off.
Tradeoffs
What I gained:
- Self-prioritizing roadmap
- Automated content creation
- Compounding growth
What I lost:
- Editorial control (users drive priorities)
- Predictable schedule (demand spikes)
- Some quality (automated content is consistent but generic)
The key insight: Users know what they want better than I do. By listening to their searches and responding automatically, I build what they need without guessing.
The full stack
| Component | Purpose |
|---|---|
| Unavailable font index | Discover demand |
| PostHog analytics | Track demand |
| Cloudflare Worker cron | Aggregate demand |
| GitHub Issues | Queue processing |
| Claude Code | Research + write |
| GitHub Actions | Orchestrate |
| Preview deployments | Review changes |
| Human review | Quality gate |
Each component is simple. Together, they create a self-improving system.