Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,22 @@ All notable changes to ShellDocs land here. Format follows [Keep a Changelog](ht

- **Preview layout.** `razor:preview stretch` (fence info string), or `Layout="stretch"` on `<DemoPreview>` / `<ComponentPreview>` / `PreviewFrame`, lets block-level components (charts, inputs, tables) fill the frame instead of shrinking to their content. `center` stays the default.
- **`not-prose` class.** Every `.shelldocs-prose` rule skips `.not-prose` subtrees. Preview frames carry it; add it to any element that should keep page typography out.
- **Generic components in markdown.** `RegisterComponentsFromAssembly` now registers generic components under their bare name (`<BarChart>`), and `razor:preview`, inline tags and `<ComponentPreview>` close them from a type-parameter attribute, as Razor does: `<BarChart TItem="SalesRow" />`. Type arguments take C# spellings (`int?`, `List<string>`, full or simple type names). Without one, `object` is used when the constraints allow it (with a warning); otherwise the component renders an inline error instead of breaking the page. A non-generic component with the same name keeps the tag. `<AutoTypeTable>` lists the type parameters.
- **Redirects.** URLs that aren't pages now redirect: content folders and version roots to their first page (`/docs` prefers unversioned pages, then the latest version), and unversioned URLs that exist in the latest version to that page (`/docs/cli/dev` → `/docs/v2.0/cli/dev`). `o.AddRedirect(from, to, permanent)` adds segment-aware prefix rules for moved content; computed redirects answer 302, rules 301. Middleware registered by `AddShellDocs` serves them before routing (so dotted version roots like `/docs/v0.2.1` work), and `shelldocs build` writes them as redirect pages. Opt out with `o.EnableRedirects = false`.
- **`ShellDocsOptions.RenderPageTitle`.** Renders frontmatter `title` as the page heading and `description` as a lead paragraph when the body doesn't start with its own `# Heading`. Off by default.

### Changed

- **`razor:preview` child content is parsed as Razor, not markdown.** Component bodies inside a fence (and `<ComponentPreview>` bodies) go through the same node parser as the fence itself: elements wrap nested components exactly as written and nothing is wrapped in `<p>`. Inline component tags in prose still take markdown bodies.
- **`ThemeToggle` works without a Blazor runtime.** Clicks are handled by `shelldocs.js` and both icons render, with CSS picking one from `<html class="dark">`.
- **`ShellDocs.Components` references the ASP.NET Core shared framework** (for the redirect middleware) instead of the `Microsoft.AspNetCore.Components.Web` package. It already read content from disk, so it was server-side in practice.

### Fixed

- **Prose styles leaked into previews.** Paragraph margins, list padding, heading sizes and link underlines from `.shelldocs-prose` hit live components and beat Tailwind's layered utilities (gaps inside cards and menus, underlined nav links).
- **Nested markup inside a preview component was mangled.** `<Navbar><div><a/>…<ThemeToggle/></div></Navbar>` closed the `<div>` before the nested component and wrapped loose text in `<p>`.
- **Theme state only followed ShellDocs' own toggle.** A component library flipping `<html class="dark">` left the stored theme and `ThemeState` stale. `shelldocs.js` now watches the class, saves it under `shelldocs-theme` and pushes it into `ThemeState`.
- **`shelldocs build` prerendered "Page not found" pages** when the csproj copied `meta.json` but not the `.md` files (`<Content Update="content/**/*.md">`). Build only mirrored `content/` when it was missing entirely; it now fills in every file publish left out. The example project's csproj is fixed too.
- **`enhancedload` handlers never ran.** Blazor raises the event through `Blazor.addEventListener`, not as a DOM event, so re-applying the theme, re-attaching the TOC and closing the mobile nav after enhanced navigation (static SSR apps) silently did nothing.

## [0.1.9-alpha] — 2026-10-02
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Already have a Blazor project? Run `shelldocs init --attach` inside it: it adds

## What you get

- **Markdown-first authoring.** YAML frontmatter, Shiki-highlighted code fences, live `razor:preview` examples, inline component tags mid-prose.
- **Markdown-first authoring.** YAML frontmatter, Shiki-highlighted code fences, live `razor:preview` examples, inline component tags mid-prose. Set `o.RenderPageTitle = true` to render the frontmatter `title` and `description` as the page header instead of writing `# Title`.
- **File-based navigation.** Drop a `.md` in `content/docs/` and it's a page. Sidebar, breadcrumb, prev/next and TOC come from the folder tree; `meta.json` controls order, dividers, subsections and hidden pages.
- **`Cmd+K` search** across titles, headings and page text, with body snippets. The index is built in memory at startup, so there's no external service; search needs a running Blazor app (it isn't available in the static export).
- **Blazor-native.** Your components render as real Razor components, not iframes.
Expand Down Expand Up @@ -63,14 +63,23 @@ The current version is the one whose `RootUrl` prefixes the path (segment-aware)

