Add the search modal to a non-DocSite site
Light up the Pennington.UI search modal on a bare AddPennington host: reference the UI library, serve its scripts, and add a trigger element.
AddDocSite ships a search modal wired up for you. On a bare AddPennington host you wire it yourself — but the index and the modal already exist, so the work is three pieces of markup, not a search UI. AddPennington emits the index at /search/{locale}/index.json; Pennington.UI carries the modal in scripts.js; and Pennington.MonorailCss already safelists the modal's styles. This guide connects them. For how that index is built and queried, see How the search index is built and queried.
Before you begin
- A bare
AddPenningtonhost styled with MonorailCSS — see Style the site with MonorailCSS - The host already serves
/search/{locale}/index.json(it does, on everyAddPenningtonhost). To shape what that index contains, see Tune what the search box returns
The BareHostSearchExample mounts the shared Bramble corpus and lights up the modal with the wiring below.
Steps
Reference Pennington.UI and Pennington.MonorailCss.
Pennington.UI serves scripts.js (the modal) and, transitively, the DeweySearch.Web browser client as static web assets under /_content/. Pennington.MonorailCss carries the modal's styles.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>BareHostSearchExample</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Pennington\Pennington.csproj" />
<ProjectReference Include="..\..\src\Pennington.MonorailCss\Pennington.MonorailCss.csproj" />
<!-- Brings scripts.js (the search modal) and, transitively, the DeweySearch.Web
browser client as static web assets under /_content. -->
<ProjectReference Include="..\..\src\Pennington.UI\Pennington.UI.csproj" />
</ItemGroup>
<ItemGroup>
<!-- No local Content/ — this example mounts the shared Bramble corpus.
Watch that folder so dev-time live reload sees edits to it. -->
<Watch Include="..\_shared\Bramble\Content\**\*.*" />
<Watch Include="Components\**\*.razor" />
</ItemGroup>
</Project>
Serve the /_content/ static assets.
UsePennington mounts your content folders, not the RCL assets. Call app.MapStaticAssets() so /_content/Pennington.UI/scripts.js and /_content/DeweySearch.Web/dewey-search.js are served — scripts.js fetches the latter on demand when search first opens.
using BareHostSearchExample.Components;
using Pennington.FrontMatter;
using Pennington.Infrastructure;
using Pennington.MonorailCss;
var builder = WebApplication.CreateBuilder(args);
// A bare AddPennington host — no DocSite. AddPennington already emits the search
// index at /search/{locale}/index.json (term shards + per-page fragments); the
// only thing this example adds is the Pennington.UI search modal on top of it,
// wired up in MainLayout.razor. Content is the shared Bramble corpus mounted at
// the root, with the blog subtree excluded because its date/author front matter
// is not part of DocFrontMatter.
builder.Services.AddPennington(penn =>
{
penn.SiteTitle = "Bramble";
penn.ContentRootPath = "../_shared/Bramble/Content";
penn.AddMarkdownContent<DocFrontMatter>(md =>
{
md.ContentPath = "../_shared/Bramble/Content";
md.BasePageUrl = "/";
md.ExcludePaths = ["blog"];
});
});
builder.Services.AddMonorailCss(_ => new MonorailCssOptions
{
ColorScheme = new NamedColorScheme
{
PrimaryColorName = ColorName.Emerald,
AccentColorName = ColorName.Amber,
BaseColorName = ColorName.Slate,
},
});
builder.Services.AddRazorComponents();
var app = builder.Build();
app.UsePennington();
app.UseMonorailCss();
app.UseAntiforgery();
// Serve the static web assets Pennington.UI and DeweySearch.Web ship under /_content
// (scripts.js, dewey-search.js). UsePennington only mounts the content folders.
app.MapStaticAssets();
app.MapRazorComponents<App>();
await app.RunOrBuildAsync(args);
Load the script and add the trigger.
In your layout, load scripts.js (defer), set data-default-locale on <body>, and add a trigger element with id="search-input". scripts.js self-initializes on load: it binds the click and the Ctrl/Cmd-K shortcut to that element, reads the locale attribute to locate the index, and pulls in dewey-search.js on demand the first time the modal opens — so you don't reference that script yourself.
@* The search modal ships in Pennington.UI/scripts.js and styles itself from the
@apply blocks Pennington.MonorailCss safelists, so lighting it up on a bare
(non-DocSite) host is three pieces of markup:
1. scripts.js in <head> — it pulls in DeweySearch.Web's client on demand the
first time search opens, so there's no separate dewey-search.js tag,
2. data-default-locale on <body> — the client reads it to find the index, and
3. a trigger element with id="search-input".
scripts.js self-initializes on load and binds the click + Ctrl/Cmd-K shortcut. *@
@inherits LayoutComponentBase
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/styles.css" />
<script src="/_content/Pennington.UI/scripts.js" defer></script>
<HeadOutlet />
</head>
<body class="bg-base-50 text-base-900 min-h-screen" data-default-locale="en">
<div class="max-w-3xl mx-auto px-6 py-10">
<header class="mb-8 flex items-center gap-4 border-b border-base-200 pb-4">
<a class="text-lg font-bold text-primary-700" href="/">Bramble</a>
<button type="button"
id="search-input"
class="ml-auto flex h-9 w-full max-w-xs items-center gap-2 rounded-lg border border-base-200 bg-base-100 px-3 text-sm text-base-500 transition-colors hover:border-base-300 hover:bg-base-200">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" class="h-4 w-4 shrink-0 stroke-current" stroke-width="2">
<circle cx="11" cy="11" r="7" />
<path stroke-linecap="round" d="m21 21-4.3-4.3" />
</svg>
<span class="flex-1 text-left">Search</span>
<kbd class="ml-auto inline-flex items-center gap-0.5 rounded border border-base-300 px-1.5 py-0.5 font-mono text-[11px] text-base-500">
<span>Ctrl</span><span>K</span>
</kbd>
</button>
</header>
<article class="prose">
@Body
</article>
<footer class="mt-12 border-t border-base-200 pt-4 text-xs text-base-500">
Bare Pennington host — the search modal comes from Pennington.UI.
</footer>
</div>
</body>
</html>
A single-locale site deployed at the domain root needs only data-default-locale. Two more <body> attributes cover the other cases:
data-default-locale— the default locale code (for exampleen). The client falls back to this when no per-locale prefix matches, so it must always be present.data-locales— a comma-separated list of every locale code, in any order (for exampleen,fr,de). The client matches the first URL path segment against this list to pick which/search/{locale}/tree to query. Leave it empty or omit it on a single-locale site.data-base-url— the deploy sub-path prefix, with a leading slash and no trailing slash (for example/docs). The client prepends it when it fetchesdewey-search.js, because a runtime-injected<script>does not pass through the base-url rewriter that fixes server-rendered links. Omit it for a domain-root deployment.
A multi-locale site deployed under /docs sets all three:
<body data-default-locale="en" data-locales="en,fr,de" data-base-url="/docs">
Note
No search CSS to write. The modal builds its DOM with class names (.search-modal, .search-result, …) that live only in scripts.js, so the MonorailCSS source scan never sees them. AddMonorailCss ships their styles anyway, so the modal is styled the moment it appears. A host that brings its own (non-MonorailCSS) stylesheet defines those class names there instead.
Verify
- Run the host and fetch
/_content/Pennington.UI/scripts.js— it returns JavaScript (text/javascript), not the not-found page. If it returns HTML,MapStaticAssetsis missing or the request is reaching the catch-all route - Press Ctrl+K (or click the trigger). The modal opens styled — a centered dialog over a dimmed backdrop
- Type a query. Results deep-link to headings (
/page/#heading) with a page breadcrumb, and the area chips filter by content area
Related
- How-to: Tune what the search box returns — configure the same index (exclude pages, weight priority, scope the indexed HTML)
- Background: How the search index is built and queried
- Background: What the DocSite and BlogSite templates wire for you
- Reference:
SearchIndexOptions