Client-Side Filtering and Search
Meno gives you two layers for narrowing a collection list, and they solve different problems. Build-time querying runs while astro build generates the page: you pass query options to getCollectionList() or run queryList() over an already-fetched list, and the output is plain HTML with no JavaScript. Client-side filtering runs in the visitor's browser: you render the full list, add filter and search controls, and the MenoFilter runtime shows and hides items live as the visitor types or clicks — without a page reload.
Reach for build-time querying when the result is fixed at publish time (a "featured" section, the five newest posts, one category's items). Reach for MenoFilter when the visitor needs to drive the filtering interactively (a searchable product grid, category tabs, a price-range slider).
Build-time querying
A collection list is a frontmatter getCollectionList() const mapped in the body. See Lists for the full pattern; this section covers the query options that narrow it.
getCollectionList(source, query, Astro, getCollection) accepts a query object with filter, sort, limit, offset, and items. The list is resolved once, at build time:
---
import { getCollection } from 'astro:content';
import { getCollectionList } from 'meno-astro';
const featuredList = await getCollectionList("posts", {
filter: { field: "featured", operator: "eq", value: true },
sort: { field: "publishedAt", order: "desc" },
limit: 3
}, Astro, getCollection);
---
<section>
{featuredList.map((post, postIndex) => (
<article>{i18n(post.title)}</article>
))}
</section>A filter is a single { field, operator, value } object. operator is the comparison (for example eq for equality). sort is { field, order } where order is "asc" or "desc". limit caps the number of items and offset skips the first N. Pass items to restrict the list to specific ids (useful for "related" sections driven by a reference field).
Because the items getCollectionList() returns are raw CMS data, render their fields through i18n(...) (see CMS) — {i18n(post.title)}, not a bare {post.title}.
queryList: narrowing an already-fetched list
When you already have a list in hand and want to derive a narrower one — for instance, fetch a collection once and then map a filtered subset per group — use queryList(items, query). It applies the same filter / sort / limit in memory, with no extra fetch. This docs site uses it to render the per-category links in its sidebar:
---
const docsList = await getCollectionList("docs", {}, Astro, getCollection);
---
{queryList(docsList, {
filter: { field: "category", operator: "eq", value: category._id },
sort: { field: "order", order: "asc" }
}).map((doc, docIndex) => (
<Link href={i18n(doc._url)}>{i18n(doc.title)}</Link>
))}queryList is the right tool inside an outer loop: fetch the parent collection once, then filter it by the current loop variable for each group. Both getCollectionList's query argument and queryList's query share the same shape, so you can move logic between them freely.
Client-side filtering and search
For interactive filtering, you render the whole list once and let MenoFilter show and hide items in the browser. MenoFilter ships with Meno and is driven entirely by data- attributes — no JavaScript to write. It initializes automatically on page load, scoped to each container it finds.
The pattern has three parts:
A container element marked
data-meno-filter="<collection>"that scopes one filter instance.Inside it, the rendered list — its wrapper marked
data-meno-list, and each item carrying its values asdata-<field>attributes so the runtime can match against them.The controls — search inputs, filter buttons, sort selects, pagination — also marked with
data-attributes.
When the visitor interacts with a control, MenoFilter reads the active criteria, matches them against each item's data-<field> attributes, and toggles visibility. Because the markup is already rendered, the page never reloads.
Rendering the list
Render the collection list normally, then add the data-meno-filter container, the data-meno-list wrapper, and a data-<field> attribute per filterable field on each item. In meno-astro, this is a <div class={style({…})}> with the data attributes alongside the class:
---
import { getCollectionList, style } from 'meno-astro';
import { BaseLayout } from 'meno-astro/components';
const productsList = await getCollectionList("products", { emitTemplate: true }, Astro, getCollection);
---
<BaseLayout meta={meta}>
<div class={style({ base: { maxWidth: "1100px", margin: "0 auto" } })} 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" />
<div class={style({ base: { display: "flex", gap: "8px" } })}>
<button class={style({ base: { padding: "8px 12px" } })} data-meno-filter-field="category" data-meno-filter-value="*">All</button>
<button class={style({ base: { padding: "8px 12px" } })} data-meno-filter-field="category" data-meno-filter-value="apparel">Apparel</button>
<button class={style({ base: { padding: "8px 12px" } })} data-meno-filter-field="category" data-meno-filter-value="home">Home</button>
</div>
<div class={style({ base: { display: "grid", gap: "16px" } })} data-meno-list>
{productsList.map((product, productIndex) => (
<div class={style({ base: { border: "1px solid", borderColor: "var(--border)", padding: "16px" } })} data-id={i18n(product._id)} data-category={i18n(product.category)} data-title={i18n(product.title)} data-description={i18n(product.description)}>
<h3 class={style({ base: { fontSize: "20px" } })}>{i18n(product.title)}</h3>
<p class={style({ base: { color: "var(--text-light)" } })}>{i18n(product.description)}</p>
</div>
))}
</div>
<div class={style({ base: { padding: "40px", textAlign: "center" } })} data-meno-empty>No products match your filters.</div>
</div>
</BaseLayout>Pass emitTemplate: true to getCollectionList() when you want MenoFilter to render past the server-rendered set: it emits a hidden <template> the runtime uses to add more matching items in the browser without a reload — handy with pagination or "Load More" over a large collection.
Container attributes
The outermost wrapper carries the instance-wide settings:
Attribute | Description |
|---|---|
| Required. Scopes all controls inside to this instance; value is the collection name. |
| Marks the element that contains the filterable items. |
| Items per page. |
| Logic between active filter fields: |
| Debounce delay for the search input. Default |
| JSON map of field types for correct comparison. |
| Sync filter state to URL query parameters. |
Filter controls
Mark any clickable element with data-meno-filter-field and data-meno-filter-value. Clicking toggles that value; a value of * (or empty) means "show all" and clears the field:
<button class={style({ base: { padding: "8px 12px" } })} data-meno-filter-field="category" data-meno-filter-value="apparel">Apparel</button>The same attributes work on checkboxes (multiple checked boxes for one field combine with OR), radio buttons, and <option>s inside a data-meno-filter-field select. Wrap a group of buttons in an element with data-meno-filter-mode="single" for radio-like behavior, or "multi" for multi-select.
Search
Mark a text input with data-meno-search. MenoFilter does case-insensitive substring matching across the listed fields (or all string fields if you omit data-meno-search-fields):
<input class={style({ base: { width: "100%", padding: "8px 12px" } })} type="text" placeholder="Search…" data-meno-search data-meno-search-fields="title,description" />Add data-meno-search-fuzzy to tolerate typos, tuned by data-meno-search-threshold (0 to 1, lower is stricter; default 0.3). Search respects the container's data-meno-debounce.
Sorting, ranges, and pagination
Sort with a button (data-meno-sort="title", optional data-meno-sort-order="desc") or a select whose option values are field:order (for example price:desc). For numeric or date ranges, pair two inputs sharing data-meno-range="price" with data-meno-range-bound="min" and "max" — and declare the field's type in data-meno-types so comparisons are numeric, not string:
<div class={style({ base: { display: "flex", gap: "8px" } })}>
<input class={style({ base: { width: "100%", padding: "8px 12px" } })} type="number" placeholder="Min $" data-meno-range="price" data-meno-range-bound="min" />
<input class={style({ base: { width: "100%", padding: "8px 12px" } })} type="number" placeholder="Max $" data-meno-range="price" data-meno-range-bound="max" />
</div>Enable pagination by setting data-meno-per-page on the container, then add data-meno-page="prev" / "next" buttons and data-meno-page-current / data-meno-page-total display spans. For an append-style flow, use a data-meno-load-more button instead.
Display and reset elements
Empty, count, and facet elements update automatically as filters change:
Attribute | Description |
|---|---|
| Shown only when no items match. |
| Displays the filtered item count ( |
| Displays how many items match a field value. |
| Clears all filters (keeps sort and search). |
| Resets everything: filters, search, sort, and pagination. |
Going further with JavaScript
MenoFilter is also a programmatic API you can drive from a component's <script> (see JavaScript) — MenoFilter.get("products") returns the instance, with methods like filter(), search(), sort(), getFacets(), and lifecycle events. The data-attribute setup above covers most pages; reach for the API for custom controls or to react to filter changes. The full method, operator, event, and attribute list is in the MenoFilter API reference.
When to use which
Build-time querying | Client-side | |
|---|---|---|
Runs | During | In the visitor's browser |
JavaScript | None shipped |
|
Result | Fixed at publish | Changes live per interaction |
Best for | Featured sections, newest-N, related items | Searchable grids, category tabs, ranges |
Tools |
|
|
The two layers compose. A common pattern is to query at build time to set the scope — say, only published items in one category — and then let MenoFilter handle interactive search and sorting within that rendered set.
Related: Lists for rendering collection lists, CMS for defining the collections you filter, JavaScript for driving the runtime, and the MenoFilter API reference for the complete attribute and method surface.