MenoFilter API
Meno gives you two complementary ways to narrow a collection of items. At build time, you query a collection with getCollectionList(...) (or filter an already-fetched array with queryList(...)) so the page ships with exactly the items you want. At runtime, MenoFilter — a small client-side library — adds live filtering, search, sort, and pagination on top of the rendered list, driven entirely by data-* attributes.
This page is the precise reference for both. For the task-oriented walkthrough, see the Filtering and Search guide. For collection-backed lists in general, see Lists; for the underlying field schema, see CMS.
Build-time query helpers
These run in a page's frontmatter (the --- block), import from meno-astro, and return a plain array you map over in markup. The result is static HTML — nothing ships to the browser unless you also wire MenoFilter.
getCollectionList
Resolve a CMS collection to an array of items at build time.
---
import { getCollectionList, style } from 'meno-astro';
import { BaseLayout } from 'meno-astro/components';
const posts = await getCollectionList("blog", {
filter: { field: "published", operator: "eq", value: true },
sort: { field: "publishedAt", order: "desc" },
limit: 6
}, Astro);
---
<BaseLayout meta={meta}>
<div class={style({ base: { display: "grid", gap: "16px" } })}>
{posts.map((blog, blogIndex) => (
<article class={style({ base: { padding: "16px" } })}>
<h3 class={style({ base: { fontSize: "20px" } })}>{blog.title}</h3>
<p>{blog.excerpt}</p>
</article>
))}
</div>
</BaseLayout>The signature is getCollectionList(source, query?, Astro, getCollection?). The source is the collection name (matching the folder under src/content/). It runs at build time and synthesizes _url and _id on each returned item. The loop variable defaults to the singularized collection name (blog for a "blog" source); make your {blog.title} templates match it, or set itemAs in the query to choose a name.
Query options
The optional second argument shapes the result. The same option shape is accepted by getCollectionList and queryList.
Option | Type | Description |
|---|---|---|
| object or object[] | One filter condition or an array of conditions (ANDed) |
| object or object[] | One sort spec or an array applied in order |
|
| Maximum items to return |
|
| Skip the first N items |
|
| Loop variable name (collection lists) |
Filter config shape
A single filter condition is { field, operator, value }. Pass an array to require multiple conditions.
const featured = await getCollectionList("products", {
filter: [
{ field: "category", operator: "eq", value: "electronics" },
{ field: "price", operator: "lte", value: 500 }
]
}, Astro);Field | Description |
|---|---|
| The item field to test |
| One of the operators below (default |
| The value to compare against |
Build-time operators: eq, neq, gt, gte, lt, lte, contains, in.
Sort config shape
A sort spec is { field, order }. order is "asc" (default) or "desc". Pass an array to break ties with successive fields.
sort: [
{ field: "featured", order: "desc" },
{ field: "publishedAt", order: "desc" }
]queryList
queryList(items, query) applies the same filter / sort / limit / offset options to an array you already have in hand — no collection fetch. Use it for a nested list filtered by an outer loop variable, or to re-slice a list you fetched once.
---
import { getCollectionList, queryList } from 'meno-astro';
const categories = await getCollectionList("categories", {}, Astro);
const allProducts = await getCollectionList("products", {}, Astro);
---
{categories.map((category, categoryIndex) => (
<section>
<h2>{category.title}</h2>
{queryList(allProducts, {
filter: { field: "categoryId", operator: "eq", value: category._id },
sort: { field: "price", order: "asc" },
limit: 4
}).map((product, productIndex) => (
<article>{product.title}</article>
))}
</section>
))}Parsing query strings
When a filter or sort comes from a string (a URL parameter, a stored config), use the parsers from meno-astro to turn it into the config objects above.
Helper | Signature | Description |
|---|---|---|
|
| Parse a filter string into a |
|
| Inverse of |
|
| Parse a sort string such as |
|
| Inverse of |
MenoFilter (client runtime)
MenoFilter is a client-side JavaScript library for filtering, sorting, searching, and paginating collection data in the browser. It works two ways that can be combined: declarative wiring through data-* attributes on your markup, and a programmatic JavaScript API. The data-attribute path needs no custom code — Meno auto-creates and wires an instance per data-meno-filter container when the page loads.
Wiring it up
Render the collection at build time, mark the container with data-meno-filter="<collection>", mark the items wrapper with data-meno-list, and give each item the data-* fields you want to filter, search, or sort on. MenoFilter reads those item attributes and re-renders the visible set as the user interacts.
---
import { getCollectionList, style } from 'meno-astro';
import { BaseLayout } from 'meno-astro/components';
const productsList = await getCollectionList("products", {}, Astro);
---
<BaseLayout meta={meta}>
<div class={style({ base: { padding: "40px" } })} data-meno-filter="products" data-meno-per-page="6">
<input
class={style({ base: { width: "100%", padding: "8px 12px" } })}
type="text"
placeholder="Search..."
data-meno-search
data-meno-search-fields="title,description"
/>
<button data-meno-filter-field="category" data-meno-filter-value="*">All</button>
<button data-meno-filter-field="category" data-meno-filter-value="electronics">Electronics</button>
<span data-meno-count="results">0</span> results
<div class={style({ base: { display: "grid", gap: "16px" } })} data-meno-list>
{productsList.map((item, itemIndex) => (
<div
class={style({ base: { border: "1px solid var(--border)", padding: "16px" } })}
data-id={item._id}
data-category={item.category}
data-price={item.price}
data-title={item.title}
data-description={item.description}
>
<h3 class={style({ base: { fontSize: "20px" } })}>{item.title}</h3>
<p>{item.description}</p>
<span class={style({ base: { fontWeight: "bold" } })}>{`$${item.price}`}</span>
</div>
))}
</div>
<div class={style({ base: { textAlign: "center", color: "var(--muted)" } })} data-meno-empty>No products found.</div>
<button data-meno-page="prev">Prev</button>
<span data-meno-page-current>1</span> / <span data-meno-page-total>1</span>
<button data-meno-page="next">Next</button>
</div>
</BaseLayout>Remember rule 1 of the dialect: styles live in style({...}), never in a raw class="..." string. The data-meno-* attributes are plain attributes alongside the class={style(...)}.
Getting an instance
Instances are registered by collection name. Each data-meno-filter container gets one automatically on load; you can also create one in your own component script.
// Get the auto-created instance
const filter = MenoFilter.get('products');
// Or create one programmatically
const filter = new MenoFilter({
collection: 'products',
perPage: 10,
filterMatch: 'and'
});
await filter.init(); // loads data from inline JSON or a static fileConstructor options
Option | Type | Default | Description | |
|---|---|---|---|---|
|
| (required) | Collection name | |
|
|
| Items per page | |
| `"and" \ | "or"` |
| How multiple filter fields combine |
|
|
| Type hints for value coercion | |
|
|
| Sync filter state to URL query params | |
| `"push" \ | "replace"` |
| URL update mode ( |
|
|
| Enable fuzzy text matching | |
|
|
| Fuzzy match threshold (0–1, lower is stricter) |
Static methods
Method | Returns | Description | |
|---|---|---|---|
| `MenoFilter \ | undefined` | Get instance by collection name |
|
| Get all registered instances | |
|
| Check whether an instance exists | |
|
| Destroy and unregister an instance | |
|
| Destroy all instances |
Filtering methods
Method | Returns | Description |
|---|---|---|
|
| Apply filter criteria (AND logic between fields) |
|
| Apply OR logic between criteria objects |
|
| Add or update a single filter (merges with existing) |
|
| Remove the filter for a specific field |
|
| Clear all filters (keeps sort and search) |
|
| Get current filter criteria |
|
| Apply a range filter (uses |
|
| Clear a range filter |
Simple equality:
filter.filter({ category: 'tech' });Multiple conditions (AND):
filter.filter({ category: 'tech', published: true });With operators:
filter.filter({
price: { $gt: 100, $lt: 500 },
tags: { $contains: 'featured' }
});OR filtering:
filter.filterOr([{ category: 'tech' }, { category: 'design' }]);Range filtering:
filter.filterRange('price', { min: 100, max: 500 });
filter.filterRange('date', { min: '2024-01-01' });Filter operators
These operators are the runtime (client) operator set, expressed as $-prefixed keys on a filter value object.
Operator | Description | Value type | |
|---|---|---|---|
| Equal to |
| |
| Not equal to |
| |
| Greater than | `number \ | string` |
| Greater than or equal | `number \ | string` |
| Less than | `number \ | string` |
| Less than or equal | `number \ | string` |
| String contains (case-insensitive) or array includes |
| |
| String does not contain or array excludes |
| |
| String starts with (case-insensitive) |
| |
| String ends with (case-insensitive) |
| |
| Value is in the given array |
| |
| Value is not in the given array |
| |
| Is empty/null ( |
|
A filter value of "*", "", null, or undefined is treated as "show all" and skipped.
Search methods
Method | Returns | Description |
|---|---|---|
|
| Search text across fields (case-insensitive) |
|
| Clear the search query |
|
| Get current search state |
|
| Enable or disable fuzzy matching |
|
| Check whether fuzzy search is on |
// Search all string fields
filter.search('react tutorial');
// Search specific fields only
filter.search('react', ['title', 'tags']);
// Enable fuzzy matching
filter.setFuzzySearch(true, 0.3);
filter.search('reckt'); // matches "react" with typo toleranceSort methods
Method | Returns | Description | |
|---|---|---|---|
|
| Sort by field ( | |
|
| Clear sorting | |
| `SortConfig \ | null` | Get the current sort configuration |
filter.sort('publishedAt', 'desc');
filter.sort('title', 'asc');
filter.clearSort();The runtime sort config is { field, order }, the same shape the build-time sort option uses.
Pagination methods
Method | Returns | Description |
|---|---|---|
|
| Go to a page number |
|
| Go to the next page |
|
| Go to the previous page |
|
| Set items per page |
|
| Get the pagination state |
PageInfo fields: current (1-based page), total (page count), hasNext, hasPrev, totalItems (filtered count), perPage.
filter.setPerPage(12);
filter.setPage(2);
const info = filter.getPageInfo();
// { current: 2, total: 5, hasNext: true, hasPrev: true, totalItems: 58, perPage: 12 }Load-more methods
An alternative to pagination where items are appended incrementally.
Method | Returns | Description |
|---|---|---|
|
| Load more items (defaults to |
|
| Get the currently visible items |
|
| Get the load-more state |
|
| Initialize load-more mode |
LoadMoreInfo fields: visible, total, remaining, hasMore.
filter.initLoadMore(6);
filter.loadMore(); // shows 6 more
filter.loadMore(3); // shows 3 more
const info = filter.getLoadMoreInfo();
// { visible: 15, total: 50, remaining: 35, hasMore: true }Data access methods
Method | Returns | Description | |
|---|---|---|---|
|
| All items (unfiltered, unsorted) | |
|
| Filtered items (before pagination) | |
|
| Current page items | |
| `CMSItem \ | undefined` | Find an item by |
|
| All unique values for a field | |
|
| Value counts from the filtered items | |
|
| Value counts from all items | |
|
| Full state snapshot |
const categories = filter.getUniqueValues('category');
// ['tech', 'design', 'marketing']
const facets = filter.getFacets('category');
// { tech: 12, design: 8, marketing: 3 }Reset
Method | Returns | Description |
|---|---|---|
|
| Reset all filters, search, sort, and pagination |
Events
Subscribe to lifecycle events with on(). It returns an unsubscribe function.
const unsubscribe = filter.on('afterFilter', (items) => {
console.log('Filtered to', items.length, 'items');
});
unsubscribe(); // laterEvent | Callback data | Description |
|---|---|---|
|
| Fired after data is loaded |
|
| Before filter criteria are applied |
|
| After filtering completes |
|
| Before sort is applied |
|
| After sorting completes |
|
| Before search is applied |
|
| After search completes |
| varies | Before DOM render |
| varies | After DOM render |
|
| When the page changes |
|
| When more items are loaded |
|
| When |
You can also watch the full state or subscribe to the current items only:
const unwatch = filter.watch((state) => {
console.log(state.pageItems.length, 'items on page', state.page.current);
});
const unsub = filter.subscribe((items) => {
console.log(items.length, 'current items');
});URL sync
Synchronize filter state with URL query parameters so users can share filtered views. Enable it with the urlSync constructor option, the data-meno-url-sync attribute, or at runtime:
filter.enableUrlSync({ mode: 'push' }); // adds browser history entries
filter.enableUrlSync(); // replaces the URL without history
filter.disableUrlSync();URL parameter format:
Filters:
?filter.category=tech&filter.price.$gt=100Sort:
?sort=publishedAt:descSearch:
?search=react&search.fields=title,tagsPage:
?page=2&perPage=12
Data attribute reference
Use these HTML attributes for declarative filtering. They sit alongside class={style(...)} on the relevant elements.
Container attributes
Attribute | Description | |
|---|---|---|
| Root filter container; value is the collection name | |
| Marks the element containing list items | |
| Items per page | |
`data-meno-filter-match="and\ | or"` | How multiple filters combine |
| Debounce delay for inputs | |
| Class added to active filter buttons | |
| JSON object of field type hints | |
| Enable URL query parameter sync |
Filter controls
Attribute | Description | |
|---|---|---|
| Field this control filters on | |
| Value to filter by (on buttons/links) | |
`data-meno-filter-mode="single\ | multi"` | Single or multi-select mode |
| Clears all filters when clicked | |
| Resets all state (filters, sort, search, page) |
Range controls
Attribute | Description | |
|---|---|---|
| Marks a range filter container | |
`data-meno-range-bound="min\ | max"` | Marks an input as the min or max bound |
Search controls
Attribute | Description |
|---|---|
| Marks a search input |
| Comma-separated fields to search |
| Enable fuzzy matching |
| Fuzzy match threshold |
Sort controls
Attribute | Description | |
|---|---|---|
| Sort by this field when clicked | |
`data-meno-sort-order="asc\ | desc"` | Sort direction |
A <select> marked with data-meno-sort reads field:order from its option values (for example price:asc, publishedAt:desc).
Pagination controls
Attribute | Description | ||
|---|---|---|---|
`data-meno-page="prev\ | next\ | n"` | Page navigation button |
| Displays the current page number | ||
| Displays the total page count | ||
| Container for auto-generated page buttons | ||
| Select/input to change items per page |
Load-more controls
Attribute | Description |
|---|---|
| "Load more" button |
| Displays the remaining item count |
Display elements
Attribute | Description | ||
|---|---|---|---|
`data-meno-count="results\ | total\ | visible"` | Shows filtered, total, or visible item count |
| Shows facet counts for a field | ||
| Shown when no items match the filters |
A data-meno-facet value may target a single value with field:value (for example data-meno-facet="category:electronics") to show the count for just that value.
Build-time querying and the MenoFilter runtime compose cleanly: render the full set (or a pre-narrowed slice) with getCollectionList / queryList, then layer data-meno-* controls on top for live interaction. See the Filtering and Search guide for end-to-end patterns, Lists for collection rendering, and CMS for defining the fields you filter on.