Blog
growth

The Content Flywheel: How User Searches Drive Automated Font Additions

How FontAlternatives' self-improving content flywheel turns user searches into automated font additions, creating compounding growth.

Mladen Ruzicic
Mladen Ruzicic
6 min

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:

  1. Lower review friction: Better automated checks mean faster reviews
  2. Expand unavailable index: More fonts to track demand for
  3. Improve Claude Code prompts: Higher quality first drafts

Things that slow it down:

  1. API cost limits: Claude Code costs ~$0.50/font
  2. Manual review required: I refuse to auto-merge
  3. 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

MetricPurpose
Searches/dayOverall demand
Unavailable searches/dayUnmet demand
Issues created/weekPipeline input
PRs merged/weekPipeline output
Fonts added/monthGrowth rate
Organic traffic/monthSEO 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

ComponentPurpose
Unavailable font indexDiscover demand
PostHog analyticsTrack demand
Cloudflare Worker cronAggregate demand
GitHub IssuesQueue processing
Claude CodeResearch + write
GitHub ActionsOrchestrate
Preview deploymentsReview changes
Human reviewQuality gate

Each component is simple. Together, they create a self-improving system.

Explore on FontAlternatives

#growth#automation#product#analytics

More from the blog