feat(shell): SEO/OG meta tags, per-page descriptions, host-run OG image routine
Merged
MH
4mo ago+1137+1137+0123456789012345678901234567890123456789-5-5-0123456789
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 readsDocsKit.configuration+
the per-page title/description and emits the meta/link tags insideShell's head.c.seoconfig 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-widec.seo.description. - OG image routine — docs-kit ships a neutral 1200×630 default so
og:imageis
never broken, and installs adocs_kit:ograke task that screenshots a site's
own landing page intoapp/assets/images/og/. The screenshot tooling
(shot-scraper / chromium) is resolved at task runtime and is never a
docs-kit runtime dependency —DocsKit::OgGeneratoris 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 newreminds the owner to run it. README +.claude/rules/seo.md- CHANGELOG updated.
Decisions (confirmed with the requester on #48)
| Fork | Chosen | Why |
|---|---|---|
| Meta engine | Native DocsUI::MetaTags | On-pattern, dependency-free, pure-Phlex head |
| OG images | Config path + shipped default + host-run rake | Image is each site's own branding; no binary blobs/browser in the gem's CI |
| Page descriptions | Authorable + #lead fallback | Best 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
ConfigurationandSeoConfig),rubocopclean.gem build --strictpackages 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.cssuntouched, nobuild:cssneeded.
New/changed specs:
spec/docs_kit/seo_config_spec.rb— defaults + per-field round-tripspec/docs_ui/meta_tags_spec.rb— description/OG/Twitter/canonical/favicon/robots/
theme-color, escaping (attribute-breakout containment), minimal-default, request branchspec/docs_ui/shell_spec.rb— Shell threads title/description into MetaTagsspec/docs_ui/page_spec.rb—self.class.description || leadresolutionspec/docs_kit/og_generator_spec.rb— shooter resolution + per-tool command buildingspec/docs_kit/og_image_spec.rb— shipped default present + valid PNG (guard)spec/generators/install_generator_spec.rb—c.seoblock +og.rake+ default image