It's all server-rendered links plus `shelldocs.js`, so it works on static hosts.

> Routes declared as `/docs/{*Path:nonfile}` don't match a URL whose **last** segment has a dot (`/docs/v0.2.1`). Pages below it (`/docs/v0.2.1/introduction`) are fine, and the selectors never link to a bare version root unless it has an `index.md`. If you add one, drop `:nonfile` from the route.
### Redirects

URLs that aren't pages redirect instead of 404ing: content folders and version roots go to their first page (`/docs/v0.2.1` → `/docs/v0.2.1/introduction`; `/docs` prefers unversioned pages, then the latest version), and unversioned URLs that exist in the latest version go there (`/docs/cli/dev` → `/docs/v0.3/cli/dev`). Add rules for moved content:

```csharp
o.AddRedirect("/docs/v0.3.0", "/docs/v0.3"); // also /docs/v0.3.0/x → /docs/v0.3/x (301)
```

`AddShellDocs` registers middleware that answers these before routing, so a dotted version root works even though `/docs/{*Path:nonfile}` routes can't match it. `shelldocs build` writes the same redirects as static pages. Turn it off with `o.EnableRedirects = false`.

## Component previews

- **Preview | Code toolbar on every example**, with copy and a ⋯ menu: *Open in new tab*, *Report a bug*, *Suggest something*. The issue links go to `https://github.com/{GitHubRepo}/issues/new`, pre-filled with the example and page URL; point them elsewhere with `o.IssueTrackerUrl`.
- **`razor:preview` fences render everything.** Several sibling components, HTML wrappers (`<div class="flex gap-2">…</div>`) and text render in order inside one frame, and the Code tab shows the whole fence.
- **Child content is Razor.** Component bodies inside a fence are parsed like the fence itself, so `<Navbar><div>…<ThemeToggle /></div></Navbar>` keeps its structure and nothing is wrapped in `<p>`.
- **Page typography stays out.** Preview frames are `not-prose`, so prose margins, list padding and link underlines don't reach your components.
- **Generic components work.** Set the type argument as Razor does: `<BarChart TItem="SalesRow" />`, `<DataTable TItem="int" />`. Libraries' generic components are registered under their bare name.
- **Centred or stretched.** Examples are centred; `razor:preview stretch` (or `Layout="stretch"` on `<DemoPreview>` / `<ComponentPreview>`) lets charts, inputs and tables fill the frame.
- **Razor-shaped attribute values work:** `Variant="ButtonVariant.Destructive"`, `@ButtonVariant.Destructive`, `@true`, `@42`, `[Flags]` values as `Bold | Italic`.
- **Attributes a static preview can't evaluate are skipped, not fatal:** `OnClick="HandleClick"`, `@onclick`, `@bind-*`, `@ref`, non-primitive parameter types, unparseable values. Each logs a warning and the component still renders.
Expand Down
23 changes: 18 additions & 5 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ Turns markdown into HTML plus typed component slots. Depends on `ShellDocs.Core`

### `ShellDocs.Components`

