| name | setup-blog |
| description | Install and configure the Blog Manager on a deco-site storefront. Covers the blog app, page routes, sections, Blog Manager MCP connection in Studio, and first-run brand context initialization. |
Set up the Blog Manager integration on a deco storefront. Follow every step exactly — this runs across many sites and must be consistent.
Step 1 — Verify dependency version
Before creating any files, verify that the apps dependency meets the minimum version:
apps must be at least 0.144.0
Check deno.json (or import_map.json) for the entry that resolves apps/:
"apps/": "https://deno.land/x/deco_apps@0.x.x/"
If the version is below the required minimum, stop and tell the user:
"The apps dependency is currently at <version>. Please update it to at least 0.144.0 before continuing — the blog requires features introduced in that version."
Only proceed once the version requirement is satisfied.
Step 2 — Create blog app re-export file
Create apps/deco/blog.ts:
export { default } from "apps/blog/mod.ts";
export * from "apps/blog/mod.ts";
Step 3 — Register the blog app as a deco block
Create .deco/blocks/deco-blog.json:
{ "__resolveType": "site/apps/deco/blog.ts" }
Step 4 — Find the correct Header and Footer names
The Header and Footer __resolveType values vary per site — do not assume "Global Header" and "Global Footer".
Before creating the page files, inspect the site's homepage block to find the exact names in use:
- Read
.deco/blocks/pages-home.json (or whichever file contains the / route).
- Find the Lazy sections that wrap the Header and Footer — copy their
__resolveType values exactly.
- Use those values in Steps 5 and 6 below wherever
"Global Header" and "Global Footer" appear.
Step 5 — Create the Blog Listing page
Create .deco/blocks/pages-Blog-listing.json:
{
"__resolveType": "website/pages/Page.tsx",
"name": "Blog",
"path": "/blog",
"seo": {
"__resolveType": "website/sections/Seo/SeoV2.tsx",
"title": "Blog",
"description": "Latest articles and posts."
},
"sections": [
{
"__resolveType": "website/sections/Rendering/Lazy.tsx",
"section": {
"__resolveType": "Global Header"
}
},
{
"__resolveType": "site/sections/Blog/BlogPosts.tsx",
"title": "Blog",
Step 6 — Create the Blog Post page
Create .deco/blocks/pages-blogpost.json:
{
"__resolveType": "website/pages/Page.tsx",
"name": "Blog Post Page",
"path": "/blog/:slug",
"seo": {
"__resolveType": "blog/sections/Seo/SeoBlogPost.tsx",
"jsonLD": {
"__resolveType": "blog/loaders/BlogPostPage.ts",
"slug": {
"__resolveType": "website/functions/requestToParam.ts",
"param": "slug"
}
}
},
"sections": [
{
"__resolveType": "website/sections/Rendering/Lazy.tsx",
"section": {
"__resolveType": "Global Header"
Both the BlogPost section and SEO use blog/loaders/BlogPostPage.ts. Blog Manager publishes posts directly to the blog block collection (collections/blog/posts/{slug}) — the loader picks them up automatically.
Step 7 — Create the Blog sections
These two sections must exist under sections/Blog/. Each site may already have them from a previous setup — check first. If they don't exist, create them based on the templates below.
sections/Blog/BlogPosts.tsx
Renders the post grid with optional category/search filtering and pagination.
import type { BlogPost } from "apps/blog/types.ts";
import Image from "apps/website/components/Image.tsx";
export interface Props {
title?: string;
posts?: BlogPost[];
postsPerPage?: number;
showMoreText?: string;
page?: number;
selectedCategory?: string;
search?: string;
baseUrl?: string;
hasMore?: boolean;
?: ;
}
() {
url = (req.);
page = (url..()) || ;
selectedCategory = url..() || ;
search = url..() || ;
filtered = props. || [];
(selectedCategory) {
filtered = filtered.( p.?.( c. === selectedCategory));
}
(search) {
q = search.();
filtered = filtered.(
p..().(q) || p.?.().(q),
);
}
perPage = props. || ;
displayCount = page * perPage;
visible = filtered.(, displayCount);
{
...props,
: visible,
page,
selectedCategory,
search,
: displayCount < filtered.,
: filtered.,
: req.,
};
}
() {
{
url = (baseUrl || );
url..(, (page + ));
;
} {
;
}
}
() {
(
);
}
() {
(
);
}
sections/Blog/BlogPost.tsx
Renders the full blog post: hero, featured image, and rich content sections.
import type { BlogPostPage } from "apps/blog/types.ts";
import Image from "apps/website/components/Image.tsx";
import { renderSection } from "apps/website/pages/Page.tsx";
import { Section } from "@deco/deco/blocks";
import type { AppContext } from "site/apps/site.ts";
export interface Props {
page?: BlogPostPage | null;
sectionsToRender?: Section[];
}
export const loader = async (props: Props, _req: Request, ctx: AppContext) => {
if (!props?.page?.post?.sections) return props;
const sectionsToRender = await Promise.all(
props.page.post..( (section, index) =>
ctx.(section, {
: [
{ : ctx. ?? , : },
{ : + index, : },
],
})
),
);
{ ...props, sectionsToRender };
};
() {
(!page) {
(
);
}
(
{}
{page.. && (
)}
{}
<div =>
{.(sectionsToRender) && sectionsToRender.(renderSection)}
</div>
</div>
);
}
() {
(
);
}
Step 8 — Add a Blog link to the Header navbar
The header block is already known from Step 4. Now add a Blog navigation link to it so users can reach /blog from any page.
-
Find the header block file.
- The header
__resolveType found in Step 4 (e.g. "Global Header") is the block name.
- Locate its file under
.deco/blocks/ — it is typically named something like header.json, global-header.json, or a similar slug derived from the block name.
- If you are unsure, list
.deco/blocks/ and open the file whose "name" field matches the header block name.
-
Inspect the navbar items.
- Inside the header block JSON, look for the array that holds navigation links. It is usually a field named
navItems, items, links, navigation, or similar — open the file and look for the array of objects that each contain a label/name and an href/url field.
-
Append the Blog entry.
-
Save the file — no other changes are needed; the header section will pick up the new link automatically.
If the header block JSON is deeply nested or uses a resolver pattern (e.g. "__resolveType": "..." for the items array), trace the resolver to find the correct place to add the link, and follow the same pattern used for existing items.
Step 9 — Connect Blog Manager in Studio
After the storefront pages are set up, connect the Blog Manager MCP agent in Studio.
- Open Studio → Settings → Virtual MCPs → + New
- Set the MCP server URL to:
https://spire-agent.infra.deco.cx/mcp
- Give it a name like "Blog Manager" and save.
- Studio will discover 20 tools including
BLOG_MANAGER — it appears as a Pinned View tab automatically.
Then configure the Blog Manager settings (in Studio → Settings → Blog Manager):
- Domain — the site's domain (e.g.
mystore.com.br)
- GitHub Owner — the org or user that owns the site's repo (e.g.
decocms)
- GitHub Repo — the repo name (e.g.
mystore)
- Language — content language (e.g.
pt-BR or en)
Then connect the required bindings in Studio → Settings → Connections:
| Binding | Required | Purpose |
|---|
| GitHub | Yes | Publishes posts to .deco/blocks/ in the site repo |
| Google Analytics 4 | Optional | Traffic and engagement data (ANALYTICS_REPORT) |
| Google Search Console | Optional | Rankings and impressions (ANALYTICS_OPPORTUNITIES) |
| SemRush | Optional | Keyword research (SEO_CONTENT_CLUSTER) |
| nanoBanana | Optional | AI cover image generation (POST_CREATE) |
Step 10 — Initialize brand context
Brand context is the mandatory first step before any content generation. The agent will not create posts until context is loaded.
- In Studio, open the Blog Manager agent — the BLOG_MANAGER Pinned View appears as a tab.
- Call BLOG_STATUS — it reports
context.hasContext: false and points to the Context tab.
- The UI redirects to the Context tab automatically when GitHub is connected but context is missing.
- Click "Build Brand Context" and enter the site URL — this scrapes the homepage, extracts brand identity with AI, and saves
brand/brand.md and brand/guardrails.md to .deco/blog-manager/brand/ in the site's GitHub repo.
- Once complete, every session automatically loads context before generating any content.
Re-run BLOG_BUILD_CONTEXT (with force: true) whenever there is a major brand or product category change.
Notes for customization
- Styles: The section templates above use plain Tailwind. Replace class names to match the site's design system (color tokens, font utilities, spacing helpers, etc.) once sections are created.
- SEO on the listing page: Fill in
title and description in pages-Blog-listing.json with the correct site-specific copy.
postsPerPage: Defaults to 100 so all posts load client-side for filtering. Reduce this if the site has many posts and server-side pagination is preferred.
- Posts storage: Blog Manager publishes posts to
.deco/blocks/collections%2Fblog%2Fposts%2F{slug}.json in the site's repo — they appear in the blog listing automatically via blog/loaders/BlogpostList.ts.
- Library files: Brand context lives at
.deco/blog-manager/brand/brand.md and .deco/blog-manager/brand/guardrails.md. Campaign data lives at .deco/blog-manager/campaigns/{weekId}/.
- Campaign workflow: Use CAMPAIGN_BRIEF (keyword research, operator selects keywords) → CAMPAIGN_PLAN (ranked post ideas) → POST_OUTLINE → POST_CREATE → BLOG_PUBLISH_POST for the full 2-gate approval flow.