Build a Product Catalog with Meno
Create a product catalog with filterable listings, price range controls, sorting, pagination, and individual product pages -- all generating static HTML from CMS data.
Prerequisites: Meno is running at localhost:3000. You have completed the Quickstart. Familiarity with the Build a Blog tutorial is helpful but not required.
What you will build:
A "products" CMS collection with structured product data
A product listing page with category filters, price range, search, sort, and pagination
Individual product pages generated from a template
Static HTML with client-side interactivity powered by MenoFilter
Step 1: Create the Products Collection
Click the CMS tab in the left sidebar.
Click the + button to create a new collection.
Set the ID to
productsand the Display Name toProducts.Add the following fields:
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| name | string | Yes | Product name |
| slug | string | Yes | URL identifier (e.g. wireless-headphones) |
| description | text | No | Detailed product description |
| price | number | No | Price in dollars |
| image | image | No | Product photo |
| category | select | No | Options: Electronics, Clothing, Accessories, Home |
| inStock | boolean | No | Default value: true |
| rating | number | No | Star rating from 1 to 5 |
Set the Slug Field to
slug.Set the URL Pattern to
/products/{{slug}}.Click Create.
Meno creates the template page at pages/templates/product.json and the data folder at cms/products/.
Step 2: Add Products
Populate the catalog with enough items to make filtering and pagination meaningful.
Add items manually
In the CMS tab, click Products to expand the collection.
Click + to create a new item.
Fill in the fields: - name:
Wireless Headphones- slug:wireless-headphones- description:Premium noise-cancelling headphones with 30-hour battery life.- price:149- image: Select an image or type/images/headphones.jpg. - category:Electronics- inStock: Toggle on. - rating:4Click Save.
Add at least 8-10 more products across different categories and price points. Here are some examples:
| Name | Category | Price | Rating | |------|----------|-------|--------| | Cotton T-Shirt | Clothing | 29 | 5 | | Leather Wallet | Accessories | 59 | 4 | | Desk Lamp | Home | 89 | 3 | | Bluetooth Speaker | Electronics | 79 | 5 | | Running Shoes | Clothing | 119 | 4 | | Watch Band | Accessories | 25 | 3 | | Throw Pillow | Home | 39 | 4 | | USB-C Hub | Electronics | 45 | 5 | | Sunglasses | Accessories | 69 | 4 | | Scented Candle | Home | 19 | 5 |
Bulk import with CSV
If you have product data in a spreadsheet, use CSV import instead of manual entry.
Click the Import button at the top of the products item list.
Select your CSV file. Column headers must match the field names:
name,slug,description,price,image,category,inStock,rating.Each row creates one CMS item. Review the imported items in the list.
Step 3: Build the Product Listing Page
Create the page
Switch to the Pages tab and click +. Name the page
products.The page opens with an empty root div.
Add the page header
With the root div selected, press Cmd+E and add a Navigation component.
Add a Section below the Navigation. Inside it, add a Heading with text
Our Products.Add a Text element below the heading:
Browse our collection of quality products.
Add the filter container
Select the root div and add another Section below the header section. This will hold all the filters, the product grid, and pagination controls.
Select the new Section. In the Attributes panel, add: -
data-meno-filter=products-data-meno-per-page=8-data-meno-types={"price":"number","rating":"number"}-data-meno-url-sync=true
The data-meno-types attribute tells MenoFilter to treat price and rating as numbers rather than strings. This is essential for range filters and numeric sorting to work correctly.
Add the product grid
Inside the filter Section, add a List element (CMS List).
Configure the List in the Properties panel: - Source Type:
collection- Source:productsInside the List, build the product card template: - Add a div as the card wrapper. - In the card's Attributes, add: -
data-category={{item.category}}-data-price={{item.price}}-data-rating={{item.rating}}-data-name={{item.name}}-data-description={{item.description}}-data-in-stock={{item.inStock}}- Inside the card, add a link element with href set to/products/{{item.slug}}. - Inside the link, add an img with src ={{item.image}}and alt ={{item.name}}. - Below the image, add an h3 with content{{item.name}}. - Add a span with content${{item.price}}for the price. - Add a span with content{{item.category}}. Style it as a small badge with a background color and border-radius.
Style the grid
Select the List container element. Set display to
grid, gridTemplateColumns torepeat(4, 1fr), and gap to24px.At the Tablet breakpoint, change gridTemplateColumns to
repeat(2, 1fr).At the Mobile breakpoint, change it to
1fr.
Step 4: Add Filtering and Search
Now add the interactive controls above the product grid. All of these work through MenoFilter data attributes -- no JavaScript to write.
Add the search bar
Inside the filter Section, above the CMS List, add a div for the controls area. Style it with display
flex, flexDirectioncolumn, and gap16px.Inside the controls div, add an input element.
Set its attributes: -
type=text-placeholder=Search products...-data-meno-search(empty value) -data-meno-search-fields=name,description
Visitors can now type to filter products by name or description in real time.
Add category filter buttons
Below the search input, add a div for the category buttons. Style it as a horizontal flex row with gap
8pxand flexWrapwrap.Add a "show all" button: - Element: button - Text:
All- Attribute:data-meno-clear(empty value)Add one button for each category:
| Button Text | Attributes |
|-------------|------------|
| Electronics | data-meno-filter-field="category" data-meno-filter-value="Electronics" |
| Clothing | data-meno-filter-field="category" data-meno-filter-value="Clothing" |
| Accessories | data-meno-filter-field="category" data-meno-filter-value="Accessories" |
| Home | data-meno-filter-field="category" data-meno-filter-value="Home" |
MenoFilter adds an active class to the currently selected button. Style the default and active states so users can see which filter is applied.
Add price range inputs
Below the category buttons, add a div for the price controls. Style it as a flex row with a label.
Add a span with text
Price:as the label.Add two input elements for the minimum and maximum price:
Min price input:
type=numberplaceholder=Mindata-meno-range=pricedata-meno-range-bound=min
Max price input:
type=numberplaceholder=Maxdata-meno-range=pricedata-meno-range-bound=max
Visitors can enter a minimum and/or maximum price. MenoFilter compares these against each item's data-price attribute using the numeric type coercion you configured in Step 3.
Add sort controls
Next to or below the price inputs, add a select dropdown for sorting.
Set the attribute
data-meno-sort(empty value) on the select element.Add option elements inside it:
| Option Text | Value |
|-------------|-------|
| Sort by... | (empty -- this is the default/no-sort option) |
| Name A-Z | name:asc |
| Name Z-A | name:desc |
| Price: Low to High | price:asc |
| Price: High to Low | price:desc |
| Top Rated | rating:desc |
The value format is field:direction. MenoFilter reads the selected option and re-orders the items accordingly. Because you set data-meno-types with "price":"number" and "rating":"number", sorting compares numeric values rather than alphabetical strings.
Add a results count and empty state
Below the controls area and above the product grid, add a p element.
Inside it, add a span with attribute
data-meno-countset toresults. MenoFilter populates this with the count of matching items.Add text next to it:
products found.Below the CMS List, add a div with attribute
data-meno-empty(empty value). Set its content toNo products match your filters. Try adjusting your search or price range.
This div is hidden when there are results and automatically shown when the filtered list is empty.
Step 5: Add Pagination
You already set data-meno-per-page="8" on the filter container in Step 3. Now add navigation controls so visitors can browse through pages.
Option A: Page navigation buttons
Below the CMS List (but still inside the filter Section), add a div for the pagination bar. Style it as a centered flex row with gap
16px.Add a button with text
Previousand attributedata-meno-page=prev. MenoFilter disables this button automatically when the user is on the first page.Add a span to show the current position. Inside it, place: - A span with attribute
data-meno-page-current(MenoFilter fills in the current page number) - The text/- A span with attributedata-meno-page-total(MenoFilter fills in the total page count)Add a button with text
Nextand attributedata-meno-page=next. Automatically disabled on the last page.
Option B: Load More button
If you prefer a "load more" pattern instead of page numbers, replace the pagination bar with a single button.
Remove the pagination div from Option A (if you added it).
Below the CMS List, add a button with text
Load More Products.Set the attribute
data-meno-load-more=8. Each click reveals 8 more items.MenoFilter automatically hides this button when all items are visible.
To show how many items are left, add a span inside the button with attribute
data-meno-remaining. The button text becomes:Load More (X remaining).
Choose either Option A or Option B -- they are mutually exclusive. Pagination resets the view to a new page; Load More appends items to the current view.
Add a per-page selector (optional)
If you chose Option A, you can add a dropdown that lets visitors choose how many items to show per page.
Add a select element with attribute
data-meno-per-page-select(empty value).Add option elements:
8,16,24,All. TheAlloption should have value0.
Step 6: Create the Product Detail Template
Each product gets its own page generated from the template.
In the Pages tab, navigate to templates/ and click product to open it.
Build the product page layout:
Hero section
Add a Navigation component at the top.
Add a Section below it. Inside, create a two-column layout: - Set the Section's inner div to display
flex, gap48px. - Add a div on the left for the image. Add an img with src ={{cms.image}}and alt ={{cms.name}}. Style it with width100%and borderRadius8px. - Add a div on the right for the product info.
Product info
Inside the right column div, add: - An h1 with content
{{cms.name}}. - A span with content{{cms.category}}. Style it as a category badge. - A p with content${{cms.price}}. Style it with a large font size and bold weight. - A p with content{{cms.description}}. - A span showing the rating:Rating: {{cms.rating}} / 5.
Responsive adjustments
At the Tablet breakpoint, change the two-column flex to flexDirection
columnso the image stacks above the info.
Page metadata
Click the page background to open page-level settings.
Set title to
{{cms.name}} | Products.Set description to
{{cms.description}}.Verify the cms configuration: - id:
products- slugField:slug- urlPattern:/products/{{slug}}
Step 7: Build and Deploy
Run the build:
``bash
bun run build
``
Check the
dist/folder:
``
dist/
products/
index.html # Product listing page
wireless-headphones/
index.html # Individual product page
cotton-t-shirt/
index.html
leather-wallet/
index.html
... (one folder per product)
sitemap.xml
robots.txt
``
Open
dist/products/index.htmlin a browser and verify each feature: - Search: Type "headphones" -- only matching products appear. - Category filters: Click "Electronics" -- the grid shows only electronics. The button receives theactiveclass. - Price range: Enter 20 in min and 80 in max -- products outside that range disappear. - Sort: Select "Price: Low to High" -- the grid re-orders by price. - Pagination: If you have more than 8 products, the page buttons appear. Navigate forward and back. - Empty state: Apply conflicting filters (e.g. category "Electronics" with max price $10) -- the "No products match" message appears. - URL sync: After applying filters, check the browser URL. It contains query parameters like?category=Electronics&price_min=20. Copy the URL, open it in a new tab, and the same filters are applied.
Click a product card to visit its detail page. Verify the name, image, price, description, and rating render correctly.
Deploy the
dist/folder:
```bash # Netlify netlify deploy --dir=dist --prod
# Cloudflare Pages npx wrangler pages deploy dist
# Or preview locally bunx serve dist ```
Under the Hood
Product Card with Data Attributes
Each product card inside the CMS List carries data-* attributes that MenoFilter reads for filtering. At build time, {{item.price}} becomes the actual value (e.g. 149), so the rendered HTML contains <div data-price="149">. MenoFilter coerces it to a number (via data-meno-types) and compares it against range inputs.
{
"type": "list",
"sourceType": "collection",
"source": "products",
"children": [
{
"type": "node",
"tag": "div",
"attributes": {
"data-category": "{{item.category}}",
"data-price": "{{item.price}}",
"data-rating": "{{item.rating}}",
"data-name": "{{item.name}}"
},
"children": [
{
"type": "link",
"href": "/products/{{item.slug}}",
"children": [
{ "type": "node", "tag": "img", "attributes": { "src": "{{item.image}}", "alt": "{{item.name}}" } },
{ "type": "node", "tag": "h3", "children": "{{item.name}}" },
{ "type": "node", "tag": "span", "children": "${{item.price}}" }
]
}
]
}
]
}Product Template Page
The template at pages/templates/product.json uses {{cms.*}} expressions replaced per item at build time:
{
"meta": {
"title": "{{cms.name}} | Products",
"description": "{{cms.description}}",
"source": "cms",
"cms": {
"id": "products",
"name": "Products",
"slugField": "slug",
"urlPattern": "/products/{{slug}}",
"fields": {
"name": { "type": "string", "required": true },
"slug": { "type": "string", "required": true },
"description": { "type": "text" },
"price": { "type": "number" },
"image": { "type": "image" },
"category": { "type": "select", "options": ["Electronics", "Clothing", "Accessories", "Home"] },
"inStock": { "type": "boolean", "default": true },
"rating": { "type": "number" }
}
}
},
"root": {
"type": "node",
"tag": "div",
"children": [
{ "type": "node", "tag": "h1", "children": "{{cms.name}}" },
{ "type": "node", "tag": "p", "children": "${{cms.price}}" },
{ "type": "node", "tag": "p", "children": "{{cms.description}}" }
]
}
}How MenoFilter Works at Runtime
MenoFilter is a lightweight client-side script that Meno automatically includes in built pages that use data-meno-filter. Here is the sequence:
On page load, MenoFilter finds all elements with
data-meno-filterand initializes a filter instance for each.It discovers the list items (children of
data-meno-listor the CMS List output), filter buttons, search inputs, range inputs, sort controls, and pagination elements.When a visitor interacts with any control, MenoFilter reads the DOM state, applies filters to the item set, sorts and paginates the result, and shows/hides items by toggling
display: none.Count elements, page indicators, and empty states update automatically.
If
data-meno-url-syncis enabled, filter state is written to URL query parameters and read back on page load.
No server round-trips occur. Everything happens in the browser against the pre-rendered HTML.
What You Built
By following this tutorial, you created:
A products CMS collection with 8 fields covering name, price, category, stock status, and rating.
A product listing page with a responsive grid that renders all products at build time.
Client-side filtering with category buttons, a price range slider, keyword search, and sorting -- all declarative via data attributes, no custom JavaScript.
Pagination (or Load More) to handle large catalogs without overwhelming the page.
Individual product pages generated from a template, each with its own URL and SEO metadata.
URL sync so filtered views are bookmarkable and shareable.
Static HTML output that works on any hosting provider with zero server dependencies.
Next Steps
CMS -- Add reference fields to link products to a "brands" or "collections" collection.
Building Components -- Extract the product card into a reusable component with variant styles.
Styling -- Add hover effects to product cards and style the active filter states.
Build a Blog -- Apply the same patterns to a content-driven blog.