Widget Areas

On this page

Widget areas are named regions in your templates where administrators can place content blocks. Use them for sidebars, footer columns, promotional banners, or any section that editors should control without touching code.

Querying Widget Areas

Use getWidgetArea() to fetch a widget area by name:

---
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>}
        <!-- Render widget content -->
      </div>
    ))}
  </aside>
)}

The function returns null if the widget area does not exist.

Widget Area Structure

A widget area contains metadata and an array of widgets:

interface WidgetArea {
	id: string;
	name: string; // Unique identifier ("sidebar", "footer-1")
	label: string; // Display name ("Main Sidebar")
	description?: string;
	widgets: Widget[];
}

interface Widget {
	id: string;
	type: "content" | "menu" | "component";
	title?: string;
	// Type-specific fields
	content?: PortableTextBlock[]; // For content widgets
	menuName?: string; // For menu widgets
	componentId?: string; // For component widgets
	componentProps?: Record<string, unknown>;
}

Widget Types

EmDash supports three widget types:

Content Widgets

Rich text content stored as Portable Text. Render using the PortableText component:

---
import { PortableText } from "emdash/ui";
---

{widget.type === "content" && widget.content && (
  <div class="widget-content">
    <PortableText value={widget.content} />
  </div>
)}

Display a navigation menu within a widget area:

---
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>
)}

Component Widgets

Render a registered component with configurable props. EmDash includes these core components:

Component IDDescriptionProps
core:recent-postsList of recent postscount, showThumbnails, showDate
core:categoriesCategory listshowCount, hierarchical
core:tagsTag cloudshowCount, limit
core:searchSearch formplaceholder
core:archivesMonthly/yearly archivestype, limit

Rendering Widgets

Create a reusable widget renderer component:

---
import { PortableText } from "emdash/ui";
import { getMenu } from "emdash";
import type { Widget } from "emdash";

// Import your widget components
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>

Example Widget Components

Recent Posts Widget

---
import { getEmDashCollection } from "emdash";

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 && (
        <img src={post.data.featured_image} alt="" class="thumbnail" />
      )}
      <a href={`/posts/${post.slug}`}>{post.data.title}</a>
      {showDate && post.data.publishedAt && (
        <time datetime={post.data.publishedAt.toISOString()}>
          {post.data.publishedAt.toLocaleDateString()}
        </time>
      )}
    </li>
  ))}
</ul>

Search Widget

---
interface Props {
  placeholder?: string;
}

const { placeholder = "Search..." } = Astro.props;
---

<form action="/search" method="get" class="search-form">
  <input
    type="search"
    name="q"
    placeholder={placeholder}
    aria-label="Search"
  />
  <button type="submit">Search</button>
</form>

Using Widget Areas in Layouts

The following example shows a blog layout with a sidebar widget area:

---
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>

Listing All Widget Areas

Use getWidgetAreas() to retrieve all widget areas with their widgets:

import { getWidgetAreas } from "emdash";

const areas = await getWidgetAreas();
// Returns all areas with widgets populated

Creating Widget Areas

Create widget areas through the admin interface at /_emdash/admin/widgets, or use the admin API:

POST /_emdash/api/widget-areas
Content-Type: application/json

{
  "name": "footer-1",
  "label": "Footer Column 1",
  "description": "First column in the footer"
}

Add a content widget:

POST /_emdash/api/widget-areas/footer-1/widgets
Content-Type: application/json

{
  "type": "content",
  "title": "About Us",
  "content": [
    {
      "_type": "block",
      "style": "normal",
      "children": [{ "_type": "span", "text": "Welcome to our site." }]
    }
  ]
}

Add a component widget:

POST /_emdash/api/widget-areas/sidebar/widgets
Content-Type: application/json

{
  "type": "component",
  "title": "Recent Posts",
  "componentId": "core:recent-posts",
  "componentProps": { "count": 5, "showDate": true }
}

API Reference

getWidgetArea(name)

Fetch a widget area by name with all widgets.

Parameters:

  • name — The widget area’s unique identifier (string)

Returns: Promise<WidgetArea | null>

getWidgetAreas()

List all widget areas with their widgets.

Returns: Promise<WidgetArea[]>

getWidgetComponents()

List available widget component definitions for the admin UI.

Returns: WidgetComponentDef[]