From a512eebc92bd7b4d960b9054d14537f6881b19f2 Mon Sep 17 00:00:00 2001 From: Florent LB Date: Mon, 10 Aug 2026 21:02:38 +0200 Subject: [PATCH 1/4] Add {get-started} The onboarding section of a hub page. It gives a new reader one opinionated path to a first success, before they face the full link list. The section is optional. Keeps the top-level install and tutorial pair as first-class fields rather than treating it as prototype back-compatibility. The designs use exactly that shape: a copyable command beside a tutorial card, with numbered steps below. The install snippet renders inside `.highlight pre` with a language class, which is what the site's copy button and syntax highlighter already select on. Both work with no extra wiring, because the hub layout kept the markdown-content id. Steps take one of three shapes: plain, a whole-card link, or two or more equally weighted options side by side. Numbering is automatic and sits before the title, because the section describes a sequence and the number is what carries that. Drops the prototype's per-step icon field. The icons restate the step title, the number badge is already the visual anchor, and a schema field for decoration makes every author pick icons that then drift between hubs. The LLM export renders the steps as an ordered list with options as sub-items. The search body gets nothing, for the same reason as the other hub directives. Co-Authored-By: Claude Opus 5 --- docs/_docset.yml | 1 + docs/examples/products/docs-builder.md | 22 ++ docs/syntax/get-started.md | 95 +++++++++ docs/syntax/hub-pages.md | 1 + .../Assets/markdown/hub.css | 198 ++++++++++++++++++ .../Myst/Directives/DirectiveBlockParser.cs | 3 + .../Myst/Directives/DirectiveHtmlRenderer.cs | 40 ++++ .../Myst/Directives/Hub/GetStartedBlock.cs | 121 +++++++++++ .../Myst/Directives/Hub/GetStartedView.cshtml | 116 ++++++++++ .../Directives/Hub/GetStartedViewModel.cs | 32 +++ .../LlmMarkdown/LlmBlockRenderers.cs | 48 +++++ .../PlainText/PlainTextBlockRenderers.cs | 1 + .../Myst/YamlSerialization.cs | 3 + tests/authoring/Blocks/Hub/GetStarted.fs | 106 ++++++++++ tests/authoring/authoring.fsproj | 1 + 15 files changed, 788 insertions(+) create mode 100644 docs/syntax/get-started.md create mode 100644 src/Elastic.Markdown/Myst/Directives/Hub/GetStartedBlock.cs create mode 100644 src/Elastic.Markdown/Myst/Directives/Hub/GetStartedView.cshtml create mode 100644 src/Elastic.Markdown/Myst/Directives/Hub/GetStartedViewModel.cs create mode 100644 tests/authoring/Blocks/Hub/GetStarted.fs diff --git a/docs/_docset.yml b/docs/_docset.yml index e518cc269..d18d7681c 100644 --- a/docs/_docset.yml +++ b/docs/_docset.yml @@ -122,6 +122,7 @@ toc: - file: file_inclusion.md - file: footnotes.md - file: frontmatter.md + - file: get-started.md - file: hero.md - file: hub-pages.md - file: icons.md diff --git a/docs/examples/products/docs-builder.md b/docs/examples/products/docs-builder.md index 1b13353a5..0a404dda6 100644 --- a/docs/examples/products/docs-builder.md +++ b/docs/examples/products/docs-builder.md @@ -12,6 +12,28 @@ description: docs-builder documentation. Build, validate, and publish Elastic do :tertiary-action: [Explore docs-builder](#explore) ::: +:::{get-started} +title: Get started in 3 steps +intro: Install docs-builder, write your first page, then preview and publish it. +steps: + - title: Install docs-builder + options: + - label: Build from source + description: Clone the repository and build the CLI yourself. + code: dotnet build + language: sh + - label: Run in a container + description: No local install needed. + url: /getting-started/installation.md + url-label: Container setup + - title: Write your first page + description: Author Markdown, add links, and use the directive set. + link: /getting-started/writing-content.md + link-label: Start writing + - title: Preview and publish + description: Serve the site locally with live reload, then publish it. +::: + ::::{card-group} :title: Get hands-on :id: hands-on diff --git a/docs/syntax/get-started.md b/docs/syntax/get-started.md new file mode 100644 index 000000000..77f301f2e --- /dev/null +++ b/docs/syntax/get-started.md @@ -0,0 +1,95 @@ +# Get started + +The onboarding section of a [hub page](hub-pages.md). It gives a new reader one opinionated path to a first success, before they face the full link list. + +The section is optional. Skip it when the hero already says what to do next. + +See the [docs-builder documentation hub](../examples/products/docs-builder.md) for a rendered section. + +## Basic + +A hub's onboarding section is three steps. The first offers two equally weighted ways to start, and the rest are single links. + +```markdown +:::{get-started} +title: Get started in 3 steps +intro: Install docs-builder, write your first page, then preview and publish it. +steps: + - title: Install docs-builder + options: + - label: Install locally + description: Install the CLI on your machine. + code: curl -sSL https://ela.st/docs-builder-install | sh + language: sh + - label: Run in a container + description: No local install needed. + url: /getting-started/installation.md + url-label: Container setup + - title: Write your first page + description: Author Markdown, add links, and use the directive set. + link: /getting-started/writing-content.md + link-label: Start writing + - title: Preview and publish + description: Serve the site locally with live reload, then publish it. + link: /getting-started/serve.md + link-label: Serve locally +::: +``` + +The body is YAML, not markdown, like [`{link-card}`](link-card.md). + +## Schema + +| Field | Notes | +|---|---| +| `title` | **Required.** H2 heading. | +| `intro` | One-line lead below the heading. | +| `steps` | The numbered steps. | + +Everything the section offers lives inside a step. An install command belongs in `steps[0].options[]`, which keeps the whole path inside the numbered sequence. + +A command in a step option goes through the standard code block, so it gets syntax highlighting and a copy button like every other code block on the site. + +Every field except `title` is optional. The [example hub](../examples/products/docs-builder.md) uses each one once, and shows all three step shapes, so you can start from it and delete what you do not need. + + +## Step shapes + +A step takes one of three shapes. + +**Plain.** A `title` and a `description`. Nothing is clickable. + +```yaml +- title: Preview and publish + description: Serve the site locally, then publish it. +``` + +**Link.** Add `link` and `link-label`, and the whole step card becomes clickable. + +```yaml +- title: Write your first page + description: Author markdown, add links, and use the directive set. + link: /getting-started/writing-content.md + link-label: Start writing +``` + +**Options.** Add `options` for two or more equally weighted paths, shown side by side. Each option takes a `label`, a `description`, and either a copyable `code` snippet with its `language`, or a `url` with a `url-label`. + +```yaml +- title: Preview and publish + options: + - label: Preview locally + description: Serve the site with live reload while you write. + code: docs-builder serve + language: sh + - label: Publish + description: Build the site and publish it. + url: /getting-started/publish.md + url-label: How to publish +``` + +Steps are numbered automatically, in source order. The number sits before the title, because the section describes a sequence and the number is what carries that. + +## Links + +Every `steps[].link` and `options[].url` validates at build time, using the same forms as [`{link-card}`](link-card.md#links). diff --git a/docs/syntax/hub-pages.md b/docs/syntax/hub-pages.md index 048ed37e6..e01bec467 100644 --- a/docs/syntax/hub-pages.md +++ b/docs/syntax/hub-pages.md @@ -56,6 +56,7 @@ Write both deliberately. The search body indexes the hero title and description | [`{hero}`](hero.md) | Identity band. Carries the product icon, the page title, a description, and up to three actions. | | [`{card-group}`](card-group.md) | Section heading and card grid. Renders as an accordion inside `{explore}`. | | [`{link-card}`](link-card.md) | One card: title, description, and a list of links. Renders as a link column inside `{explore}`. | +| [`{get-started}`](get-started.md) | Onboarding funnel. An install command, a tutorial link, and numbered steps. | | [`{explore}`](explore.md) | The browse-everything section. A stack of collapsible accordions. | ## Page skeleton diff --git a/src/Elastic.Documentation.Site/Assets/markdown/hub.css b/src/Elastic.Documentation.Site/Assets/markdown/hub.css index 6f87d5928..f3da2b672 100644 --- a/src/Elastic.Documentation.Site/Assets/markdown/hub.css +++ b/src/Elastic.Documentation.Site/Assets/markdown/hub.css @@ -150,6 +150,204 @@ flex-shrink: 0; } + /* Get started ------------------------------------------------------ */ + .hub-get-started { + @apply mx-auto w-full max-w-5xl; + margin-bottom: 56px; + scroll-margin-top: 96px; + } + .hub-get-started .hub-get-started-title { + margin: 0 0 8px; + font-size: 1.375rem; + font-weight: 700; + color: var(--color-ink); + } + .hub-get-started .hub-get-started-intro { + margin: 0 0 20px; + font-size: 0.9375rem; + color: var(--color-ink-light, #4c4c4c); + line-height: 1.5; + } + .hub-get-started .hub-get-started-install { + position: relative; + flex: 1 1 380px; + min-width: 0; + margin: 0; + } + .hub-get-started .hub-get-started-install pre { + margin: 0; + padding: 12px 16px; + overflow-x: auto; + background: var(--color-ink-navy); + border-radius: 10px; + } + .hub-get-started .hub-get-started-install code { + font-family: var(--font-mono, monospace); + font-size: 0.8125rem; + color: var(--color-white); + background: transparent; + } + + .hub-get-started .hub-get-started-steps { + @apply grid gap-4; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + margin: 0; + padding: 0; + list-style: none; + } + .hub-get-started .hub-get-started-step { + @apply relative flex flex-col; + padding: 20px; + background: var(--color-white); + border: 1px solid var(--color-grey-20); + border-radius: 14px; + } + .hub-get-started .hub-get-started-step-head { + @apply flex items-center; + gap: 12px; + margin-bottom: 10px; + padding-right: 34px; + } + .hub-get-started .hub-get-started-step-num { + position: absolute; + top: 20px; + right: 20px; + font-size: 0.8125rem; + font-weight: 600; + color: var(--color-grey-70, #98a2b3); + letter-spacing: 0.06em; + } + .hub-get-started .hub-get-started-step-title { + margin: 0; + font-size: 0.9375rem; + font-weight: 700; + color: var(--color-ink-dark); + } + .hub-get-started .hub-get-started-step-desc { + margin: 0; + font-size: 0.8125rem; + color: var(--color-ink-light, #4c4c4c); + line-height: 1.45; + } + + /* Step 01 with two equal start options spans the full row. */ + .hub-get-started .hub-get-started-steps:has(.hub-get-started-step-wide) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .hub-get-started .hub-get-started-step-wide { + grid-column: 1 / -1; + container-type: inline-size; + } + .hub-get-started .hub-get-started-options { + display: grid; + grid-template-columns: 1fr 1fr; + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid var(--color-grey-20); + } + .hub-get-started .hub-get-started-option { + @apply flex flex-col; + gap: 10px; + padding-right: 28px; + } + .hub-get-started .hub-get-started-option + .hub-get-started-option { + padding-right: 0; + padding-left: 28px; + border-left: 1px solid var(--color-grey-20); + } + .hub-get-started .hub-get-started-option-label { + font-size: 0.875rem; + font-weight: 700; + color: var(--color-ink-dark); + } + .hub-get-started .hub-get-started-option-desc { + margin: 0; + font-size: 0.8125rem; + color: var(--color-ink-light, #4c4c4c); + line-height: 1.45; + } + .hub-get-started .hub-get-started-option .hub-get-started-install { + flex: none; + width: 100%; + margin-top: auto; + } + .hub-get-started .hub-get-started-option-btn { + @apply inline-flex items-center justify-center; + gap: 8px; + width: 100%; + min-height: 48px; + margin-top: auto; + padding: 0 18px; + font-size: 0.875rem; + font-weight: 600; + color: var(--color-ink-dark); + background: var(--color-white); + border: 1px solid var(--color-ink-dark); + border-radius: 8px; + transition: + background 0.15s ease, + border-color 0.15s ease; + } + .hub-get-started .hub-get-started-option-btn:hover, + .hub-get-started .hub-get-started-option-btn:focus-visible { + background: var(--color-grey-10); + border-color: var(--color-ink-dark); + color: var(--color-ink-dark); + } + .hub-get-started .hub-get-started-option-link { + @apply inline-flex items-center gap-1; + align-self: flex-start; + font-size: 0.8125rem; + font-weight: 600; + color: var(--color-blue-elastic-100, #0b64dd); + } + .hub-get-started .hub-get-started-option-link:hover, + .hub-get-started .hub-get-started-option-link:focus-visible { + text-decoration: underline; + } + @container (max-width: 560px) { + .hub-get-started .hub-get-started-options { + grid-template-columns: 1fr; + } + .hub-get-started .hub-get-started-option { + padding-right: 0; + } + .hub-get-started .hub-get-started-option + .hub-get-started-option { + padding-left: 0; + padding-top: 16px; + margin-top: 4px; + border-left: 0; + border-top: 1px solid var(--color-grey-20); + } + } + + /* Steps 02/03 are clickable link cards. */ + .hub-get-started .hub-get-started-step-link { + padding: 0; + transition: + box-shadow 0.15s, + border-color 0.15s; + } + .hub-get-started .hub-get-started-step-link:hover, + .hub-get-started .hub-get-started-step-link:focus-visible { + border-color: var(--color-blue-elastic); + box-shadow: 0 1px 4px rgb(0 0 0 / 0.07); + } + .hub-get-started .hub-get-started-step-anchor { + @apply flex h-full flex-col; + padding: 20px; + color: inherit; + text-decoration: none; + } + .hub-get-started .hub-get-started-step-arrow { + @apply inline-flex items-center gap-1; + margin-top: auto; + padding-top: 14px; + font-size: 0.8125rem; + font-weight: 600; + color: var(--color-blue-elastic-100, #0b64dd); + } + /* Zone (section heading) ------------------------------------------ */ .hub-zone { @apply mx-auto w-full max-w-5xl; diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs index 37f48de27..991bed87a 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs @@ -149,6 +149,9 @@ protected override DirectiveBlock CreateFencedBlock(BlockProcessor processor) if (info.IndexOf("{link-card}") > 0) return new LinkCardBlock(this, context); + if (info.IndexOf("{get-started}") > 0) + return new GetStartedBlock(this, context); + if (info.IndexOf("{agent-skill}") > 0) return new AgentSkillBlock(this, context); diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs index ae5b57917..b5ada458f 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs @@ -116,6 +116,9 @@ protected override void Write(HtmlRenderer renderer, DirectiveBlock directiveBlo case LinkCardBlock linkCardBlock: WriteLinkCard(renderer, linkCardBlock); return; + case GetStartedBlock getStartedBlock: + WriteGetStarted(renderer, getStartedBlock); + return; case PageCardBlock pageCardBlock: WritePageCard(renderer, pageCardBlock); return; @@ -263,6 +266,43 @@ private static void WriteLinkCard(HtmlRenderer renderer, LinkCardBlock block) RenderRazorSlice(slice, renderer); } + private static void WriteGetStarted(HtmlRenderer renderer, GetStartedBlock block) + { + var data = block.Data; + var steps = new List(data.Steps.Length); + for (var i = 0; i < data.Steps.Length; i++) + { + var step = data.Steps[i]; + steps.Add(new GetStartedStepViewModel + { + Number = i + 1, + Title = step.Title, + DescriptionHtml = RenderInlineMarkdown(step.Description), + Link = step.Link, + LinkLabel = step.LinkLabel, + Options = [.. step.Options.Select(option => new GetStartedOptionViewModel + { + Label = option.Label, + DescriptionHtml = RenderInlineMarkdown(option.Description), + Code = option.Code, + Language = option.Language, + Url = option.Url, + UrlLabel = option.UrlLabel + })] + }); + } + + var slice = GetStartedView.Create(new GetStartedViewModel + { + DirectiveBlock = block, + Title = data.Title, + IntroHtml = RenderInlineMarkdown(data.Intro), + Steps = steps, + SitePathPrefix = block.Build.UrlPathPrefix + }); + RenderRazorSlice(slice, renderer); + } + // The hero description is a directive option, not a body block, so it never reaches // the document pipeline. Render it with the default Markdig pipeline for basic inline // markup. Substitutions, roles, and link validation do not apply inside this option. diff --git a/src/Elastic.Markdown/Myst/Directives/Hub/GetStartedBlock.cs b/src/Elastic.Markdown/Myst/Directives/Hub/GetStartedBlock.cs new file mode 100644 index 000000000..d2915faa2 --- /dev/null +++ b/src/Elastic.Markdown/Myst/Directives/Hub/GetStartedBlock.cs @@ -0,0 +1,121 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Markdown.Diagnostics; +using YamlDotNet.Core; +using YamlDotNet.Serialization; + +namespace Elastic.Markdown.Myst.Directives.Hub; + +/// +/// The first hub-body section: a short onboarding funnel with an intro line, an +/// optional install snippet, an optional tutorial link, and a set of numbered +/// steps. The schema is YAML-formatted in the directive body for predictable +/// structure. +/// +/// +/// +/// :::{get-started} +/// title: Get started in 3 steps +/// intro: Spin up Kibana, connect your data, and start exploring in minutes. +/// ::: +/// +/// +public class GetStartedBlock(DirectiveBlockParser parser, ParserContext context) + : DirectiveBlock(parser, context) +{ + public override string Directive => "get-started"; + + public GetStartedData Data { get; private set; } = GetStartedData.Empty; + + public override void FinalizeAndValidate(ParserContext context) + { + var yaml = HubYamlBody.Extract(this, new BuildContextFileReader(Build.ReadFileSystem)); + if (yaml is null) + { + this.EmitError("{get-started} requires a YAML body. See the get-started directive docs."); + return; + } + + try + { + Data = YamlSerialization.Deserialize(yaml, Build.ProductsConfiguration) ?? GetStartedData.Empty; + } + catch (YamlException ex) + { + this.EmitError($"{{get-started}} YAML parse error: {ex.Message}"); + return; + } + + if (string.IsNullOrWhiteSpace(Data.Title)) + this.EmitError("{get-started} requires a `title` field in its YAML body."); + + + foreach (var step in Data.Steps) + { + if (!string.IsNullOrWhiteSpace(step.Link)) + step.Link = DirectiveLinkValidator.ValidateAndResolve(step.Link, this, context); + foreach (var option in step.Options) + option.Url = DirectiveLinkValidator.ValidateAndResolve(option.Url, this, context); + } + } +} + +[YamlSerializable] +public record GetStartedData +{ + [YamlMember(Alias = "title")] + public string? Title { get; set; } + + [YamlMember(Alias = "intro")] + public string? Intro { get; set; } + + [YamlMember(Alias = "steps")] + public GetStartedStep[] Steps { get; set; } = []; + + public static GetStartedData Empty { get; } = new(); +} + +[YamlSerializable] +public record GetStartedStep +{ + [YamlMember(Alias = "title")] + public string? Title { get; set; } + + [YamlMember(Alias = "description")] + public string? Description { get; set; } + + /// When set, the whole step card links here. + [YamlMember(Alias = "link")] + public string? Link { get; set; } + + [YamlMember(Alias = "link-label")] + public string? LinkLabel { get; set; } + + /// Two-or-more equally-weighted start options rendered side by side. + [YamlMember(Alias = "options")] + public GetStartedStepOption[] Options { get; set; } = []; +} + +[YamlSerializable] +public record GetStartedStepOption +{ + [YamlMember(Alias = "label")] + public string? Label { get; set; } + + [YamlMember(Alias = "description")] + public string? Description { get; set; } + + [YamlMember(Alias = "code")] + public string? Code { get; set; } + + [YamlMember(Alias = "language")] + public string? Language { get; set; } + + [YamlMember(Alias = "url")] + public string? Url { get; set; } + + [YamlMember(Alias = "url-label")] + public string? UrlLabel { get; set; } +} diff --git a/src/Elastic.Markdown/Myst/Directives/Hub/GetStartedView.cshtml b/src/Elastic.Markdown/Myst/Directives/Hub/GetStartedView.cshtml new file mode 100644 index 000000000..9d0e433e3 --- /dev/null +++ b/src/Elastic.Markdown/Myst/Directives/Hub/GetStartedView.cshtml @@ -0,0 +1,116 @@ +@inherits RazorSlice + +
+
+ @if (!string.IsNullOrWhiteSpace(Model.Title)) + { +

@Model.Title

+ } + @if (!string.IsNullOrWhiteSpace(Model.IntroHtml)) + { +

@(new HtmlString(Model.IntroHtml))

+ } +
+ + @if (Model.Steps.Count > 0) + { +
    + @foreach (var step in Model.Steps) + { + @if (step.Options.Count > 0) + { +
  1. +
    + + @if (!string.IsNullOrWhiteSpace(step.Title)) + { +

    @step.Title

    + } +
    + @if (!string.IsNullOrWhiteSpace(step.DescriptionHtml)) + { +

    @(new HtmlString(step.DescriptionHtml))

    + } +
    + @foreach (var option in step.Options) + { +
    + @if (!string.IsNullOrWhiteSpace(option.Label)) + { + @option.Label + } + @if (!string.IsNullOrWhiteSpace(option.DescriptionHtml)) + { +

    @(new HtmlString(option.DescriptionHtml))

    + } + @if (!string.IsNullOrWhiteSpace(option.Code)) + { +
    +
    @option.Code
    +
    + } + @if (!string.IsNullOrWhiteSpace(option.Url)) + { + @if (!string.IsNullOrWhiteSpace(option.Code)) + { + + @(string.IsNullOrWhiteSpace(option.UrlLabel) ? "Learn more" : option.UrlLabel) + + + } + else + { + @(string.IsNullOrWhiteSpace(option.UrlLabel) ? "Get started" : option.UrlLabel) + } + } +
    + } +
    +
  2. + } + else if (!string.IsNullOrWhiteSpace(step.Link)) + { + + } + else + { +
  3. +
    + + @if (!string.IsNullOrWhiteSpace(step.Title)) + { +

    @step.Title

    + } +
    + @if (!string.IsNullOrWhiteSpace(step.DescriptionHtml)) + { +

    @(new HtmlString(step.DescriptionHtml))

    + } +
  4. + } + } +
+ } +
diff --git a/src/Elastic.Markdown/Myst/Directives/Hub/GetStartedViewModel.cs b/src/Elastic.Markdown/Myst/Directives/Hub/GetStartedViewModel.cs new file mode 100644 index 000000000..4067e13ac --- /dev/null +++ b/src/Elastic.Markdown/Myst/Directives/Hub/GetStartedViewModel.cs @@ -0,0 +1,32 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Elastic.Markdown.Myst.Directives.Hub; + +public class GetStartedViewModel : HubDirectiveViewModel +{ + public required string? Title { get; init; } + public required string? IntroHtml { get; init; } + public required IReadOnlyList Steps { get; init; } +} + +public sealed record GetStartedStepViewModel +{ + public required int Number { get; init; } + public required string? Title { get; init; } + public required string? DescriptionHtml { get; init; } + public required string? Link { get; init; } + public required string? LinkLabel { get; init; } + public required IReadOnlyList Options { get; init; } +} + +public sealed record GetStartedOptionViewModel +{ + public required string? Label { get; init; } + public required string? DescriptionHtml { get; init; } + public required string? Code { get; init; } + public required string? Language { get; init; } + public required string? Url { get; init; } + public required string? UrlLabel { get; init; } +} diff --git a/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmBlockRenderers.cs b/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmBlockRenderers.cs index a8de73952..0a4073322 100644 --- a/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmBlockRenderers.cs +++ b/src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmBlockRenderers.cs @@ -514,6 +514,9 @@ protected override void Write(LlmMarkdownRenderer renderer, DirectiveBlock obj) case LinkCardBlock linkCardBlock: WriteLinkCardBlock(renderer, linkCardBlock); return; + case GetStartedBlock getStartedBlock: + WriteGetStartedBlock(renderer, getStartedBlock); + return; } // Ensure single empty line before directive @@ -580,6 +583,51 @@ private static void WriteHeroBlock(LlmMarkdownRenderer renderer, HeroBlock heroB renderer.EnsureLine(); } + // The onboarding path is a sequence, so it exports as an ordered list. Options under a step + // become sub-items, each with its command or its link. + private static void WriteGetStartedBlock(LlmMarkdownRenderer renderer, GetStartedBlock block) + { + var data = block.Data; + renderer.EnsureBlockSpacing(); + + if (!string.IsNullOrEmpty(data.Title)) + { + renderer.WriteLine($"## {data.Title}"); + renderer.EnsureLine(); + } + if (!string.IsNullOrEmpty(data.Intro)) + { + renderer.WriteLine(data.Intro); + renderer.EnsureLine(); + } + + for (var i = 0; i < data.Steps.Length; i++) + WriteGetStartedStep(renderer, data.Steps[i], i + 1); + + renderer.EnsureLine(); + } + + private static void WriteGetStartedStep(LlmMarkdownRenderer renderer, GetStartedStep step, int number) + { + renderer.EnsureLine(); + var title = string.IsNullOrEmpty(step.Link) + ? step.Title + : $"[{step.Title}]({HubLinkForLlm(renderer, step.Link)})"; + renderer.WriteLine($"{number}. {title}"); + + if (!string.IsNullOrEmpty(step.Description)) + renderer.WriteLine($" {step.Description}"); + + foreach (var option in step.Options) + { + renderer.WriteLine($" - {option.Label}: {option.Description}"); + if (!string.IsNullOrEmpty(option.Code)) + renderer.WriteLine($" `{option.Code}`"); + if (!string.IsNullOrEmpty(option.Url)) + renderer.WriteLine($" [{option.UrlLabel ?? "Get started"}]({HubLinkForLlm(renderer, option.Url)})"); + } + } + // The curated grouping of links is what a hub page is for, so the export keeps the whole // structure: each section becomes a heading and each card a titled link list. private static void WriteExploreBlock(LlmMarkdownRenderer renderer, ExploreBlock block) diff --git a/src/Elastic.Markdown/Myst/Renderers/PlainText/PlainTextBlockRenderers.cs b/src/Elastic.Markdown/Myst/Renderers/PlainText/PlainTextBlockRenderers.cs index 6e60bfbe2..3b01468f1 100644 --- a/src/Elastic.Markdown/Myst/Renderers/PlainText/PlainTextBlockRenderers.cs +++ b/src/Elastic.Markdown/Myst/Renderers/PlainText/PlainTextBlockRenderers.cs @@ -291,6 +291,7 @@ protected override void Write(PlainTextRenderer renderer, DirectiveBlock obj) case ExploreBlock: case CardGroupBlock: case LinkCardBlock: + case GetStartedBlock: return; case AgentSkillBlock agentSkillBlock: diff --git a/src/Elastic.Markdown/Myst/YamlSerialization.cs b/src/Elastic.Markdown/Myst/YamlSerialization.cs index 5f4cc7308..9f005703b 100644 --- a/src/Elastic.Markdown/Myst/YamlSerialization.cs +++ b/src/Elastic.Markdown/Myst/YamlSerialization.cs @@ -43,4 +43,7 @@ public static T Deserialize(string yaml, ProductsConfiguration products) [YamlSerializable(typeof(ChangelogDirectiveBundleConfigYaml))] [YamlSerializable(typeof(Elastic.Markdown.Myst.Directives.Hub.LinkCardData))] [YamlSerializable(typeof(Elastic.Markdown.Myst.Directives.Hub.LinkCardLink))] +[YamlSerializable(typeof(Elastic.Markdown.Myst.Directives.Hub.GetStartedData))] +[YamlSerializable(typeof(Elastic.Markdown.Myst.Directives.Hub.GetStartedStep))] +[YamlSerializable(typeof(Elastic.Markdown.Myst.Directives.Hub.GetStartedStepOption))] public partial class DocsBuilderYamlStaticContext; diff --git a/tests/authoring/Blocks/Hub/GetStarted.fs b/tests/authoring/Blocks/Hub/GetStarted.fs new file mode 100644 index 000000000..9205f6d80 --- /dev/null +++ b/tests/authoring/Blocks/Hub/GetStarted.fs @@ -0,0 +1,106 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information +module ``AuthoringTests``.``block elements``.``hub``.``get started elements`` + +open Xunit +open authoring + +type ``get started with a title and intro`` () = + static let markdown = Setup.Markdown """ +:::{get-started} +title: Get started in 3 steps +intro: Install, write, preview. +steps: + - title: Install + description: Install the CLI. +::: +""" + + [] + let ``renders the heading`` () = + markdown |> convertsToContainingHtml """

