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.
// lib/lynkow.ts
import { createClient } from 'lynkow'
export const lynkow = createClient({
siteId: process.env.NEXT_PUBLIC_LYNKOW_SITE_ID!,
fetchOptions: {
next: { revalidate: 60 },
},
})1. Enable search
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.
2. Basic search
Use lynkow.search.search() to query from a Server Component, API route, or Client Component.
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
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:
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:
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.
// 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 “{q}”</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.
'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.
## 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=10Parameter | Type | Default | Description |
|---|---|---|---|
| string | required | Search query |
| string | all | Filter by locale code |
| string | all | Filter by category slug |
| string | all | Filter by tag slug |
| number | 1 | Page number |
| number | 20 | Results per page (max 100) |
Response format:
{
"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.
7. Multi-language search
Use the locale parameter to search within a specific language:
// 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
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
curl -s "https://search.lynkow.com/public/YOUR_SITE_ID/search?q=pagniation"
# Should still find articles about "pagination"Test filters
# 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 |
| Returns typed |
Client-side autocomplete |
| No SDK needed client-side |
LLM discovery | Automatic via | Search section added when enabled |
MCP tool |
| For AI agents (Claude Code, Cursor) |
Sync | Automatic | Publish/update/archive/delete synced in real-time |