feat(landing): DocsUI::Landing — a config-driven marketing landing page
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 optionalinstallcode snippet, andctas. - Feature grid —
featurescards (lucide icon + title + body). - Documentation index — grouped from
nav_groups, so it never drifts from the authored pages. Toggle withc.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 plusCta/Featurevalue objects (Hash → value-object normalization, mirroringTopbarLink). Wired asc.landing, memoized likec.seo.app/components/docs_ui/landing.rb— the component.- Generator —
landings#shownow rendersDocsUI::Landing; the initializer template documentsc.landing. - Dogfood — the docs-kit site's own landing now uses it (config in
docs/config/initializers/docs_kit.rb), replacing the hand-rolledViews::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—#landingreturns a memoizedLandingConfig.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.