Get started in 3 steps

""" + + [] + let ``numbers steps from one, zero padded`` () = + markdown |> convertsToContainingRawHtml """""" + + // Nothing renders between the intro and the numbered list. The section is the steps. + [] + let ``renders nothing above the steps`` () = + markdown |> doesNotConvertToContainingHtml "hub-get-started-actions" + + [] + let ``has no errors`` () = markdown |> hasNoErrors + +type ``get started with a link step`` () = + static let markdown = Setup.Markdown """ +:::{get-started} +title: Get started +steps: + - title: Write your first page + description: Author markdown. + link: /index.md + link-label: Start writing +::: +""" + + [] + let ``makes the whole step clickable`` () = + markdown |> convertsToContainingHtml """Start writing""" + + [] + let ``has no errors`` () = markdown |> hasNoErrors + +type ``get started with option steps`` () = + static let markdown = Setup.Markdown """ +:::{get-started} +title: Get started +steps: + - title: Preview and publish + options: + - label: Preview locally + description: Serve with live reload. + code: docs-builder serve + language: sh + - label: Publish + description: Build and publish. + url: /index.md + url-label: How to publish +::: +""" + + [] + let ``renders both options`` () = + markdown |> convertsToContainingHtml """Preview locally""" + + [] + let ``renders the option command`` () = + markdown |> convertsToContainingHtml """docs-builder serve""" + + [] + let ``has no errors`` () = markdown |> hasNoErrors + +type ``get started with a relative step link`` () = + static let markdown = Setup.Markdown """ +:::{get-started} +title: Get started +steps: + - title: Broken + link: nope.md +::: +""" + + [] + let ``rejects a relative path`` () = + markdown |> hasError "must be an absolute path starting with `/`" + +type ``get started without a body`` () = + static let markdown = Setup.Markdown """ +:::{get-started} +::: +""" + + [] + let ``errors`` () = + markdown |> hasError "{get-started}" diff --git a/tests/authoring/authoring.fsproj b/tests/authoring/authoring.fsproj index 723a8d7ba..4bd3e42fe 100644 --- a/tests/authoring/authoring.fsproj +++ b/tests/authoring/authoring.fsproj @@ -57,6 +57,7 @@ + From 8ceced16e2db6674a01af842ae1003f3f28008d6 Mon Sep 17 00:00:00 2001 From: Florent LB Date: Wed, 12 Aug 2026 20:55:36 +0200 Subject: [PATCH 2/4] Apply review feedback to the get-started section Martijn's review on #3825 covered the whole page, so the same rules apply here. - Get started type moves onto the site's scale. The values it carried were 16px-root arithmetic against a 14px root, so every one rendered small. - Step numbers move from grey-70 to ink-light. grey-70 reads at 3.31:1 on white, short of the 4.5:1 minimum, and ink-light carries the same quiet weight at 7.03:1. Co-Authored-By: Claude Opus 5 --- .../Assets/markdown/hub.css | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/Elastic.Documentation.Site/Assets/markdown/hub.css b/src/Elastic.Documentation.Site/Assets/markdown/hub.css index f3da2b672..844ead925 100644 --- a/src/Elastic.Documentation.Site/Assets/markdown/hub.css +++ b/src/Elastic.Documentation.Site/Assets/markdown/hub.css @@ -158,13 +158,13 @@ } .hub-get-started .hub-get-started-title { margin: 0 0 8px; - font-size: 1.375rem; + font-size: var(--text-xl); font-weight: 700; color: var(--color-ink); } .hub-get-started .hub-get-started-intro { margin: 0 0 20px; - font-size: 0.9375rem; + font-size: var(--text-base); color: var(--color-ink-light, #4c4c4c); line-height: 1.5; } @@ -183,7 +183,7 @@ } .hub-get-started .hub-get-started-install code { font-family: var(--font-mono, monospace); - font-size: 0.8125rem; + font-size: var(--text-sm); color: var(--color-white); background: transparent; } @@ -212,20 +212,23 @@ position: absolute; top: 20px; right: 20px; - font-size: 0.8125rem; + font-size: var(--text-sm); font-weight: 600; - color: var(--color-grey-70, #98a2b3); + /* grey-70 reads at 3.31:1 on white, short of the 4.5:1 minimum. ink-light + carries the same quiet weight at 7.03:1 and is what other secondary text + on the page already uses. */ + color: var(--color-ink-light); letter-spacing: 0.06em; } .hub-get-started .hub-get-started-step-title { margin: 0; - font-size: 0.9375rem; + font-size: var(--text-base); font-weight: 700; color: var(--color-ink-dark); } .hub-get-started .hub-get-started-step-desc { margin: 0; - font-size: 0.8125rem; + font-size: var(--text-sm); color: var(--color-ink-light, #4c4c4c); line-height: 1.45; } @@ -256,13 +259,13 @@ border-left: 1px solid var(--color-grey-20); } .hub-get-started .hub-get-started-option-label { - font-size: 0.875rem; + font-size: var(--text-sm); font-weight: 700; color: var(--color-ink-dark); } .hub-get-started .hub-get-started-option-desc { margin: 0; - font-size: 0.8125rem; + font-size: var(--text-sm); color: var(--color-ink-light, #4c4c4c); line-height: 1.45; } @@ -278,7 +281,7 @@ min-height: 48px; margin-top: auto; padding: 0 18px; - font-size: 0.875rem; + font-size: var(--text-sm); font-weight: 600; color: var(--color-ink-dark); background: var(--color-white); @@ -297,7 +300,7 @@ .hub-get-started .hub-get-started-option-link { @apply inline-flex items-center gap-1; align-self: flex-start; - font-size: 0.8125rem; + font-size: var(--text-sm); font-weight: 600; color: var(--color-blue-elastic-100, #0b64dd); } @@ -343,7 +346,7 @@ @apply inline-flex items-center gap-1; margin-top: auto; padding-top: 14px; - font-size: 0.8125rem; + font-size: var(--text-sm); font-weight: 600; color: var(--color-blue-elastic-100, #0b64dd); } From 0a3e470c868f1f4de38885f392a4a07b282570a3 Mon Sep 17 00:00:00 2001 From: Florent LB Date: Thu, 13 Aug 2026 09:59:50 +0200 Subject: [PATCH 3/4] Raise get-started text to body size Martijn's point about tiny fonts applies to every piece of hub text. Step descriptions, option labels, option descriptions, option buttons, and the step links now read at the body size. The install command keeps 14px, which is what every other code block on the site renders at. Co-Authored-By: Claude Opus 5 --- .../Assets/markdown/hub.css | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Elastic.Documentation.Site/Assets/markdown/hub.css b/src/Elastic.Documentation.Site/Assets/markdown/hub.css index 844ead925..a8c483f9b 100644 --- a/src/Elastic.Documentation.Site/Assets/markdown/hub.css +++ b/src/Elastic.Documentation.Site/Assets/markdown/hub.css @@ -212,7 +212,7 @@ position: absolute; top: 20px; right: 20px; - font-size: var(--text-sm); + font-size: var(--text-base); font-weight: 600; /* grey-70 reads at 3.31:1 on white, short of the 4.5:1 minimum. ink-light carries the same quiet weight at 7.03:1 and is what other secondary text @@ -228,7 +228,7 @@ } .hub-get-started .hub-get-started-step-desc { margin: 0; - font-size: var(--text-sm); + font-size: var(--text-base); color: var(--color-ink-light, #4c4c4c); line-height: 1.45; } @@ -259,13 +259,13 @@ border-left: 1px solid var(--color-grey-20); } .hub-get-started .hub-get-started-option-label { - font-size: var(--text-sm); + font-size: var(--text-base); font-weight: 700; color: var(--color-ink-dark); } .hub-get-started .hub-get-started-option-desc { margin: 0; - font-size: var(--text-sm); + font-size: var(--text-base); color: var(--color-ink-light, #4c4c4c); line-height: 1.45; } @@ -281,7 +281,7 @@ min-height: 48px; margin-top: auto; padding: 0 18px; - font-size: var(--text-sm); + font-size: var(--text-base); font-weight: 600; color: var(--color-ink-dark); background: var(--color-white); @@ -300,7 +300,7 @@ .hub-get-started .hub-get-started-option-link { @apply inline-flex items-center gap-1; align-self: flex-start; - font-size: var(--text-sm); + font-size: var(--text-base); font-weight: 600; color: var(--color-blue-elastic-100, #0b64dd); } @@ -346,7 +346,7 @@ @apply inline-flex items-center gap-1; margin-top: auto; padding-top: 14px; - font-size: var(--text-sm); + font-size: var(--text-base); font-weight: 600; color: var(--color-blue-elastic-100, #0b64dd); } From 0bbe5cc3169f0170c44cff8fbab6bc387440be1e Mon Sep 17 00:00:00 2001 From: Florent LB Date: Thu, 13 Aug 2026 11:40:07 +0200 Subject: [PATCH 4/4] Use the shared arrow in get-started Both links that carry an arrow here send the reader onward out of the card, which is one of the two cases the site marks. They now use the same glyph and hover slide as every other hub arrow. The option button carries none, like every other button on the site. Co-Authored-By: Claude Opus 5 --- .../Myst/Directives/Hub/GetStartedView.cshtml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Elastic.Markdown/Myst/Directives/Hub/GetStartedView.cshtml b/src/Elastic.Markdown/Myst/Directives/Hub/GetStartedView.cshtml index 9d0e433e3..9108884af 100644 --- a/src/Elastic.Markdown/Myst/Directives/Hub/GetStartedView.cshtml +++ b/src/Elastic.Markdown/Myst/Directives/Hub/GetStartedView.cshtml @@ -55,8 +55,8 @@ { @(string.IsNullOrWhiteSpace(option.UrlLabel) ? "Learn more" : option.UrlLabel) - } @@ -87,8 +87,8 @@ }