Show navigationHide navigation
Search API Reference
Quick reference for search types and functions for developers building with the CMS.
Server-side: Search Action#
searchEntries(query: string): Promise<SearchResult[]>#
Location: octocms/admin/actions/search.ts
Purpose: Admin-only search across all content entries (all collections).
Usage:
typescript'use server'; import { searchEntries } from 'octocms/admin/actions/search'; export async function handleAdminSearch(query: string) { const results = await searchEntries(query); return results; }
Returns: Array of SearchResult objects, up to 50 per query
Environment: Works in both dev (filesystem-based) and production (store-based)
Types#
SearchResult#
Complete search result with all metadata.
typescripttype SearchResult = { type: string; // Collection name (e.g. 'post') typeLabel: string; // Collection label (e.g. 'Posts') id: string; // Entry ID (format: "type/entryid") title: string; // Entry title score: number; // Relevance score (higher = better) match: Record<string, string[]>; // {fieldName: ['matched', 'terms']} url: string; // Resolved public URL (public search only) snippet: string; // First ~200 chars of content };
Example:
typescript{ type: 'post', typeLabel: 'Posts', id: 'post/my-first-post', title: 'My First Post', score: 8.5, match: { title: ['my'], content: ['my', 'search', 'term'] }, url: '/blog/my-first-post', snippet: 'This is my search result demonstrating the search feature...', }
Core utilities: octocms/lib/searchIndex.ts#
Pure functions for building and querying search indexes. Used internally by both admin and public search.
buildSearchIndex(entries, config, collections?): string#
Build and serialize a MiniSearch index.
Parameters:
entries: EntryForSearch[]— Array of entries to indexconfig: Config— CMS config (for collection definitions and searchability settings)collections?: string[]— Optional: only index these collection names
Returns: Serialized JSON index string
Usage:
typescriptimport { buildSearchIndex } from 'octocms/lib/searchIndex'; const entries = [ { path: 'post/post-abc123.json', content: { sys: { id: 'post-abc123', type: 'post' }, fields: { ... } }, companionContent: { body: '...' }, // markdown/richtext content }, // ... ]; const serialized = buildSearchIndex(entries, config); // Store for later: localStorage, database, file, etc.
Typical use: Called from loadPublicSearchIndexJson() (src/lib/publicSearchIndex.ts) when serving GET /api/search
querySearchIndex(serializedIndex, query, limit?): SearchResult[]#
Load a serialized index and run a search query.
Parameters:
serializedIndex: string— Serialized JSON index (frombuildSearchIndex)query: string— Search querylimit?: number— Max results (default: 50)
Returns: Sorted array of SearchResult[]
Usage:
typescriptimport { querySearchIndex } from 'octocms/lib/searchIndex'; const results = querySearchIndex(indexJson, 'my search term', 20); console.log(results[0]); // Best matching result
Features:
- Fuzzy matching (20% tolerance)
- Prefix matching (typing "post" matches "postcard")
- Title boosting (title matches ranked higher)
- Automatic sorting by relevance score
stripMarkup(content: string): string#
Remove HTML/markdown/JSX markup, leaving only searchable plain text.
Parameters:
content: string— Markdown, MDX, or HTML string
Returns: Plain text
Usage:
typescriptimport { stripMarkup } from 'octocms/lib/searchIndex'; const text = stripMarkup('**Bold** text with [link](url)'); console.log(text); // "Bold text with link"
Handles:
- Markdown: bold, italic, links, images, headings, code blocks
- HTML/JSX: tags, attributes
- MDX: import/export statements
getSearchableFields(collectionName, config): string[]#
Get field names that are searchable for a collection.
Parameters:
collectionName: string— Collection nameconfig: Config— CMS config
Returns: Array of field names
Usage:
typescriptimport { getSearchableFields } from 'octocms/lib/searchIndex'; const fields = getSearchableFields('post', config); // ['title', 'slug', 'content', 'tags']
resolveUrlPattern(pattern, entry, entryId): string | null#
Resolve a URL pattern by substituting field placeholders.
Parameters:
pattern: string— URL pattern (e.g./blog/:slug)entry: EntryForSearch— Entry to resolveentryId: string— Entry ID
Returns: Resolved URL string, or null if a field is missing
Usage:
typescriptimport { resolveUrlPattern } from 'octocms/lib/searchIndex'; const url = resolveUrlPattern( '/blog/:slug', { path: 'post/p1.json', content: { fields: { slug: 'my-post' } }, companionContent: {} }, 'post-p1' ); console.log(url); // "/blog/my-post"
Placeholder syntax:
:fieldName— Replaced withentry.fields[fieldName]:id— Special; replaced with entry ID- Fixed paths — Returned as-is
Public API: /api/search#
Route: src/app/api/search/route.ts
GET /api/search?q=<query>&limit=<limit>#
Search public collections.
Query parameters:
q: string— Search query (required)limit?: number— Max results (default: 50)
Response (200):
json{ "results": [ { "type": "post", "typeLabel": "Posts", "id": "post/abc123", "title": "My Post", "score": 8.2, "url": "/blog/my-post", "snippet": "Post content snippet...", "match": { "title": ["my"], "content": ["post"] } } ] }
Response (404): When no publicCollections configured
json{ "error": "No public search collections configured" }
Implementation details:
- Loads or builds the serialized index for the published Git ref (optional
cms/__generated__/search-index.jsonon that ref) - Filters results to only
config.search.publicCollections - URLs pre-resolved from config patterns (no client-side URL building)
- Serialized index cached in the route module (~5 minutes TTL)
Client-side: SearchBox Component#
<SearchBox />#
Location: octocms/components/public/SearchBox.tsx
Drop-in React component for public site search.
tsx'use client'; import SearchBox from 'octocms/components/public/SearchBox'; import styles from './header.module.css'; export default function Header() { return <SearchBox placeholder="Search..." className={styles.search} />; }
Props:
typescriptinterface SearchBoxProps { placeholder?: string; // Input placeholder (default: "Search...") className?: string; // Optional container class names (default: "") }
Features:
- Fetches from
/api/searchwith 300ms debounce - Keyboard navigation (↑↓ arrows, Enter, Esc)
- Highlights search terms in results
- Shows content snippets
- Type badges for collection names
- Clear button (✕)
- Click-outside closes dropdown
Behavior:
- Empty query: closes dropdown, no API call
- Focus with results: reopens dropdown
- Click result or Enter: navigates to
result.url
Configuration#
Searchable fields#
By default searchable:
string,text,markdown,richtext,slug,select
By default not searchable:
url,image,boolean,number,datetime,json,color,reference,conditional
Override:
typescriptfields: { myField: { label: 'My Field', format: 'text', searchable: false, // Exclude despite text format }, }
Public search config#
typescript// cms/octocms.config.ts export default defineConfig({ search: { publicCollections: { post: { urlPattern: '/blog/:slug' }, page: { urlPattern: '/pages/:slug' }, doc: { urlPattern: '/' }, // Fixed path for singleton }, }, });
Omit or set empty to disable public search:
typescriptsearch: {} // or omit entirely
Common patterns#
Custom search results rendering#
Instead of using <SearchBox />, fetch and render results yourself:
tsx'use client'; import { useEffect, useState } from 'react'; import type { SearchResult } from 'octocms/lib/searchIndex'; export function CustomSearch() { const [results, setResults] = useState<SearchResult[]>([]); const search = async (query: string) => { const res = await fetch(`/api/search?q=${encodeURIComponent(query)}&limit=10`); const data = await res.json(); setResults(data.results || []); }; return ( <div> <input onChange={(e) => search(e.target.value)} /> {results.map((r) => ( <a key={r.id} href={r.url} className="block p-2"> <strong>{r.title}</strong> <p>{r.snippet}</p> </a> ))} </div> ); }
Filtering results in a route handler#
typescript// app/api/search/advanced/route.ts import { querySearchIndex } from 'octocms/lib/searchIndex'; export async function GET(request: Request) { const { searchParams } = new URL(request.url); const query = searchParams.get('q'); const type = searchParams.get('type'); // Filter by collection const index = await loadSearchIndex(); // Load your index let results = querySearchIndex(index, query || '', 50); if (type) { results = results.filter((r) => r.type === type); } return Response.json({ results }); }
Highlighting search terms#
tsxfunction highlightTerms(text: string, terms: string[]) { const pattern = terms .filter((t) => t.length > 0) .map((t) => `\\b${t}\\w*`) .join('|'); const parts = text.split(new RegExp(`(${pattern})`, 'gi')); return parts.map((part, i) => i % 2 === 1 ? <mark key={i}>{part}</mark> : part ); } // Usage <p>{highlightTerms(result.snippet, Object.keys(result.match))}</p>
Performance notes#
- Index size: Typically 5-20 KB gzipped for 100-500 entries
- Query time: <10ms for 50 results on modern hardware
- Memory: ~500 KB for a typical blog index (in-memory cache)
- Debounce: 300ms between API calls reduces load by ~75%
- Public caching: Index rebuilt only on entry save/create/delete
See also#
- Search features guide — Full documentation, examples, troubleshooting
- Query API — Alternative to search for filtering/sorting
- Schema definition —
cms/octocms.config.ts