Add full-text search with typo tolerance to your Next.js site. Lynkow Instant Search indexes all your published content and keeps the index in sync automatically -- you just query the SDK.

Prerequisites

You should have the Lynkow SDK installed and a client initialized. If not, see Guide 1: Quick Start.

TypeScript
// lib/lynkow.ts
import { createClient } from 'lynkow'

export const lynkow = createClient({
  siteId: process.env.NEXT_PUBLIC_LYNKOW_SITE_ID!,
  fetchOptions: {
    next: { revalidate: 60 },
  },
})

Go to your Lynkow admin dashboard: Settings > SEO > Search and toggle it on. This triggers a full index of all your published content. From that point on, every publish, update, archive, or delete is synced to the search index automatically.


Use lynkow.search.search() to query from a Server Component, API route, or Client Component.

TypeScript
import { lynkow } from '@/lib/lynkow'

const results = await lynkow.search.search('pagination')

results.data   // Array of matching articles
results.meta   // { total, page, totalPages, perPage, query, processingTimeMs }

Filter by locale, category, or tag

TypeScript
const results = await lynkow.search.search('formulaire', {
  locale: 'fr',
  category: 'guides',     // category slug
  tag: 'forms',           // tag slug
  page: 1,
  limit: 10,
})

Typo tolerance

Lynkow Instant Search handles typos automatically. A query for "pagniation" will still find articles about pagination. No configuration needed.


3. Search result shape

Each hit in results.data contains:

TypeScript
interface SearchHit {
  id: string              // Content UUID
  title: string           // Article title
  slug: string            // URL slug
  excerpt: string         // Short summary
  path: string            // Full URL path (e.g. "/docs/guides/forms")
  locale: string          // Content locale
  type: string            // Content type (e.g. "post")
  categories: Array<{ name: string; slug: string }>
  tags: Array<{ name: string; slug: string }>
  metaTitle: string
  metaDescription: string
  authorName: string
  featuredImage: string | null
  publishedAt: number     // Unix timestamp
  updatedAt: number       // Unix timestamp
  _formatted?: Record<string, string>  // Highlighted matches (HTML)
}

The _formatted object contains the same fields but with matching terms wrapped in <em> tags for highlighting.

The meta object:

TypeScript
interface SearchMeta {
  total: number           // Total matching results
  page: number            // Current page
  totalPages: number      // Total pages
  perPage: number         // Results per page
  query: string           // The query as received
  processingTimeMs: number // Search engine response time
}

4. Build a search page (Server Component)

A full search page with pagination using Server Components -- no client-side JavaScript needed.

TypeScript
// app/search/page.tsx
import { lynkow } from '@/lib/lynkow'
import Link from 'next/link'
import type { SearchHit } from 'lynkow'

interface Props {
  searchParams: Promise<{ q?: string; page?: string }>
}

export default async function SearchPage({ searchParams }: Props) {
  const { q, page } = await searchParams

  if (!q) {
    return (
      <div>
        <h1>Search</h1>
        <form action="/search" method="get">
          <input type="search" name="q" placeholder="Search articles..." autoFocus />
          <button type="submit">Search</button>
        </form>
      </div>
    )
  }

  const results = await lynkow.search.search(q, {
    page: Number(page) || 1,
    limit: 20,
  })

  return (
    <div>
      <h1>Results for &ldquo;{q}&rdquo;</h1>
      <p>
        {results.meta.total} result{results.meta.total !== 1 ? 's' : ''} in{' '}
        {results.meta.processingTimeMs}ms
      </p>

      {results.data.length === 0 ? (
        <p>No results found. Try a different query.</p>
      ) : (
        <ul>
          {results.data.map((hit: SearchHit) => (
            <li key={hit.id}>
              <Link href={hit.path}>
                <h2>{hit.title}</h2>
                {hit.excerpt && <p>{hit.excerpt}</p>}
                {hit.categories.length > 0 && (
                  <span>{hit.categories.map((c) => c.name).join(', ')}</span>
                )}
              </Link>
            </li>
          ))}
        </ul>
      )}

      {results.meta.totalPages > 1 && (
        <nav>
          {results.meta.page > 1 && (
            <Link href={`/search?q=${q}&page=${results.meta.page - 1}`}>Previous</Link>
          )}
          {results.meta.page < results.meta.totalPages && (
            <Link href={`/search?q=${q}&page=${results.meta.page + 1}`}>Next</Link>
          )}
        </nav>
      )}
    </div>
  )
}

5. Build a search box with autocomplete (Client Component)

For real-time search-as-you-type, use a Client Component that calls the SDK with a debounce.

TypeScript
'use client'

import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import type { SearchHit } from 'lynkow'

// Import your SDK client -- must be available client-side
const SITE_ID = process.env.NEXT_PUBLIC_LYNKOW_SITE_ID!
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'https://search.lynkow.com'

