Build a Blog with Meno
Create a complete blog with a CMS-powered content system, a filterable index page, and individually generated post pages -- all outputting static HTML.
Prerequisites: Meno is running at localhost:3000. You have completed the Quickstart and are comfortable creating pages and adding elements.
What you will build:
A "blog" CMS collection with structured fields
A blog index page that lists all posts with filtering and search
A template page that generates individual URLs for each post
Static HTML output ready for deployment
Step 1: Create the Blog Collection
The CMS stores your blog posts as structured JSON files on disk. Start by defining the collection schema.
Click the CMS tab in the left sidebar. You will see an empty collections list if this is a fresh project.
Click the + button to create a new collection.
In the dialog that appears, set the ID to
blogand the Display Name toBlog Posts.Add the following fields one by one. For each field, click Add Field, choose the type, and fill in the details:
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| title | string | Yes | The post headline |
| slug | string | Yes | URL-friendly identifier (e.g. my-first-post) |
| excerpt | text | No | A short summary shown on the index page |
| content | rich-text | No | The full post body with formatting |
| coverImage | image | No | Featured image displayed at the top of the post |
| author | string | No | Default value: Admin |
| publishedAt | date | No | The publication date |
| category | select | No | Options: Technology, Design, Business, Lifestyle |
| featured | boolean | No | Default value: false |
Set the Slug Field to
slug. This tells Meno which field to use when building URLs.Set the URL Pattern to
/blog/{{slug}}. Each post will be accessible at a path like/blog/my-first-post.Click Create.
Meno creates a template page at pages/templates/blog-post.json and a data folder at cms/blog/. You will customize the template page in Step 4.
Step 2: Add Blog Posts
With the collection in place, populate it with content.
In the CMS tab, click the Blog Posts collection to expand it.
Click the + button next to the collection name to create a new item.
The CMS field editor opens on the right. Fill in each field: - title:
Getting Started with Static Sites- slug:getting-started-with-static-sites- excerpt:Learn why static HTML is faster, more secure, and easier to deploy than traditional server-rendered websites.- content: Write a few paragraphs using the rich text editor. Add headings, bold text, and links to test formatting. - coverImage: Click the file picker and select an image from your project's images folder, or type a path like/images/static-sites.jpg. - author:Jane Doe- publishedAt: Select today's date. - category: ChooseTechnologyfrom the dropdown. - featured: Toggle on.Click Save. The item appears in the collection list.
Repeat to create at least 3 more posts. Use different categories so you can test filtering later. For example: - "Designing for Accessibility" (category: Design) - "Remote Work Productivity Tips" (category: Business) - "Minimalist Living Guide" (category: Lifestyle)
Tip: If you have content in a spreadsheet, click the Import button at the top of the item list and select a CSV file. Column headers must match your field names (title, slug, excerpt, etc.).
Step 3: Create the Blog Index Page
The index page lists all blog posts in a grid with links to each individual post.
Set up the page
Switch to the Pages tab in the left sidebar.
Click + to create a new page. Name it
blog.The page opens in the canvas. You see an empty root div.
Add the page header
With the root div selected, press Cmd+E to open the Command Palette.
Search for Navigation and add it. This places your site navigation at the top.
Select the root div again and press Cmd+E. Search for Section and add it below the Navigation. This section will hold the page title.
Inside the Section, add a Heading element. Set its text to
Blog.Add a Text element below the heading. Set its text to
Thoughts, tutorials, and stories from our team.
Add the CMS List
Select the root div and add another Section below the first one. This section will contain the post grid.
Select the new Section and press Cmd+E. Search for List and select the CMS List option.
The List configuration appears in the Properties panel. Set these values: - Source Type:
collection- Source:blog- Sort Field:publishedAt- Sort Order:desc(newest first) - Limit:10
Build the post card template
Everything you add inside the CMS List becomes the template that repeats for each blog post.
Inside the List, add a link element. In the Properties panel, set its href to
/blog/{{item.slug}}. This creates a clickable link to each post's individual page.Inside the link, add an img element. Set its src attribute to
{{item.coverImage}}and alt to{{item.title}}.Below the image (still inside the link), add an h3 element. Set its text content to
{{item.title}}.Add a p element below the heading. Set its text to
{{item.excerpt}}.Add another p element for metadata. Set its text to
{{item.author}} -- {{item.publishedAt}}.
Style the layout
Select the List's container element. In the style panel, set display to
gridand gridTemplateColumns to1fr 1fr 1fr. Set gap to32px.Switch to the Tablet breakpoint and change gridTemplateColumns to
1fr 1fr.Switch to the Mobile breakpoint and change it to
1fr.
The canvas now shows your blog posts in a responsive grid. Each card displays the cover image, title, excerpt, and author.
Set page metadata
Click the page background to deselect all elements. The Properties panel shows page-level settings.
Set title to
Blog | My Site.Set description to
Read our latest posts on technology, design, business, and lifestyle.
Step 4: Create the Blog Post Template
The template page generates a unique HTML file for every item in the blog collection. You already have the skeleton at pages/templates/blog-post.json from Step 1. Now customize its layout.
In the Pages tab, navigate to templates/ and click blog-post to open it.
The canvas shows the template page. You will see placeholder content with
{{cms.fieldName}}expressions.
Build the post layout
Clear the existing content if needed. Add a Navigation component at the top.
Add a Section below the Navigation for the hero area.
Inside the Section, add an img element. Set src to
{{cms.coverImage}}and alt to{{cms.title}}. Style it full-width.Add an h1 element below the image. Set its content to
{{cms.title}}.Add a p element for the byline:
By {{cms.author}} on {{cms.publishedAt}}.Add a category badge -- a span element with content
{{cms.category}}. Give it a background color and rounded corners for a pill-style look.
Add the main content
Add another Section below the hero for the post body.
Add a div element inside it. Set its content to
{{cms.content}}. Since the content field is rich-text, Meno renders the full HTML (headings, paragraphs, links, images) at build time.Style the content div with a comfortable reading width: maxWidth
720px, margin0 auto, lineHeight1.8.
Configure page metadata
Click the page background to open page-level settings.
Set title to
{{cms.title}} | Blog. Each generated page will have the post title in its browser tab.Set description to
{{cms.excerpt}}. Search engines will show the excerpt as the page snippet.Verify the cms settings in the page configuration: - id:
blog- slugField:slug- urlPattern:/blog/{{slug}}
These settings are already populated from when you created the collection. They tell the build system which collection to use and how to generate URLs.
Step 5: Add Category Filtering
Make the blog index page interactive by letting visitors filter posts by category and search by keyword. This uses MenoFilter, which works entirely with data attributes on your HTML -- no custom JavaScript required.
Add data attributes to list items
Go back to the blog index page (not the template).
Select the outermost element of the card template inside the CMS List (the link wrapper you created in Step 3).
In the Properties panel, open the Attributes section.
Add a custom attribute:
data-categorywith value{{item.category}}.Add another:
data-featuredwith value{{item.featured}}.
These attributes are rendered into the HTML for each post, allowing MenoFilter to read and filter them client-side.
Wrap with a filter container
Select the Section that contains the CMS List.
In the Attributes section, add
data-meno-filterwith valueblog.Add
data-meno-url-syncwith valuetrue. This syncs the active filters to the URL, so visitors can share filtered views.
Add filter buttons
Inside the filter Section, above the CMS List, add a div to hold the filter buttons. Style it as a horizontal flex row with gap.
Inside the div, add a button element with text
All. Add the attributedata-meno-clear(empty value). This button clears all active filters.Add four more buttons, one for each category:
| Button Text | Attribute 1 | Attribute 2 |
|-------------|-------------|-------------|
| Technology | data-meno-filter-field="category" | data-meno-filter-value="Technology" |
| Design | data-meno-filter-field="category" | data-meno-filter-value="Design" |
| Business | data-meno-filter-field="category" | data-meno-filter-value="Business" |
| Lifestyle | data-meno-filter-field="category" | data-meno-filter-value="Lifestyle" |
MenoFilter automatically adds an active CSS class to the selected button. Style the active state with a different background color.
Add search
Above the filter buttons (or alongside them), add an input element with
type="text"andplaceholder="Search posts...".Add the attribute
data-meno-search(empty value).Add
data-meno-search-fieldswith valuetitle,excerpt. This tells MenoFilter which data attributes to search through.
Add a results count and empty state
Below the filter buttons, add a span element with the attribute
data-meno-countset toresults. MenoFilter will fill this with the number of matching posts (e.g. "4").Add a text node next to it that reads
posts foundso the full text reads "4 posts found".Below the CMS List, add a div with attribute
data-meno-empty. Set its content toNo posts match your filters. Try a different category or search term.This element is hidden when results exist and shown when the list is empty.
Step 6: Build and Deploy
Your blog is complete. Generate the static output.
Open a terminal and run:
``bash
bun run build
``
Meno processes everything and writes the output to the
dist/folder. Check the structure:
``
dist/
blog/
index.html # Blog index page with all posts
getting-started-with-static-sites/
index.html # Individual post page
designing-for-accessibility/
index.html
remote-work-productivity-tips/
index.html
minimalist-living-guide/
index.html
sitemap.xml # Auto-generated, includes all post URLs
robots.txt # Auto-generated
``
Each blog post has its own directory with an
index.htmlfile, giving you clean URLs like/blog/getting-started-with-static-sites.
Open
dist/blog/index.htmlin a browser. You should see: - All posts listed in a grid, sorted by date (newest first) - Category filter buttons that show/hide posts instantly - A search input that filters by title and excerpt - A results count that updates as you filter - An empty state message when no posts match
Click any post card to navigate to its individual page. Verify the title, cover image, content, and metadata render correctly.
Deploy the
dist/folder to any static hosting provider:
```bash # Netlify netlify deploy --dir=dist --prod
# Cloudflare Pages npx wrangler pages deploy dist
# GitHub Pages - push dist/ to the gh-pages branch
# Or preview locally bunx serve dist ```
Under the Hood
Blog Index Page JSON
The CMS List on the index page generates HTML for each post using template expressions. Here is the core structure:
{
"type": "node",
"tag": "div",
"attributes": {
"data-meno-filter": "blog",
"data-meno-url-sync": "true"
},
"children": [
{
"type": "node",
"tag": "button",
"attributes": { "data-meno-clear": "" },
"children": "All"
},
{
"type": "node",
"tag": "button",
"attributes": {
"data-meno-filter-field": "category",
"data-meno-filter-value": "Technology"
},
"children": "Technology"
},
{
"type": "list",
"sourceType": "collection",
"source": "blog",
"sort": { "field": "publishedAt", "order": "desc" },
"limit": 10,
"children": [
{
"type": "link",
"href": "/blog/{{item.slug}}",
"children": [
{
"type": "node",
"tag": "img",
"attributes": {
"src": "{{item.coverImage}}",
"alt": "{{item.title}}",
"loading": "lazy"
}
},
{
"type": "node",
"tag": "h3",
"children": "{{item.title}}"
},
{
"type": "node",
"tag": "p",
"children": "{{item.excerpt}}"
}
]
}
]
},
{
"type": "node",
"tag": "div",
"attributes": { "data-meno-empty": "" },
"children": "No posts match your filters."
}
]
}Blog Post Template JSON
The template page in pages/templates/blog-post.json uses {{cms.*}} expressions that are replaced with each item's data at build time:
{
"meta": {
"title": "{{cms.title}} | Blog",
"description": "{{cms.excerpt}}",
"source": "cms",
"cms": {
"id": "blog",
"name": "Blog Posts",
"slugField": "slug",
"urlPattern": "/blog/{{slug}}",
"fields": {
"title": { "type": "string", "required": true },
"slug": { "type": "string", "required": true },
"excerpt": { "type": "text" },
"content": { "type": "rich-text" },
"coverImage": { "type": "image" },
"author": { "type": "string", "default": "Admin" },
"publishedAt": { "type": "date" },
"category": {
"type": "select",
"options": ["Technology", "Design", "Business", "Lifestyle"]
},
"featured": { "type": "boolean", "default": false }
}
}
},
"root": {
"type": "node",
"tag": "article",
"children": [
{
"type": "node",
"tag": "h1",
"children": "{{cms.title}}"
},
{
"type": "node",
"tag": "p",
"children": "By {{cms.author}} on {{cms.publishedAt}}"
},
{
"type": "node",
"tag": "div",
"children": "{{cms.content}}"
}
]
}
}CMS Item File
Each blog post saved via the CMS panel is stored as a JSON file at cms/blog/{filename}.json:
{
"_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"_filename": "getting-started-with-static-sites",
"_createdAt": "2025-03-15T10:00:00Z",
"_updatedAt": "2025-03-15T10:30:00Z",
"title": "Getting Started with Static Sites",
"slug": "getting-started-with-static-sites",
"excerpt": "Learn why static HTML is faster, more secure, and easier to deploy.",
"content": "<h2>Why Static?</h2><p>Static sites load instantly...</p>",
"coverImage": "/images/static-sites.jpg",
"author": "Jane Doe",
"publishedAt": "2025-03-15",
"category": "Technology",
"featured": true
}What You Built
By following this tutorial, you created:
A CMS collection with 9 fields covering all common blog content needs.
A blog index page with a responsive post grid, server-rendered from CMS data.
Client-side filtering with category buttons, keyword search, results count, and empty state -- all powered by MenoFilter data attributes with no custom JavaScript.
A blog post template that generates one HTML page per post, each with its own SEO metadata.
Static HTML output that can be deployed anywhere -- no server, no database, no runtime dependencies.
Next Steps
Building Components -- Extract the post card into a reusable component.
Styling -- Polish the blog layout with responsive styles and theme colors.
CMS -- Add reference fields to link posts to an authors collection.
Build a Product Catalog -- Apply the same CMS and filtering patterns to a product catalog with price ranges, sorting, and pagination.