The Razor class library everyone references. Depends on `ShellDocs.Core`, `ShellDocs.Markdown`, `ShellDocs.Tokens`, `ShellIcons.Blazor`.
The Razor class library everyone references. Depends on `ShellDocs.Core`, `ShellDocs.Markdown`, `ShellDocs.Tokens`, `ShellIcons.Blazor` and the ASP.NET Core shared framework (it reads content from disk and runs middleware, so it's server-side).

- `AddShellDocs(options)` and `ShellDocsOptions`.
- Layouts: `DocsLayout` (`TopNav` / `Sidebar` variants), `HomeLayout`.
- Chrome: `DocsHeader`, `DocsSidebar`, `DocsSidebarHeader`, `DocsMobileBar`, `PackageSelector`, `VersionSelector`, `DocsBreadcrumb`, `PrevNextNav`, `TableOfContents`, `SearchDialog`, `ThemeToggle`, `DocsFooter`, `BrandLogo`.
- Content primitives (auto-registered for markdown): `Callout`, `Card`, `CardGrid`, `LinkCard`, `Steps`/`Step`, `FileTree`/`FileTreeItem`, `Tabs`/`Tab`, `CodeGroup`/`CodeTab`, `TypeTable`/`TypeRow`, `AutoTypeTable`, `ComponentPreview`, `DemoPreview`.
- Render machinery (`[ShellDocsIgnore]`, not reachable from markdown): `MarkdownContent`, `PreviewFrame`.
- Services: `DocsPageState` (scoped), `DocsVersionResolver` (singleton).
- Services: `DocsPageState` (scoped), `DocsVersionResolver` and `DocsRedirects` (singletons), plus the redirect middleware.
- `wwwroot/shelldocs.js` and `wwwroot/shelldocs-theme.css`.

### `ShellDocs.Tokens`
Expand Down Expand Up @@ -124,6 +124,8 @@ Placeholders plus a slot list keep everything at render time: no generated Razor
- `ComponentSlot` → `<DynamicComponent>` with parameters from `SlotRenderer.BuildParameters`.
- `PreviewSlot` → `<PreviewFrame Id="preview-N">`, where `SlotRenderer.RenderNodes` emits elements and components as real render-tree nodes, so wrappers keep their children under interactive re-renders.

**Generic components** register under their bare name (`TypeRegistry.TagNameOf`) as open definitions. `SlotRenderer.Component` closes one per use with `GenericComponents.Close`, reading attributes named after the type parameters (`TItem="int"`, resolved from C# spellings across loaded assemblies) or falling back to `object`; one it can't close renders a `.shelldocs-render-error` element instead. A non-generic component with the same name keeps the tag.

Component child markup is rendered recursively. Inline tags in prose take markdown bodies (`SlotRenderer.FromMarkup`, dedented first, since Markdig treats 4-space indentation as a code block). Inside a `razor:preview` and in `<ComponentPreview>` the body is Razor: `SlotRenderer.FromRazor` parses it with `PreviewParser` and emits real elements, so wrappers around nested components stay intact. Child tags named after a `RenderFragment` parameter (`<Icon>` → `Alert.Icon`) are routed into that slot.

**Parameter coercion.** Attribute values are strings. `BuildParameters` matches them to `[Parameter]` properties case-insensitively and coerces string, bool, char, numeric primitives and enums, accepting Razor forms: a leading `@`, `@( … )`, `Type.Member` enum values, `A | B` flags, numeric suffixes, `@null`. It never throws. Anything static markup can't set is skipped with a logged warning: directive attributes, delegate/`EventCallback` parameters, unsupported types, unparseable values, unknown attributes on components without a `CaptureUnmatchedValues` catch-all, and child content on components without a plain `RenderFragment ChildContent`.
Expand Down Expand Up @@ -169,6 +171,16 @@ Runtime queries: `ResolveByUrl` is a case-insensitive dictionary lookup on norma

All answers are computed at render time and emitted as plain `<a href>`, so prerendered pages behave identically.

### Redirects

`DocsRedirects` (singleton) maps URLs that aren't pages to a target, computed once from the graph and versions:

- content folders and version roots → their first page, within the folder's version scope (a folder holding version folders → its first unversioned page, else the latest version's);
- unversioned URLs that exist in the latest version → that page;
- `AddRedirect` rules on top: segment-aware prefixes that may chain into the computed redirects. They are the only thing that can redirect an existing page.

`DocsRedirectMiddleware` runs first in the pipeline (added by an `IStartupFilter`, so no `Program.cs` change) and answers 302 for computed redirects or 301 for permanent rules, keeping `PathBase` and the query string. Running before routing is what makes `/docs/v0.2.1` work. It also serves the full map at `/_shelldocs/redirects.json` for `shelldocs build`. `EnableRedirects = false` leaves it out.

---

## Search
Expand Down Expand Up @@ -223,11 +235,12 @@ One neutral, shadcn-shaped palette of CSS variables in `ShellDocs.Tokens/tokens.

`shelldocs build`:

1. `dotnet publish -c Release` into `obj/shelldocs-publish` (mirroring `content/` into it if the csproj didn't copy it).
1. `dotnet publish -c Release` into `obj/shelldocs-publish`, then copies in any `content/` file publish left out (a csproj that copies `meta.json` but not `.md`).
2. Builds the navigation graph and collects every URL (visible and hidden) plus `/`.
3. `PrerenderRunner` starts the published app on a free port, requests each URL and writes `<output>/<path>/index.html`.
4. Merges the published `wwwroot/` into the output without overwriting prerendered HTML.
5. Optionally rewrites `<base href>` in every HTML file, copies `index.html` to `404.html`, and with `--site-url` writes `sitemap.xml`, `robots.txt` and `og:*` meta.
4. Reads the app's redirect map and writes a redirect page for each source URL without a prerendered page (meta refresh plus `location.replace`, relative to `<base href>`).
5. Merges the published `wwwroot/` into the output without overwriting prerendered HTML.
6. Optionally rewrites `<base href>` in every HTML file, copies `index.html` to `404.html`, and with `--site-url` writes `sitemap.xml`, `robots.txt` and `og:*` meta.

---

Expand Down
3 changes: 2 additions & 1 deletion examples/ShellDocs.Preview/ShellDocs.Preview.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
</ItemGroup>

<ItemGroup>
<Content Update="content/**/*.md;content/**/meta.json" CopyToOutputDirectory="PreserveNewest" />
<Content Include="content/**/*.md" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
<Content Update="content/**/meta.json" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
<!-- <DemoPreview> reads these at runtime. The Razor SDK drops .razor files from
publish even with Content metadata, so ship them as None items. -->
<None Include="Demos/**/*.razor" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
Expand Down
13 changes: 13 additions & 0 deletions examples/ShellDocs.Preview/Ui/ValueBadge.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@typeparam TValue

@* Stand-in generic library component for exercising razor:preview type arguments. *@
<span class="value-badge">
<span class="value-badge-label">@Label</span>
<span class="value-badge-value">@Value</span>
<span class="value-badge-type">@typeof(TValue).Name</span>
</span>

@code {
[Parameter] public string? Label { get; set; }
[Parameter] public TValue? Value { get; set; }
}
20 changes: 20 additions & 0 deletions examples/ShellDocs.Preview/Ui/ValueBadge.razor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.value-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.65rem;
border: 1px solid var(--border);
border-radius: 999px;
font-size: 0.8125rem;
background: var(--card);
}
.value-badge-label { color: var(--muted-foreground); }
.value-badge-value { font-weight: 600; }
.value-badge-type {
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--muted-foreground);
padding: 0.05rem 0.4rem;
border-radius: 999px;
background: var(--muted);
}
4 changes: 3 additions & 1 deletion examples/ShellDocs.Preview/content/docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,6 @@ shelldocs build
- `--spa-fallback` copies `index.html` to `404.html`.
- `--site-url https://docs.example.com` emits `sitemap.xml`, `robots.txt` and `og:` meta tags.

In the static output, navigation, sidebar sections, the mobile menu, the version and package selectors, tabs, preview tabs and menus, the table of contents, the theme toggle and code copy all work through `shelldocs.js`. Search, the desktop sidebar-collapse button and stateful demo components still need a running Blazor app (Server or WebAssembly).
In the static output, navigation, sidebar sections, the mobile menu, the version and package selectors, tabs, preview tabs and menus, the table of contents, the theme toggle and code copy all work through `shelldocs.js`. Search, the desktop sidebar-collapse button and stateful demo components still need a running Blazor Server app (interactive server rendering).

URLs that aren't pages (folders, version roots, unversioned URLs that moved into a version) become redirect pages in the static output, the same redirects the running app answers.
16 changes: 16 additions & 0 deletions examples/ShellDocs.Preview/content/docs/markdown-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ order: 10
---
```

With `o.RenderPageTitle = true`, ShellDocs renders `title` as the page's heading and `description` as a lead paragraph, so pages don't need a `# Title` line. A page whose body starts with its own `# Heading` keeps it.

## Standard markdown works

Headings, lists, tables, code fences, images, links — all standard:
Expand Down Expand Up @@ -72,3 +74,17 @@ Examples are centred. Add `stretch` to the info string (`razor:preview stretch`)
```

Preview frames are marked `not-prose`, so the page's typography (paragraph margins, list padding, link underlines) never reaches the components inside. Add the class to any other element that should opt out.

### Generic components

Generic components (`@typeparam TValue`) register under their bare name. Set the type argument the way Razor does, as an attribute named after the type parameter. Attribute values are then coerced to the closed type:

```razor:preview
<div style="display: flex; gap: 0.75rem; flex-wrap: wrap;">
<ValueBadge TValue="int" Label="Downloads" Value="1200" />
<ValueBadge TValue="decimal" Label="Price" Value="9.99" />
<ValueBadge TValue="string" Label="Status" Value="stable" />
</div>
```

Type arguments accept C# spellings (`int`, `int?`, `List<string>`, `MyApp.Models.Product`). Without one, ShellDocs uses `object` when the constraints allow it and logs a warning; a type it can't resolve shows an inline error. `<ComponentPreview Component="ValueBadge" TValue="int" … />` works the same way.
Loading
Loading