feat(landing): DocsUI::Landing — a config-driven marketing landing page

Merged
MH
4mo ago
+512-65

Why

Every consuming site — and this dogfood site — hand-rolls its home page: a hero, some feature cards, and a list of doc links. That's the same shape every time, so it should be a component you configure, not code you copy.

This gap is sharpest on a mounted docs app — a docs section living inside a larger Rails app whose / is already taken (e.g. an authenticated dashboard). There, the landing can't live at / like a standalone docs site; it lives at the docs root, and the app owns the layout. This PR is contributed back from exactly that use case (a fintech app mounting its developer docs at /docs), which is the first time the landing pattern has been exercised outside a standalone site.

What

A shared DocsUI::Landing component driven by a new c.landing config block (DocsKit::LandingConfig):

  • Hero — eyebrow, title (wrap a run in **double asterisks** to accent it in the primary color, so a site can highlight a word without HTML), lead, an optional install code snippet, and ctas.
  • Feature grid — features cards (lucide icon + title + body).
  • Documentation index — grouped from nav_groups, so it never drifts from the authored pages. Toggle with c.landing.doc_index = false.
DocsKit.configure do |c|
  c.landing.eyebrow  = "docs-kit"
  c.landing.title    = "Shared docs chrome for **Rails**, in Phlex."
  c.landing.lead     = "Configure it once, write your pages, deploy with one workflow."
  c.landing.install  = { code: 'gem "docs-kit"', filename: "Gemfile", lexer: :ruby }
  c.landing.ctas     = [{ label: "Get started", href: "/docs/installation", style: :primary }]
  c.landing.features = [{ icon: "code", title: "Syntax highlighting", body: "…" }]
end

# a controller that includes DocsKit::Controller
def show = render_page(DocsUI::Landing.new)

Every field is optional — with an empty c.landing it still renders a minimal hero (brand + doc index), never a broken page. It composes DocsUI::Shell (a full document), so it renders with layout: false like DocsUI::Page, and its .md/.text twin works like any page.

Changed

  • lib/docs_kit/landing_config.rb — the config plus Cta / Feature value objects (Hash → value-object normalization, mirroring TopbarLink). Wired as c.landing, memoized like c.seo.
  • app/components/docs_ui/landing.rb — the component.
  • Generator — landings#show now renders DocsUI::Landing; the initializer template documents c.landing.
  • Dogfood — the docs-kit site's own landing now uses it (config in docs/config/initializers/docs_kit.rb), replacing the hand-rolled Views::Landings::Show.

Tests

  • spec/docs_kit/landing_config_spec.rb — the config + value objects (defaults, normalization, btn_class, external?, install lexer).
  • spec/docs_kit/configuration_spec.rb — #landing returns a memoized LandingConfig.
  • docs/spec/requests/progressive_enhancement_spec.rb — the rendered landing (hero title with the highlighted span, a feature, a CTA, the doc index) served with JS off.

Full gem suite 755 examples, 0 failures, 94.7% line coverage; rubocop clean (131 files); dogfood request specs green.

Notes

The component spec is a config/value-object unit spec rather than a full-render spec, because rendering DocsUI::Shell standalone needs a live Rails view context (csrf_meta_tags / stylesheet_link_tag / importmap tags) — the same reason DocsUI::Page isn't loaded in the Rails-free suite. The full render is covered by the dogfood request spec, matching how the existing chrome is tested.

Follow-up (not in this PR): a DocsUI::Landing section on the Components doc page.