```
### Keyboard shortcut
Add Cmd+K / Ctrl+K to focus search:
```html
```
### Search prerequisites
Search requires per-collection enablement:
1. In admin: Edit Content Type -> check "Search" in Features
2. Mark fields as `"searchable": true` in the seed file
3. Only searchable fields of searchable collections are indexed
## SEO Meta
> [!IMPORTANT]
> On server-rendered content pages that fetch the entry with `getEmDashEntry()`
> and render ``, EmDash automatically applies the SEO panel's
> description, image, canonical URL, and noindex setting. The panel title
> supplies the social and JSON-LD title contributions. The panel data uses the
> entry query the page already runs, so it adds no additional query.
>
> `` cannot set the document ``, so use `getSeoMeta()` for
> the title. Prerendered pages, pages without ``, hand-rolled query
> paths, and multi-entry collection results do not receive this overlay. Use
> `getSeoMeta()` or the raw SEO data on those paths.
Use `getSeoMeta()` when the template needs to set `` or the page does not
receive the automatic overlay:
```typescript
import { getSeoMeta } from "emdash";
const seo = getSeoMeta(post, {
siteTitle: "My Blog",
siteUrl: Astro.url.origin,
path: `/posts/${slug}`,
defaultOgImage: featuredImageUrl, // Optional fallback
});
```
Use in your layout's ``:
```astro
{seo.title}
{seo.description && }
{seo.canonical && }
{seo.ogImage && }
{seo.robots && }
```
### Custom title and description defaults
Pass computed fallbacks through `defaultTitle` and `defaultDescription`. Values
set in the SEO panel take precedence over these defaults:
```ts
import { getSeoMeta } from "emdash";
const seo = getSeoMeta(post, {
defaultTitle: `${post.data.title}: A practical guide`,
defaultDescription: `Read ${post.data.title} on My Blog.`,
});
```
## Comments
Built-in comments system:
```astro
---
import { Comments, CommentForm } from "emdash/ui/comments";
---
```
Comments are enabled per-collection in the seed: `"commentsEnabled": true`.
## Page Contributions (Plugin Head/Body Injection)
Plugins can inject content into the `` and `` of pages. To support this, use the page contribution components:
```astro
---
import { EmDashHead, EmDashBodyStart, EmDashBodyEnd } from "emdash/ui";
import { createPublicPageContext } from "emdash/page";
const pageCtx = createPublicPageContext({
Astro,
kind: content ? "content" : "custom",
pageType: "article",
title: fullTitle,
pageTitle: post.data.title,
description,
canonical,
image,
content: { collection: "posts", id: post.data.id, slug },
});
---
```
This enables plugins (analytics, tracking pixels, structured data, etc.) to contribute to any page.
## Bylines
Bylines are author profiles, independent of user accounts. They support guest authors and multi-author attribution with role labels.
### Eagerly loaded on entries
Bylines are automatically attached to every entry by the query layer:
```astro
{/* Primary author */}
{post.data.byline && (
{post.data.byline.displayName}
)}
{/* All credits (includes roleLabel for co-authors, guest essays, etc.) */}
{post.data.bylines?.map(credit => (
{credit.byline.displayName}
{credit.roleLabel && ({credit.roleLabel})}
))}
```
- `entry.data.byline` -- primary `BylineSummary` or `null`
- `entry.data.bylines` -- array of `ContentBylineCredit` (each has `.byline`, `.roleLabel`, `.source`)
### Standalone query functions
```typescript
import { getByline, getBylineBySlug } from "emdash";
// Look up a specific byline
const byline = await getBylineBySlug("jane-doe");
```
### BylineSummary shape
```typescript
interface BylineSummary {
id: string;
slug: string;
displayName: string;
bio: string | null;
avatarMediaId: string | null;
websiteUrl: string | null;
isGuest: boolean;
}
```
### ContentBylineCredit shape
```typescript
interface ContentBylineCredit {
byline: BylineSummary;
sortOrder: number;
roleLabel: string | null; // e.g., "Guest essay", "Photographer"
source?: "explicit" | "inferred"; // "inferred" = fallback from author_id
}
```
## Dark Mode Pattern
Cookie-based theme switching (no flash on load):
```html
```
Then use CSS variables that change based on `.dark` class:
```css
:root {
--color-bg: #ffffff;
--color-text: #1a1a1a;
}
:root.dark {
--color-bg: #0d0d0d;
--color-text: #ededed;
}
```
## Layout Pattern
A typical base layout:
```astro
---
import { getMenu, getEmDashCollection } from "emdash";
import { WidgetArea, EmDashHead, EmDashBodyStart, EmDashBodyEnd } from "emdash/ui";
import { createPublicPageContext } from "emdash/page";
import LiveSearch from "emdash/ui/search";
interface Props {
title: string;
description?: string | null;
image?: string | null;
content?: { collection: string; id: string; slug?: string | null };
}
const { title, pageTitle, description, image, content } = Astro.props;
const menu = await getMenu("primary");
const pageCtx = createPublicPageContext({
Astro,
kind: content ? "content" : "custom",
pageType: "website",
title,
pageTitle: pageTitle ?? title,
description,
image,
content,
});
---
{title}
{description && }
```