export function SearchBox() {
  const [query, setQuery] = useState('')
  const [hits, setHits] = useState<SearchHit[]>([])
  const [isOpen, setIsOpen] = useState(false)
  const router = useRouter()

  useEffect(() => {
    if (!query.trim()) {
      setHits([])
      return
    }

    const timer = setTimeout(async () => {
      const res = await fetch(
        `${API_URL}/public/${SITE_ID}/search?q=${encodeURIComponent(query)}&limit=5`
      )
      const json = await res.json()
      setHits(json.data || [])
      setIsOpen(true)
    }, 200) // 200ms debounce

    return () => clearTimeout(timer)
  }, [query])

  return (
    <div style={{ position: 'relative' }}>
      <input
        type="search"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        onFocus={() => hits.length > 0 && setIsOpen(true)}
        onKeyDown={(e) => {
          if (e.key === 'Enter' && query.trim()) {
            router.push(`/search?q=${encodeURIComponent(query)}`)
            setIsOpen(false)
          }
        }}
        placeholder="Search..."
      />

      {isOpen && hits.length > 0 && (
        <ul
          style={{
            position: 'absolute',
            top: '100%',
            left: 0,
            right: 0,
            background: 'white',
            border: '1px solid #e5e7eb',
            borderRadius: 8,
            boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
            listStyle: 'none',
            padding: 0,
            margin: '4px 0 0',
            zIndex: 50,
          }}
        >
          {hits.map((hit) => (
            <li key={hit.id}>
              <a
                href={hit.path}
                onClick={() => setIsOpen(false)}
                style={{ display: 'block', padding: '8px 12px', textDecoration: 'none' }}
              >
                <strong>{hit.title}</strong>
                {hit.categories.length > 0 && (
                  <span style={{ color: '#6b7280', fontSize: '0.875rem', marginLeft: 8 }}>
                    {hit.categories[0].name}
                  </span>
                )}
              </a>
            </li>
          ))}
        </ul>
      )}
    </div>
  )
}

This component calls the public search API directly from the browser -- no SDK import needed client-side. The search.lynkow.com endpoint handles CORS automatically.


6. LLM and AI agent access

When search is enabled, Lynkow automatically does three things that make your content searchable by AI:

llms.txt integration

Your llms.txt file gets a new ## Search section with the full endpoint URL, parameters, and an example query. Any LLM that reads your llms.txt (ChatGPT, Claude, Perplexity) learns how to search your content instead of crawling every page.

Markdown
## Search

This site has a full-text search API with typo tolerance.

**Endpoint:**
GET https://search.lynkow.com/public/{siteId}/search?q={query}

**Parameters:** q (required), locale, category, tag, page, limit (max 100)

Public search API

The search endpoint requires no authentication. Any HTTP client can call it:

GET https://search.lynkow.com/public/{siteId}/search?q=pagination&locale=en&category=guides&limit=10

Parameter

Type

Default

Description

q

string

required

Search query

locale

string

all

Filter by locale code

category

string

all

Filter by category slug

tag

string

all

Filter by tag slug

page

number

1

Page number

limit

number

20

Results per page (max 100)

Response format:

JSON
{
  "data": [
    {
      "id": "uuid",
      "title": "Dynamic Forms",
      "slug": "forms",
      "path": "/guides/forms",
      "excerpt": "How to build forms with Lynkow",
      "categories": [{ "name": "Guides", "slug": "guides" }],
      "publishedAt": 1712505600
    }
  ],
  "meta": {
    "total": 12,
    "page": 1,
    "totalPages": 1,
    "perPage": 20,
    "query": "pagination",
    "processingTimeMs": 3
  }
}

MCP tool

AI agents using Lynkow's MCP server get a search_site_contents tool that wraps the search API. This lets tools like Claude Code or Cursor search your documentation without parsing HTML.


Use the locale parameter to search within a specific language:

TypeScript
// French content only
const results = await lynkow.search.search('formulaire', { locale: 'fr' })

// English content only
const results = await lynkow.search.search('form', { locale: 'en' })

// All languages (default)
const results = await lynkow.search.search('form')

8. Testing

Verify search is enabled

Bash
curl -s "https://search.lynkow.com/public/YOUR_SITE_ID/search?q=test"

A 503 response means search is not enabled for this site. Enable it in Settings > SEO > Search.

Test typo tolerance

Bash
curl -s "https://search.lynkow.com/public/YOUR_SITE_ID/search?q=pagniation"
# Should still find articles about "pagination"

Test filters

Bash
# By category
curl -s "https://search.lynkow.com/public/YOUR_SITE_ID/search?q=test&category=guides"

# By locale
curl -s "https://search.lynkow.com/public/YOUR_SITE_ID/search?q=test&locale=fr"

Summary

What

How

Details

Enable search

Admin: Settings > SEO > Search

One toggle, auto-indexes all content

Server-side search

lynkow.search.search(query, options)

Returns typed SearchResponse

Client-side autocomplete

fetch() to /public/{siteId}/search

No SDK needed client-side

LLM discovery

Automatic via llms.txt

Search section added when enabled

MCP tool

search_site_contents

For AI agents (Claude Code, Cursor)

Sync

Automatic

Publish/update/archive/delete synced in real-time