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.

typescript
type 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 index
  • config: Config — CMS config (for collection definitions and searchability settings)
  • collections?: string[] — Optional: only index these collection names

Returns: Serialized JSON index string

Usage:

typescript
import { 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 (from buildSearchIndex)
  • query: string — Search query
  • limit?: number — Max results (default: 50)

Returns: Sorted array of SearchResult[]

Usage:

typescript
import { 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:

typescript
import { 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 name
  • config: Config — CMS config

Returns: Array of field names

Usage:

typescript
import { 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 resolve
  • entryId: string — Entry ID

Returns: Resolved URL string, or null if a field is missing

Usage:

typescript
import { 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 with entry.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.json on 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#

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:

typescript
interface SearchBoxProps {
  placeholder?: string;   // Input placeholder (default: "Search...")
  className?: string;     // Optional container class names (default: "")
}

Features:

  • Fetches from /api/search with 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:

typescript
fields: {
  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:

typescript
search: {} // 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#

tsx
function 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