Un thème EmDash est un projet Astro qu’un autre développeur peut initialiser avec create-astro. Construisez et testez le projet comme un site complet, puis incluez un seed qui crée le modèle de contenu attendu par ses routes et composants.
Partir d’un modèle actuel
Choisissez le modèle existant le plus proche du site visé et de la cible de déploiement. Les variantes Node et Cloudflare regroupent base de données, stockage, adaptateur et configuration middleware.
Le modèle blog est une base utile pour un site de contenu :
npm create astro@latest -- --template @emdash-cms/template-blog
Utilisez @emdash-cms/template-blog-cloudflare lorsque le thème résultant cible Cloudflare Workers, D1 et R2.
Conserver la structure du modèle
Le modèle blog actuel utilise ces chemins pertinents :
astro.config.mjs
emdash-env.d.ts
package.json
seed/
└── seed.json
src/
├── components/
│ └── PostCard.astro
├── layouts/
│ └── Base.astro
├── live.config.ts
├── pages/
│ ├── index.astro
│ ├── category/[slug].astro
│ ├── pages/[slug].astro
│ ├── posts/index.astro
│ ├── posts/[slug].astro
│ ├── search.astro
│ └── tag/[slug].astro
└── styles/
Les modèles starter, portfolio et marketing utilisent des routes différentes. Copiez les fichiers réels de la base choisie plutôt que de supposer que chaque thème a une route catch-all pour les pages.
Pointer vers le seed
Les modèles actuels déclarent le chemin du seed dans package.json :
{
"name": "@example/emdash-theme-publication",
"private": true,
"type": "module",
"emdash": {
"seed": "seed/seed.json"
}
}
EmDash découvre aussi .emdash/seed.json et le repli conventionnel seed/seed.json. Utilisez le champ du paquet lors de la distribution d’un modèle pour que le fichier prévu soit explicite.
Définir le modèle de contenu
Si vous partez du modèle blog, modifiez son seed/seed.json existant plutôt que de le remplacer par un modèle sans lien. Le seed réduit suivant conserve les collections et les données structurelles utilisées dans les exemples de ce guide :
{
"$schema": "https://emdashcms.com/seed.schema.json",
"version": "1",
"meta": {
"name": "Publication",
"description": "A publication with posts"
},
"settings": {
"title": "Publication",
"tagline": "Latest articles"
},
"collections": [
{
"slug": "posts",
"label": "Posts",
"labelSingular": "Post",
"supports": ["drafts", "revisions", "search", "seo"],
"fields": [
{
"slug": "title",
"label": "Title",
"type": "string",
"required": true,
"searchable": true
},
{
"slug": "excerpt",
"label": "Excerpt",
"type": "text"
},
{
"slug": "featured_image",
"label": "Featured image",
"type": "image"
},
{
"slug": "content",
"label": "Content",
"type": "portableText",
"searchable": true
}
]
},
{
"slug": "pages",
"label": "Pages",
"labelSingular": "Page",
"supports": ["drafts", "revisions", "search"],
"fields": [
{
"slug": "title",
"label": "Title",
"type": "string",
"required": true,
"searchable": true
},
{
"slug": "content",
"label": "Content",
"type": "portableText",
"searchable": true
},
{
"slug": "template",
"label": "Page template",
"type": "select",
"defaultValue": "default",
"validation": {
"options": ["default", "full-width", "landing"]
}
}
]
}
],
"menus": [
{
"name": "primary",
"label": "Primary navigation",
"items": [
{ "type": "custom", "label": "Home", "url": "/" },
{ "type": "custom", "label": "Posts", "url": "/posts" }
]
}
],
"widgetAreas": [
{
"name": "sidebar",
"label": "Sidebar",
"widgets": []
}
],
"content": {
"posts": [
{
"id": "post-welcome",
"slug": "welcome",
"status": "published",
"data": {
"title": "Welcome",
"excerpt": "The first article",
"content": []
}
}
]
}
}
L’id d’une entrée de contenu est un identifiant local au seed utilisé par les références. Il n’est pas forcé de devenir l’ID en base d’une entrée routable. Son slug devient l’identifiant de route exposé comme entry.id par l’API de requête.
Lisez Fichiers seed avant d’ajouter taxonomies, bylines, références de menu, médias, redirections, zones de widgets, sections, localisation ou comportement en cas de conflit.
Construire des routes rendues côté serveur
Les modèles EmDash actuels utilisent output: "server". Les routes interrogent le contenu live à chaque requête. N’ajoutez pas getStaticPaths() aux routes de contenu d’un thème sauf si le modèle utilise EmDash délibérément uniquement comme source au build.
L’archive suivante trie en base via le champ stocké published_at :
---
import { getEmDashCollection } from "emdash";
import Base from "../../layouts/Base.astro";
const { entries: posts, error, cacheHint } = await getEmDashCollection("posts", {
orderBy: { published_at: "desc" },
});
if (error) return new Response("Could not load posts", { status: 500 });
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---
<Base title="Posts">
{posts.map((post) => (
<article>
<h2><a href={`/posts/${post.id}`}>{post.data.title}</a></h2>
{post.data.excerpt && <p>{post.data.excerpt}</p>}
</article>
))}
</Base>
orderBy est un objet champ vers direction. Utilisez orderBy: { published_at: "desc" }, pas sort, sortBy ni un callback JavaScript.
La route suivante résout et affiche un article :
---
import { decodeSlug, getEmDashEntry } from "emdash";
import { PortableText } from "emdash/ui";
import Base from "../../layouts/Base.astro";
const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.redirect("/404");
const { entry: post, error, cacheHint } = await getEmDashEntry("posts", slug);
if (error) return new Response("Could not load the post", { status: 500 });
if (!post) return Astro.redirect("/404");
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---
<Base title={post.data.title} content={{ collection: "posts", id: post.data.id, slug }}>
<article>
<h1 {...post.edit.title}>{post.data.title}</h1>
<PortableText value={post.data.content} />
</article>
</Base>
Utilisez post.id dans les URL de route et post.data.id lorsqu’un helper exige l’ID de contenu stocké.
Interroger la navigation gérée par le site
Les valeurs gérées par le CMS doivent provenir des API correspondantes. Les modèles actuels utilisent getSiteSettings(), getMenu() et <WidgetArea /> dans leurs layouts :
---
import { getMenu, getSiteSettings } from "emdash";
import { WidgetArea } from "emdash/ui";
const [settings, primary] = await Promise.all([
getSiteSettings(),
getMenu("primary"),
]);
---
<header>
<a href="/">{settings.title}</a>
<nav>
{primary?.items.map((item) => <a href={item.url}>{item.label}</a>)}
</nav>
</header>
<main><slot /></main>
<aside><WidgetArea name="sidebar" /></aside>
Le texte de design statique peut rester dans les fichiers Astro. Les valeurs que les administrateurs doivent pouvoir modifier doivent être représentées dans les paramètres, le contenu, les menus ou les widgets.
Afficher les champs image
Les champs image sont des valeurs média, pas des chaînes URL. Passez la valeur complète au composant Image pour que le stockage local et les fournisseurs d’images se résolvent de façon cohérente :
---
import { Image } from "emdash/ui";
const { post } = Astro.props;
---
<article>
{post.data.featured_image && (
<Image
image={post.data.featured_image}
alt={post.data.title}
width={800}
height={450}
/>
)}
<h2><a href={`/posts/${post.id}`}>{post.data.title}</a></h2>
</article>
Le composant utilise le texte alternatif du champ sauf si vous le remplacez. Réservez priority à une image attendue above the fold ; les autres restent en chargement différé.
Proposer des choix de mise en page
Le seed de départ ci-dessus inclut un champ select pour les sites où les éditeurs ont besoin de plusieurs layouts de page. En ajoutant la fonctionnalité à un autre seed, utilisez des valeurs stables qui correspondent à des composants connus :
{
"slug": "template",
"label": "Page template",
"type": "select",
"defaultValue": "default",
"validation": {
"options": ["default", "full-width", "landing"]
}
}
La route peut alors choisir dans une carte explicite de composants :
---
import { decodeSlug, getEmDashEntry } from "emdash";
import PageDefault from "../../layouts/PageDefault.astro";
import PageFullWidth from "../../layouts/PageFullWidth.astro";
import PageLanding from "../../layouts/PageLanding.astro";
const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.redirect("/404");
const { entry: page } = await getEmDashEntry("pages", slug);
if (!page) return Astro.redirect("/404");
const layouts = {
default: PageDefault,
"full-width": PageFullWidth,
landing: PageLanding,
};
const Layout = layouts[page.data.template as keyof typeof layouts] ?? PageDefault;
---
<Layout {page} />
La carte explicite empêche une valeur de champ stockée de devenir un chemin de module arbitraire.
Ajouter la recherche
Activez search dans chaque collection qui doit apparaître dans les résultats, et marquez les champs pertinents comme searchable. Les modèles actuels utilisent LiveSearch pour une route de recherche prête à l’emploi :
---
import LiveSearch from "emdash/ui/search";
import Base from "../layouts/Base.astro";
---
<Base title="Search">
<h1>Search</h1>
<LiveSearch placeholder="Search posts and pages" collections={["posts", "pages"]} />
</Base>
Sur un site Astro i18n, LiveSearch utilise Astro.currentLocale. Passez locale={null} uniquement lorsque la page recherche intentionnellement toutes les locales.
Seed de sections réutilisables
Les sections offrent aux éditeurs des points de départ Portable Text réutilisables. Ajoutez-les lorsque le design inclut un motif de contenu répété, comme un appel à l’action :
{
"version": "1",
"sections": [
{
"slug": "newsletter-signup",
"title": "Newsletter signup",
"description": "Heading and copy for the newsletter form",
"keywords": ["newsletter", "email"],
"content": [
{
"_type": "block",
"_key": "newsletter-heading",
"style": "h2",
"children": [
{ "_type": "span", "_key": "newsletter-heading-text", "text": "Get new articles by email" }
]
}
]
}
]
}
L’assistant de configuration permet d’omettre les entrées seed, bylines et termes de taxonomie. Les sections et le reste du modèle structurel sont toujours appliqués.
Ajouter des blocks de page structurés
Utilisez un champ blocks lorsque la page elle-même est une composition ordonnée de sections typées. Utilisez un block Portable Text personnalisé lorsque l’objet appartient à un document rich text et participe au flux des paragraphes.
Le modèle marketing seede cinq types de blocks conservés et les autorise dans le champ content des Pages. Son wrapper associe chaque _type généré à un composant Astro :
---
import { Blocks, defineBlockComponents } from "emdash/ui";
import type { PageContentBlock } from "../../emdash-env";
import FAQ from "./blocks/FAQ.astro";
import Features from "./blocks/Features.astro";
import Hero from "./blocks/Hero.astro";
import Pricing from "./blocks/Pricing.astro";
import Testimonials from "./blocks/Testimonials.astro";
const components = defineBlockComponents<PageContentBlock>({
marketing_hero: Hero,
marketing_features: Features,
marketing_testimonials: Testimonials,
marketing_pricing: Pricing,
marketing_faq: FAQ,
});
---
<Blocks value={Astro.props.value} components={components} />
L’union PageContentBlock générée aligne la carte de composants sur chaque type de block autorisé et version conservée. Chaque valeur de block seedée inclut _type, _version et une _key stable.
Utilisez un block Portable Text personnalisé pour un objet entre paragraphes, comme un diagramme intégré dans un article. Mappez cet objet via l’option components.type du composant PortableText. Un seed peut inclure la valeur stockée, mais un plugin natif doit enregistrer un éditeur personnalisé pour créer et modifier cet objet Portable Text.
Utilisez un plugin natif lorsque le block nécessite un éditeur personnalisé réutilisable et des composants de rendu empaquetés. Les plugins sandbox ne peuvent pas fournir de composants de rendu Astro dans le build du site.
Tester le modèle
-
Initialisez le modèle dans un répertoire propre avec
create-astro. -
Installez les dépendances et exécutez les commandes de build et de vérification de types du site.
-
Commencez avec une base de données vide et terminez
/_emdash/admin/setup. -
Testez la configuration avec et sans contenu d’exemple.
-
Ouvrez chaque route avec contenu seed, contenu nouvellement créé, sans image optionnelle et avec une collection vide.
-
Modifiez paramètres du site, menus, affectations de taxonomie, Portable Text et blocks via l’admin. Pour les blocks, ajoutez, réordonnez, dupliquez et supprimez chaque type autorisé. Confirmez que les routes publiques utilisent les valeurs modifiées.
-
Appliquez la configuration spécifique au déploiement dans un environnement jetable et vérifiez stockage média, aperçus et rendu serveur.
Publier le modèle
Un dépôt GitHub peut être utilisé directement avec la syntaxe de modèle github: d’Astro :
npm create astro@latest -- --template github:example/emdash-theme-publication
Avant publication, supprimez bases de données et uploads locaux, gardez les secrets hors du dépôt, vérifiez les chemins de paquets depuis un checkout propre et documentez la cible de déploiement configurée par le modèle.
Liste de contrôle
-
astro.config.mjsutilise la sortie serveur et les adaptateurs de déploiement prévus. -
src/live.config.tsenregistreemdashLoader(). -
package.json#emdash.seedpointe vers un fichier existant. - Chaque collection et champ interrogés est déclaré par le seed.
- Les exemples de requête utilisent
orderByet le bon identifiant d’entrée. - Les valeurs de site gérées par le CMS ne sont pas dupliquées comme constantes permanentes du modèle.
- Une configuration propre fonctionne avec et sans contenu d’exemple.
- Le build et la vérification de types du modèle passent pour la cible supportée.