小部件区域是模板中的命名区域,管理员可以在其中放置内容块。将它们用于侧边栏、页脚列、促销横幅或编辑人员需要在不接触代码的情况下控制的任何部分。
查询小部件区域
使用 getWidgetArea() 按名称获取小部件区域:
---
import { getWidgetArea } from "emdash";
const sidebar = await getWidgetArea("sidebar");
---
{sidebar && sidebar.widgets.length > 0 && (
<aside class="sidebar">
{sidebar.widgets.map(widget => (
<div class="widget">
{widget.title && <h3>{widget.title}</h3>}
<!-- 渲染小部件内容 -->
</div>
))}
</aside>
)}
如果小部件区域不存在,函数返回 null。
小部件区域结构
小部件区域包含元数据和一个小部件数组:
interface WidgetArea {
id: string;
name: string; // 唯一标识符 ("sidebar", "footer-1")
label: string; // 显示名称 ("主侧边栏")
description?: string;
widgets: Widget[];
}
interface Widget {
id: string;
type: "content" | "menu" | "component";
title?: string;
// 类型特定字段
content?: PortableTextBlock[]; // 用于内容小部件
menuName?: string; // 用于菜单小部件
componentId?: string; // 用于组件小部件
componentProps?: Record<string, unknown>;
}
小部件类型
EmDash 支持三种小部件类型:
内容小部件
以 Portable Text 格式存储的富文本内容。使用 PortableText 组件渲染:
---
import { PortableText } from "emdash/ui";
---
{widget.type === "content" && widget.content && (
<div class="widget-content">
<PortableText value={widget.content} />
</div>
)}
菜单小部件
在小部件区域内显示导航菜单:
---
import { getMenu } from "emdash";
const menu = widget.menuName ? await getMenu(widget.menuName) : null;
---
{widget.type === "menu" && menu && (
<nav class="widget-nav">
<ul>
{menu.items.map(item => (
<li><a href={item.url}>{item.label}</a></li>
))}
</ul>
</nav>
)}
组件小部件
渲染具有可配置 props 的注册组件。EmDash 包含以下核心组件:
| 组件 ID | 描述 | Props |
|---|---|---|
core:recent-posts | 最新文章列表 | count, showThumbnails, showDate |
core:categories | 分类列表 | showCount, hierarchical |
core:tags | 标签云 | showCount, limit |
core:search | 搜索表单 | placeholder |
core:archives | 月度/年度归档 | type, limit |
渲染小部件
创建一个可重用的小部件渲染器组件:
---
import { PortableText } from "emdash/ui";
import { getMenu } from "emdash";
import type { Widget } from "emdash";
// 导入小部件组件
import RecentPosts from "./widgets/RecentPosts.astro";
import Categories from "./widgets/Categories.astro";
import TagCloud from "./widgets/TagCloud.astro";
import SearchForm from "./widgets/SearchForm.astro";
import Archives from "./widgets/Archives.astro";
interface Props {
widget: Widget;
}
const { widget } = Astro.props;
const componentMap: Record<string, any> = {
"core:recent-posts": RecentPosts,
"core:categories": Categories,
"core:tags": TagCloud,
"core:search": SearchForm,
"core:archives": Archives,
};
const menu = widget.type === "menu" && widget.menuName
? await getMenu(widget.menuName)
: null;
---
<div class="widget">
{widget.title && <h3 class="widget-title">{widget.title}</h3>}
{widget.type === "content" && widget.content && (
<div class="widget-content">
<PortableText value={widget.content} />
</div>
)}
{widget.type === "menu" && menu && (
<nav class="widget-menu">
<ul>
{menu.items.map(item => (
<li><a href={item.url}>{item.label}</a></li>
))}
</ul>
</nav>
)}
{widget.type === "component" && widget.componentId && componentMap[widget.componentId] && (
<Fragment>
{(() => {
const Component = componentMap[widget.componentId!];
return <Component {...widget.componentProps} />;
})()}
</Fragment>
)}
</div>
小部件组件示例
最新文章小部件
以下组件渲染最新文章,支持可选的缩略图和日期:
---
import { getEmDashCollection } from "emdash";
import { Image } from "emdash/ui";
interface Props {
count?: number;
showThumbnails?: boolean;
showDate?: boolean;
}
const { count = 5, showThumbnails = false, showDate = true } = Astro.props;
const { entries: posts } = await getEmDashCollection("posts", {
limit: count,
orderBy: { publishedAt: "desc" },
});
---
<ul class="recent-posts">
{posts.map(post => (
<li>
{showThumbnails && post.data.featured_image && (
<Image image={post.data.featured_image} alt="" class="thumbnail" />
)}
<a href={`/posts/${post.data.slug}`}>{post.data.title}</a>
{showDate && post.data.publishedAt && (
<time datetime={post.data.publishedAt.toISOString()}>
{post.data.publishedAt.toLocaleDateString()}
</time>
)}
</li>
))}
</ul>
搜索小部件
以下组件渲染一个提交到搜索页面的搜索表单:
---
interface Props {
placeholder?: string;
}
const { placeholder = "搜索..." } = Astro.props;
---
<form action="/search" method="get" class="search-form">
<input
type="search"
name="q"
placeholder={placeholder}
aria-label="搜索"
/>
<button type="submit">搜索</button>
</form>
在布局中使用小部件区域
以下示例展示了带有侧边栏小部件区域的博客布局:
---
import { getWidgetArea } from "emdash";
import WidgetRenderer from "../components/WidgetRenderer.astro";
const sidebar = await getWidgetArea("sidebar");
---
<div class="layout">
<main class="content">
<slot />
</main>
{sidebar && sidebar.widgets.length > 0 && (
<aside class="sidebar">
{sidebar.widgets.map(widget => (
<WidgetRenderer widget={widget} />
))}
</aside>
)}
</div>
<style>
.layout {
display: grid;
grid-template-columns: 1fr 300px;
gap: 2rem;
}
@media (max-width: 768px) {
.layout {
grid-template-columns: 1fr;
}
}
</style>
列出所有小部件区域
使用 getWidgetAreas() 获取所有小部件区域及其小部件:
import { getWidgetAreas } from "emdash";
const areas = await getWidgetAreas();
// 返回所有已填充小部件的区域
创建小部件区域
通过 /_emdash/admin/widgets 的管理界面创建小部件区域,或使用管理 API:
POST /_emdash/api/widget-areas
Content-Type: application/json
{
"name": "footer-1",
"label": "页脚第 1 列",
"description": "页脚中的第一列"
}
添加内容小部件:
POST /_emdash/api/widget-areas/footer-1/widgets
Content-Type: application/json
{
"type": "content",
"title": "关于我们",
"content": [
{
"_type": "block",
"style": "normal",
"children": [{ "_type": "span", "text": "欢迎来到我们的网站。" }]
}
]
}
添加组件小部件:
POST /_emdash/api/widget-areas/sidebar/widgets
Content-Type: application/json
{
"type": "component",
"title": "最新文章",
"componentId": "core:recent-posts",
"componentProps": { "count": 5, "showDate": true }
}
API 参考
getWidgetArea(name)
按名称获取小部件区域及所有小部件。
参数:
name— 小部件区域的唯一标识符(string)
返回: Promise<WidgetArea | null>
getWidgetAreas()
列出所有小部件区域及其小部件。
返回: Promise<WidgetArea[]>
getWidgetComponents()
列出管理 UI 可用的小部件组件定义。
返回: WidgetComponentDef[]