496 lines
10 KiB
Markdown
496 lines
10 KiB
Markdown
# EmDash API Reference
|
|
|
|
Quick reference for EmDash-specific APIs used when porting themes.
|
|
|
|
> **See also:** The `scaffold/` directory contains working examples of all these patterns. When in doubt, copy from there.
|
|
|
|
## Content Retrieval
|
|
|
|
EmDash's query functions follow Astro's [live content collections](https://docs.astro.build/en/reference/experimental-flags/live-content-collections/) pattern, returning structured results for graceful error handling.
|
|
|
|
### getEmDashCollection
|
|
|
|
Fetch multiple entries from a collection.
|
|
|
|
```typescript
|
|
import { getEmDashCollection } from "emdash";
|
|
|
|
// Returns { entries, error }
|
|
const { entries: posts } = await getEmDashCollection("posts");
|
|
|
|
// With filters
|
|
const { entries: posts } = await getEmDashCollection("posts", {
|
|
status: "published",
|
|
limit: 10,
|
|
where: { category: "news" },
|
|
});
|
|
```
|
|
|
|
### getEmDashEntry
|
|
|
|
Fetch a single entry by slug.
|
|
|
|
```typescript
|
|
import { getEmDashEntry } from "emdash";
|
|
|
|
// Returns { entry, error, isPreview }
|
|
const { entry: post } = await getEmDashEntry("posts", "hello-world");
|
|
|
|
if (!post) {
|
|
return Astro.redirect("/404");
|
|
}
|
|
```
|
|
|
|
### Entry Shape
|
|
|
|
```typescript
|
|
interface Entry {
|
|
id: string;
|
|
collection: string;
|
|
data: {
|
|
title: string;
|
|
slug: string;
|
|
content: PortableTextBlock[];
|
|
featured_image?: ImageField; // { src, alt } - NOT a string!
|
|
// ... custom fields
|
|
};
|
|
}
|
|
```
|
|
|
|
## Field Types at Runtime
|
|
|
|
**IMPORTANT:** Field types have specific runtime shapes. The most common mistake is treating image fields as strings.
|
|
|
|
### Image Fields
|
|
|
|
Image fields are **objects**, not strings:
|
|
|
|
```typescript
|
|
interface ImageField {
|
|
src: string; // The resolved URL
|
|
alt?: string;
|
|
}
|
|
```
|
|
|
|
```astro
|
|
{/* CORRECT */}
|
|
{post.data.featured_image?.src && (
|
|
<img
|
|
src={post.data.featured_image.src}
|
|
alt={post.data.featured_image.alt || post.data.title}
|
|
/>
|
|
)}
|
|
|
|
{/* WRONG - renders [object Object] */}
|
|
<img src={post.data.featured_image} />
|
|
```
|
|
|
|
### Reference Fields
|
|
|
|
In seed files use `"$ref:id"` format. At runtime they may be resolved objects or strings.
|
|
|
|
### PortableText Fields
|
|
|
|
Rich content is an array of blocks with `_type` property.
|
|
|
|
## Site Settings
|
|
|
|
### getSiteSettings
|
|
|
|
Get all site settings.
|
|
|
|
```typescript
|
|
import { getSiteSettings } from "emdash";
|
|
|
|
const settings = await getSiteSettings();
|
|
console.log(settings.title); // "My Site"
|
|
console.log(settings.logo?.url); // Resolved media URL
|
|
```
|
|
|
|
### getSiteSetting
|
|
|
|
Get a single setting.
|
|
|
|
```typescript
|
|
import { getSiteSetting } from "emdash";
|
|
|
|
const title = await getSiteSetting("title");
|
|
const logo = await getSiteSetting("logo");
|
|
```
|
|
|
|
### Available Settings
|
|
|
|
| Key | Type | Description |
|
|
| ------------ | ---------------- | ------------------------ |
|
|
| `title` | `string` | Site name |
|
|
| `tagline` | `string` | Site tagline/description |
|
|
| `logo` | `MediaReference` | Site logo with URL |
|
|
| `favicon` | `MediaReference` | Favicon with URL |
|
|
| `social` | `SocialLinks` | Social media URLs |
|
|
| `timezone` | `string` | Site timezone |
|
|
| `dateFormat` | `string` | Date display format |
|
|
|
|
## Navigation Menus
|
|
|
|
### getMenu
|
|
|
|
Fetch a menu by name with resolved URLs.
|
|
|
|
```typescript
|
|
import { getMenu } from "emdash";
|
|
|
|
const menu = await getMenu("primary");
|
|
|
|
if (menu) {
|
|
console.log(menu.items); // MenuItem[]
|
|
}
|
|
```
|
|
|
|
### getMenus
|
|
|
|
Get all menus (names only).
|
|
|
|
```typescript
|
|
import { getMenus } from "emdash";
|
|
|
|
const menus = await getMenus();
|
|
// [{ id, name, label }]
|
|
```
|
|
|
|
### MenuItem Shape
|
|
|
|
```typescript
|
|
interface MenuItem {
|
|
id: string;
|
|
label: string;
|
|
url: string; // Resolved URL
|
|
target?: string;
|
|
children: MenuItem[];
|
|
}
|
|
```
|
|
|
|
### Rendering Menus
|
|
|
|
```astro
|
|
---
|
|
import { getMenu } from "emdash";
|
|
|
|
const primaryMenu = await getMenu("primary");
|
|
---
|
|
<nav>
|
|
{primaryMenu?.items.map(item => (
|
|
<a href={item.url}>{item.label}</a>
|
|
))}
|
|
</nav>
|
|
```
|
|
|
|
## Taxonomies
|
|
|
|
### getTaxonomyTerms
|
|
|
|
Get all terms for a taxonomy.
|
|
|
|
```typescript
|
|
import { getTaxonomyTerms } from "emdash";
|
|
|
|
const categories = await getTaxonomyTerms("categories");
|
|
const tags = await getTaxonomyTerms("tags");
|
|
```
|
|
|
|
### getTerm
|
|
|
|
Get a single term by slug.
|
|
|
|
```typescript
|
|
import { getTerm } from "emdash";
|
|
|
|
const term = await getTerm("categories", "news");
|
|
console.log(term?.label); // "News"
|
|
console.log(term?.count); // Number of entries
|
|
```
|
|
|
|
### getEntryTerms
|
|
|
|
Get terms assigned to a specific entry.
|
|
|
|
> **IMPORTANT:** This function does NOT take a `db` parameter.
|
|
|
|
```typescript
|
|
import { getEntryTerms } from "emdash";
|
|
|
|
// Get all terms for an entry
|
|
const terms = await getEntryTerms("posts", post.id);
|
|
|
|
// Get only categories
|
|
const categories = await getEntryTerms("posts", post.id, "categories");
|
|
```
|
|
|
|
### getEntriesByTerm
|
|
|
|
Get entries that have a specific term.
|
|
|
|
```typescript
|
|
import { getEntriesByTerm } from "emdash";
|
|
|
|
const posts = await getEntriesByTerm("posts", "categories", "news");
|
|
```
|
|
|
|
### TaxonomyTerm Shape
|
|
|
|
```typescript
|
|
interface TaxonomyTerm {
|
|
id: string;
|
|
name: string; // Taxonomy name
|
|
slug: string; // Term slug
|
|
label: string; // Display label
|
|
children: TaxonomyTerm[];
|
|
count?: number;
|
|
}
|
|
```
|
|
|
|
## Widget Areas
|
|
|
|
### getWidgetArea
|
|
|
|
Get a widget area by name.
|
|
|
|
```typescript
|
|
import { getWidgetArea } from "emdash";
|
|
|
|
const sidebar = await getWidgetArea("sidebar");
|
|
|
|
if (sidebar) {
|
|
console.log(sidebar.widgets); // Widget[]
|
|
}
|
|
```
|
|
|
|
### Widget Types
|
|
|
|
| Type | Description | Key Fields |
|
|
| ----------- | -------------------- | ---------------------- |
|
|
| `content` | Rich text (PT) | `content` |
|
|
| `menu` | Navigation menu | `menuName` |
|
|
| `component` | Registered component | `componentId`, `props` |
|
|
|
|
## Sections (Reusable Blocks)
|
|
|
|
Sections are reusable content blocks that editors can insert via `/section` slash command.
|
|
|
|
### getSection
|
|
|
|
Get a single section by slug.
|
|
|
|
```typescript
|
|
import { getSection } from "emdash";
|
|
|
|
const cta = await getSection("newsletter-cta");
|
|
// Returns { id, slug, title, content, keywords, source }
|
|
```
|
|
|
|
### getSections
|
|
|
|
List sections with optional filters.
|
|
|
|
```typescript
|
|
import { getSections } from "emdash";
|
|
|
|
// Get all sections
|
|
const all = await getSections();
|
|
|
|
// Filter by source: "theme" | "user" | "import"
|
|
const imported = await getSections({ source: "import" });
|
|
```
|
|
|
|
### Section Sources
|
|
|
|
| Source | Description |
|
|
| -------- | --------------------------------------- |
|
|
| `theme` | Defined in seed file |
|
|
| `user` | Created by editors in admin |
|
|
| `import` | Imported from WordPress reusable blocks |
|
|
|
|
## Search
|
|
|
|
### search
|
|
|
|
Global search across collections.
|
|
|
|
```typescript
|
|
import { search } from "emdash";
|
|
|
|
const results = await search("hello world", {
|
|
collections: ["posts", "pages"], // Optional: limit to specific collections
|
|
status: "published", // Optional: filter by status
|
|
limit: 20, // Optional: max results
|
|
});
|
|
|
|
// Returns { results: SearchResult[], total, nextCursor? }
|
|
results.results.forEach((r) => {
|
|
console.log(r.collection); // "posts"
|
|
console.log(r.id); // Entry ID
|
|
console.log(r.title); // Entry title
|
|
console.log(r.slug); // Entry slug
|
|
console.log(r.snippet); // HTML snippet with <mark> highlights
|
|
console.log(r.score); // Relevance score
|
|
});
|
|
```
|
|
|
|
### LiveSearch Component
|
|
|
|
Ready-to-use search with instant results:
|
|
|
|
```astro
|
|
---
|
|
import LiveSearch from "emdash/ui/search";
|
|
---
|
|
|
|
<LiveSearch
|
|
placeholder="Search..."
|
|
collections={["posts", "pages"]}
|
|
/>
|
|
```
|
|
|
|
Features:
|
|
|
|
- Debounced instant search
|
|
- Prefix matching (automatic `*` suffix)
|
|
- Porter stemming ("run" finds "running")
|
|
- Result snippets with `<mark>` highlights
|
|
|
|
### Search Configuration
|
|
|
|
Search is enabled per-collection via admin UI:
|
|
|
|
1. Edit Content Type → check "Search" in Features
|
|
2. Edit fields → check "Searchable" for text fields
|
|
|
|
Only collections with search enabled are indexed.
|
|
|
|
## Rendering Content
|
|
|
|
### PortableText Component
|
|
|
|
```astro
|
|
---
|
|
import { PortableText } from "emdash/ui";
|
|
---
|
|
|
|
<PortableText value={post.data.content} />
|
|
```
|
|
|
|
## CLI Commands
|
|
|
|
### Apply Seed
|
|
|
|
The seed is inlined into the build and applied automatically on the first request when the database is empty and the setup wizard hasn't been completed. Validation runs at apply time — errors surface in the dev server logs. To re-seed during iteration, delete `data.db` and restart the dev server.
|
|
|
|
Common validation errors caught:
|
|
|
|
- Image fields with raw URLs (should use `$media`)
|
|
- Reference fields with raw IDs (should use `$ref:id`)
|
|
- PortableText not an array or missing `_type`
|
|
- Type mismatches (string vs number, etc.)
|
|
|
|
### Export Seed
|
|
|
|
```bash
|
|
# Export schema only
|
|
emdash export-seed
|
|
|
|
# Export schema and all content
|
|
emdash export-seed --with-content
|
|
|
|
# Export specific collections
|
|
emdash export-seed --with-content=posts,pages
|
|
```
|
|
|
|
## Configuration
|
|
|
|
### astro.config.mjs
|
|
|
|
```javascript
|
|
import { defineConfig } from "astro/config";
|
|
import emdash, { local } from "emdash/astro";
|
|
import { sqlite } from "emdash/db";
|
|
|
|
export default defineConfig({
|
|
integrations: [
|
|
emdash({
|
|
database: sqlite({ url: "file:./data.db" }),
|
|
storage: local({
|
|
directory: "./uploads",
|
|
baseUrl: "/_emdash/api/media/file",
|
|
}),
|
|
}),
|
|
],
|
|
});
|
|
```
|
|
|
|
### live.config.ts
|
|
|
|
```typescript
|
|
// src/live.config.ts
|
|
import { defineLiveCollection } from "astro:content";
|
|
import { emdashLoader } from "emdash/runtime";
|
|
|
|
export const collections = {
|
|
_emdash: defineLiveCollection({ loader: emdashLoader() }),
|
|
};
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### Homepage with Recent Posts
|
|
|
|
```astro
|
|
---
|
|
import { getEmDashCollection, getSiteSettings } from "emdash";
|
|
import Base from "../layouts/Base.astro";
|
|
|
|
const settings = await getSiteSettings();
|
|
const { entries: posts } = await getEmDashCollection("posts", { limit: 10 });
|
|
---
|
|
<Base title={settings.title}>
|
|
{posts.map(post => (
|
|
<article>
|
|
<a href={`/posts/${post.data.slug}`}>{post.data.title}</a>
|
|
</article>
|
|
))}
|
|
</Base>
|
|
```
|
|
|
|
### Category Archive
|
|
|
|
```astro
|
|
---
|
|
import { getTerm, getEntriesByTerm } from "emdash";
|
|
|
|
const { slug } = Astro.params;
|
|
const category = await getTerm("categories", slug);
|
|
const posts = await getEntriesByTerm("posts", "categories", slug);
|
|
---
|
|
<h1>{category?.label}</h1>
|
|
{posts.map(post => (
|
|
<a href={`/posts/${post.data.slug}`}>{post.data.title}</a>
|
|
))}
|
|
```
|
|
|
|
### Dynamic Navigation
|
|
|
|
```astro
|
|
---
|
|
import { getMenu, getSiteSettings } from "emdash";
|
|
|
|
const settings = await getSiteSettings();
|
|
const primaryMenu = await getMenu("primary");
|
|
---
|
|
<header>
|
|
<a href="/">{settings.title}</a>
|
|
<nav>
|
|
{primaryMenu?.items.map(item => (
|
|
<a href={item.url}>{item.label}</a>
|
|
))}
|
|
</nav>
|
|
</header>
|
|
```
|