Boring Docs

SEO, Analytics, Blog — Ship + Measure + Write

Metadata helpers, self-hosted beacon, markdown posts.

SEO, Analytics, Blog

Three jobs that compound: Google has to find you (SEO), you have to see what works (analytics), and you need something worth finding (blog). Most founders do these in the wrong order — ads before content, content before measurement. We're going oldest-first: be findable, be measurable, then write. Come on, this is the fun part.

Be findable: metadata is a habit, not a task

Every page in this app gets its meta from one helper: getSEOTags() in libs/seo.js. Title, description, keywords, canonical URL, OpenGraph, Twitter card, robots directives — one call, same shape everywhere:

export const metadata = getSEOTags({
  canonicalUrlRelative: "/pricing",
  title: "Pricing — BoringPush",
  description: "One payment, yours forever. What $199 buys and why.",
});

New page? Copy those five lines before you write a single pixel of UI. It takes ten seconds and it's the difference between "Google shows my page" and "Google shows my page well." The two mistakes that actually hurt: duplicate titles across pages (every tab in the search results says the same thing — ask me how I know), and missing canonicals (your ?v=2 cache-buster links competing with the real URL in the index).

Blog posts and docs go further automatically. Each article's generateMetadata pulls title, description, keywords, and canonical from frontmatter, sets OG type: "article" with publishedTime, and attaches the cover image for share cards. You write markdown; the machinery writes meta. Your only job: frontmatter with a real description (under 160 characters, human sentence, no keyword stuffing — write it like the text under a blue link, because that's literally what it becomes).

Verify. View source on any page and read your own <head>. Then paste the URL into opengraph.xyz — if the card looks wrong there, it looks wrong everywhere. Fix it before you share, not after.

Be measurable: analytics without the surveillance bill

Here's my little rant, then the setup. Google Analytics is free the way a puppy is free: the script costs you ~150KB of main-thread time on every load (we measured — it's why our PageSpeed work bans render-blocking third parties), and you pay again in cookie banners and GDPR paperwork. So this kit ships its own beacon: AnalyticsPageView fires navigator.sendBeacon to /api/analytics/track on every route change, storing page views in your own SQLite. One session cookie by default, none in cookie-free mode, no third party, no script tax — sendBeacon fires after the page is interactive and never blocks rendering.

Two things that'll save you confusion later. First, exclude yourself: append ?boring_ignore=1 once (or set localStorage boring_ignore) and your own clicking stops polluting the numbers. Do this on day one, before you launch — founder traffic dwarfing real data makes every early chart a lie. Second, the blog index's "trending" section reads these same stats (last 30 days, safe-wrapped so a dead analytics DB never breaks the page). Your most-read posts surface themselves. That's the loop: write → measure → write more of what worked.

Verify. Open the network tab, click around, watch /api/analytics/track fire 204s. Then check the dashboard count moves. If events fire but counts don't move, the analytics DB path differs from the app DB path — check SQLITE_PATH vs the analytics store config.

Be writable: the blog that publishes when you save a file

Drop a .md file into data/blog/ with frontmatter (title, description, date, tags, cover, keywords) and the post is live: rendered HTML with heading anchors, syntax-highlighted code, computed reading time, FAQ schema if you include a ## FAQ section, JSON-LD, sitemap entry, RSS entry, and a raw markdown mirror at /blog/raw/[slug] for AI chatbots to quote you. No admin panel, no deploy step, no CMS subscription. I wrote this sentence in a text editor and you're reading it on the internet — that's the whole CMS.

The parts people skip (don't skip them): cover images — every post needs one, with real coverAlt text, because share cards without images get scrolled past and alt text is free accessibility. The description — it becomes the meta description, the share text, and the raw-mirror lede; write it once, carefully. Tags — they generate tag hub pages (/blog/tag/x) which are crawlable landing pages of their own; tag consistently (lowercase, hyphenated) or you get three hubs for one topic.

The cadence talk. Publishing 144 posts in a week looks exactly like what it is, and Google's first-crawl dates know the truth regardless of what your frontmatter claims. Backdating doesn't buy history. What buys traffic: one honest post a week, every week, aimed at a question your buyer actually typed. Ten real posts beat a hundred generated ones — the generated ones compete with every other generated corpus, and yours has no distribution advantage. Write from your scars (deployments, outages, migrations); scars rank because nobody else has them.

Verify. New post checklist: renders at /blog/slug ✓, in /blog list ✓, in sitemap.xml ✓, in /rss.xml ✓, share card on opengraph.xyz ✓, raw mirror at /blog/raw/slug ✓. Six checks, thirty seconds, every time.

Tell Google you exist (Search Console in ten minutes)

Nobody links to a week-old site, so don't wait to be discovered — introduce yourself. Google Search Console, Domain property (covers apex, www, everything), verify with a DNS TXT record at Cloudflare. Then Sitemaps → submit sitemap.xml, and URL Inspection → Request Indexing for /, /blog, and your three best posts. New sites get crawled within days of asking; without asking, within whenever.

Set expectations honestly: week one, only thin pages (TOS, privacy) may show for brand searches — that's normal, it means the crawler found the domain and hasn't gotten around to the good stuff. The homepage title carries your exact brand string, the docs carry long-tail answers, and every indexed page compounds. Check the queries report monthly: it tells you what buyers actually typed, which becomes your content roadmap. The tool is free, the data is the strategy. Don't skip it.

Write, measure, adjust. That's the engine. Next: 10-security — keeping the whole thing yours.