feat(shell): SEO/OG meta tags, per-page descriptions, host-run OG image routine

Merged
MH
4mo ago
+1137-5

Summary

Closes #48. Every docs-kit page now ships a complete SEO <head> — meta
description, Open Graph, Twitter Card, canonical, favicon, robots, and
theme-color — so a link shared to Slack/Discord/X/LinkedIn renders a rich card
instead of a bare URL. It's all config-driven; a site that sets nothing still gets
a valid minimal card, so this is fully backwards-compatible.

Native Phlex (no meta-tags gem, no new runtime dependency), matching how the
brand-marks / topbar-links features were added.

What's new

  • DocsUI::MetaTags — a Phlex component that reads DocsKit.configuration +
    the per-page title/description and emits the meta/link tags inside Shell's head.
  • c.seo config block (DocsKit::SeoConfig) with backwards-safe defaults:
    description, og_image, og_type, twitter_card/site/creator, locale,
    site_url, robots, favicon, theme_color.
  • Per-page descriptions — DocsUI::Page.description "...", falling back to the
    page's #lead, then to the site-wide c.seo.description.
  • OG image routine — docs-kit ships a neutral 1200×630 default so og:image is
    never broken, and installs a docs_kit:og rake task that screenshots a site's
    own landing page into app/assets/images/og/. The screenshot tooling
    (shot-scraper / chromium) is resolved at task runtime and is never a
    docs-kit runtime dependency — DocsKit::OgGenerator is loaded only by the task.
    A guard spec keeps the shipped default present. This mirrors phlex-reactive's
    vendored-client re-sync: a documented command + a guard spec, not a cron.
  • Install path — the generator documents c.seo, installs the task + default
    image; docs-kit new reminds the owner to run it. README + .claude/rules/seo.md
    • CHANGELOG updated.

Decisions (confirmed with the requester on #48)

ForkChosenWhy
Meta engineNative DocsUI::MetaTagsOn-pattern, dependency-free, pure-Phlex head
OG imagesConfig path + shipped default + host-run rakeImage is each site's own branding; no binary blobs/browser in the gem's CI
Page descriptionsAuthorable + #lead fallbackBest SEO, least authoring friction

Rejected: the meta-tags gem (hard dep + non-Phlex head), a runtime GET /og.png
endpoint (larger surface), committing screenshots into the gem (blobs every site
overrides), and a cron refresh (phlex-reactive's precedent is a guard spec).

Test plan

  • bundle exec rake → 714 examples, 0 failures; coverage 94.45% (100% for
    Configuration and SeoConfig), rubocop clean.
  • gem build --strict packages cleanly and includes the OG image + new files.
  • Backwards compat verified: an unconfigured site renders a valid minimal card and
    its <head> is a strict superset of before.
  • No CSS classes emitted — application.tailwind.css untouched, no build:css needed.

New/changed specs:

  • spec/docs_kit/seo_config_spec.rb — defaults + per-field round-trip
  • spec/docs_ui/meta_tags_spec.rb — description/OG/Twitter/canonical/favicon/robots/
    theme-color, escaping (attribute-breakout containment), minimal-default, request branch
  • spec/docs_ui/shell_spec.rb — Shell threads title/description into MetaTags
  • spec/docs_ui/page_spec.rb — self.class.description || lead resolution
  • spec/docs_kit/og_generator_spec.rb — shooter resolution + per-tool command building
  • spec/docs_kit/og_image_spec.rb — shipped default present + valid PNG (guard)
  • spec/generators/install_generator_spec.rb — c.seo block + og.rake + default image

https://claude.ai/code/session_01FPQb6z3YwcKRMbvoJhdxnX