Skip to content

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

Merged
mhenrixon merged 2 commits into
mainfrom
feat/landing-component
Jul 4, 2026
Merged

feat(landing): DocsUI::Landing — a config-driven marketing landing page#55
mhenrixon merged 2 commits into
mainfrom
feat/landing-component

Conversation

@mhenrixon

Copy link
Copy Markdown
Owner

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):

  • Heroeyebrow, 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 gridfeatures 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.
  • Generatorlandings#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.

mhenrixon added 2 commits July 4, 2026 16:34
Every consuming site (and this dogfood site) was hand-rolling a home page.
Add a shared DocsUI::Landing component driven by a new c.landing config block
(DocsKit::LandingConfig):

- a hero: eyebrow, title (wrap a run in **double asterisks** to accent it in the
  primary color), lead, an optional install code snippet, and CTA buttons;
- a features card grid; and
- a registry-grouped documentation index built from nav_groups, so it never
  drifts from the authored pages.

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

- lib/docs_kit/landing_config.rb: the config + Cta/Feature value objects (Hash →
  value-object normalization, like 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 the initializer),
  proving the pattern on a real site.

This is the first landing pattern proven on a MOUNTED docs app (a docs section
inside a larger Rails app whose "/" is already taken) — contributed back from that
use case.

Tests: 91 config/component-config examples + a dogfood request spec; full gem
suite 755 green, 94.7% line coverage, rubocop clean.
The docs_chrome system spec asserts have_link('Get started', href: '/docs/overview');
the dogfood landing config pointed it at /docs/installation. Align the CTA with
the existing chrome contract.
@mhenrixon mhenrixon self-assigned this Jul 4, 2026
@mhenrixon mhenrixon merged commit 41ecde4 into main Jul 4, 2026
5 checks passed
@mhenrixon mhenrixon added the enhancement New feature or request label Jul 4, 2026
@mhenrixon mhenrixon deleted the feat/landing-component branch July 4, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant