Tutorials

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.

  1. Click the CMS tab in the left sidebar. You will see an empty collections list if this is a fresh project.

  2. Click the + button to create a new collection.

  3. In the dialog that appears, set the ID to blog and the Display Name to Blog Posts.

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

  1. Set the Slug Field to slug. This tells Meno which field to use when building URLs.

  2. Set the URL Pattern to /blog/{{slug}}. Each post will be accessible at a path like /blog/my-first-post.

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

  1. In the CMS tab, click the Blog Posts collection to expand it.

  2. Click the + button next to the collection name to create a new item.

  3. 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: Choose Technology from the dropdown. - featured: Toggle on.

  4. Click Save. The item appears in the collection list.

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

  1. Switch to the Pages tab in the left sidebar.

  2. Click + to create a new page. Name it blog.

  3. The page opens in the canvas. You see an empty root div.

Add the page header

  1. With the root div selected, press Cmd+E to open the Command Palette.

  2. Search for Navigation and add it. This places your site navigation at the top.

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

  4. Inside the Section, add a Heading element. Set its text to Blog.

  5. Add a Text element below the heading. Set its text to Thoughts, tutorials, and stories from our team.

Add the CMS List

  1. Select the root div and add another Section below the first one. This section will contain the post grid.

  2. Select the new Section and press Cmd+E. Search for List and select the CMS List option.

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

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

  2. Inside the link, add an img element. Set its src attribute to {{item.coverImage}} and alt to {{item.title}}.

  3. Below the image (still inside the link), add an h3 element. Set its text content to {{item.title}}.

  4. Add a p element below the heading. Set its text to {{item.excerpt}}.

  5. Add another p element for metadata. Set its text to {{item.author}} -- {{item.publishedAt}}.

Style the layout

  1. Select the List's container element. In the style panel, set display to grid and gridTemplateColumns to 1fr 1fr 1fr. Set gap to 32px.

  2. Switch to the Tablet breakpoint and change gridTemplateColumns to 1fr 1fr.

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

  1. Click the page background to deselect all elements. The Properties panel shows page-level settings.

  2. Set title to Blog | My Site.

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

  1. In the Pages tab, navigate to templates/ and click blog-post to open it.

  2. The canvas shows the template page. You will see placeholder content with {{cms.fieldName}} expressions.

Build the post layout

  1. Clear the existing content if needed. Add a Navigation component at the top.

  2. Add a Section below the Navigation for the hero area.

  3. Inside the Section, add an img element. Set src to {{cms.coverImage}} and alt to {{cms.title}}. Style it full-width.

  4. Add an h1 element below the image. Set its content to {{cms.title}}.

  5. Add a p element for the byline: By {{cms.author}} on {{cms.publishedAt}}.

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

  1. Add another Section below the hero for the post body.

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

  3. Style the content div with a comfortable reading width: maxWidth 720px, margin 0 auto, lineHeight 1.8.

Configure page metadata

  1. Click the page background to open page-level settings.

  2. Set title to {{cms.title}} | Blog. Each generated page will have the post title in its browser tab.

  3. Set description to {{cms.excerpt}}. Search engines will show the excerpt as the page snippet.

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

  1. Go back to the blog index page (not the template).

  2. Select the outermost element of the card template inside the CMS List (the link wrapper you created in Step 3).

  3. In the Properties panel, open the Attributes section.

  4. Add a custom attribute: data-category with value {{item.category}}.

  5. Add another: data-featured with 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

  1. Select the Section that contains the CMS List.

  2. In the Attributes section, add data-meno-filter with value blog.

  3. Add data-meno-url-sync with value true. This syncs the active filters to the URL, so visitors can share filtered views.

Add filter buttons

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

  2. Inside the div, add a button element with text All. Add the attribute data-meno-clear (empty value). This button clears all active filters.

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

  1. Above the filter buttons (or alongside them), add an input element with type="text" and placeholder="Search posts...".

  2. Add the attribute data-meno-search (empty value).

  3. Add data-meno-search-fields with value title,excerpt. This tells MenoFilter which data attributes to search through.

Add a results count and empty state

  1. Below the filter buttons, add a span element with the attribute data-meno-count set to results. MenoFilter will fill this with the number of matching posts (e.g. "4").

  2. Add a text node next to it that reads posts found so the full text reads "4 posts found".

  3. Below the CMS List, add a div with attribute data-meno-empty. Set its content to No 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.

  1. Open a terminal and run:

``bash bun run build ``

  1. 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 ``

  1. Each blog post has its own directory with an index.html file, giving you clean URLs like /blog/getting-started-with-static-sites.

  1. Open dist/blog/index.html in 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

  1. Click any post card to navigate to its individual page. Verify the title, cover image, content, and metadata render correctly.

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

Building the future of digital experiences, one website at a time

© 2026 Company. All rights reserved.