Combine published-only filtering, tag slugs, search terms, and pagination while understanding the released search implementation and its scaling limits.

A custom blog index needs a consistent definition of its result set. Apply publication status, tag selection, and search before deciding which page to show. Otherwise, a page can contain fewer results than expected or expose records that the public index should exclude.
BTST's Blog operation supports these filters together. This guide describes the released @btst/stack@3.0.2 behavior and the tradeoffs to consider before building a custom search page in Next.js, TanStack Start, or React Router.
For a user-facing server request, use myStack.forRequest(request).operations.blog.listPosts(...). It runs input validation and the operation's authorization and lifecycle behavior. Your configured server policy must permit the intended public read.
For a public index, set published: true in application code. Do not forward an arbitrary visitor-supplied published value. Leaving the field unspecified is not a substitute for a public-content policy.
The operation accepts:
| Field | Meaning in this release |
|---|---|
published | Filter on publication state |
tagSlug | Match a stored tag slug |
query | Case-insensitive substring search over title, body, or excerpt |
offset | Nonnegative integer result offset |
limit | Integer page size from 1 through 100 |
slug | Match one exact post slug |
The search string is limited to 200 characters. It is not a relevance-ranked search engine: the implementation uses substring matching, not stemming, typo correction, or semantic search.
The following helper accepts an offset, search query, and tag slug while fixing public visibility and page size:
import { PostListQuerySchema } from "@btst/stack/plugins/blog/api";
export function readBlogFilters(url: URL) {
return PostListQuerySchema.parse({
published: true,
limit: 12,
offset: url.searchParams.get("offset") ?? "0",
query: url.searchParams.get("q")?.trim() || undefined,
tagSlug: url.searchParams.get("tag") || undefined,
});
}
Inside your existing server handler, pass that result to the request-scoped Blog operation. Handle a validation failure as invalid input according to your framework's response conventions; do not turn it into a successful response containing an unrestricted list.
Use the operation's items for the current page and total for the total number of matching records. The next offset is the current offset plus the page size. A next-page link is needed only when that offset is below total. Preserve q and tag when constructing pagination URLs with URLSearchParams.
When the visitor changes a search term or tag, reset the offset to zero. An offset that existed in a broader result set can legitimately return an empty page after a filter narrows it.
Without a search term, the released getter pushes publication state, exact slug, and tag membership filters into the adapter, then applies limit and offset there. It counts the matching records separately. An unknown tag returns an empty result.
With a search term, the getter retrieves the records matching the other filters, performs the text search in application memory, and slices the matches afterward. This preserves the meaning of total and pagination, but a page size of twelve does not limit that search to twelve database records.
For a small editorial collection, this may be sufficient. As content grows, measure response time and the number of records scanned for broad searches. A dedicated index or database search implementation may become appropriate. Keep publication and access rules aligned in any replacement, including how content is removed after unpublishing.
Results are sorted by createdAt descending in this release. Changing a post's publication timestamp does not turn this into a publication-date sort. Offset pagination can also shift when new records are inserted between page requests; do not promise a stable cursor or immutable snapshot.
Use a labeled search field, an explicit submit action, and links for pagination. A URL that includes the chosen term, tag, and offset lets visitors refresh or share their current results. Display the selected filter and provide a clear way to remove it.
For server-rendered frameworks, load the first result page on the server and hydrate the same query state. The TanStack blog SEO guide and React Router hydration guide cover that integration. Decide which curated tag or listing pages deserve search indexing; arbitrary search combinations do not automatically need sitemap entries.
Test empty results, an unknown tag, mixed-case searches, a query found only in the excerpt, and an offset beyond the end. Include a matching draft and verify that anonymous results exclude it. Check direct requests as well as navigation from the blog page.
See the Blog documentation, released list schema and operation, and getter implementation for the exact contract.