---
title: "Composables"
description: "Server-side composables for accessing page data at runtime."
canonical_url: "https://nuxtseo.com/docs/ai-ready/nitro-api/composables"
last_updated: "2026-09-25T23:02:43.736Z"
---

Use these functions in Nitro server code. Enable a database, directly or through runtime sync, before querying stored pages.
Nuxt auto-imports `queryPages`, `searchPages`, `countPages`, `streamPages`, `indexPage`, and `indexPageByRoute`.
Other exports need an explicit `#ai-ready` import.

## `queryPages`

**Type:** `(event?: H3Event, options?: QueryPagesOptions) => Promise<PageEntry[] | PageData[]>`{lang="ts"}

Without `route`, returns a page array. With `route`, returns one page or `undefined`.
Set `includeMarkdown: true` to include the Markdown body. List queries exclude error pages by default.

```ts [server/api/pages.get.ts]
import { queryPages } from '#ai-ready'

export default defineEventHandler(async (event) => {
  return queryPages(event, { limit: 20 })
})
```

**Single page lookup:**

```ts [server/api/page.get.ts]
export default defineEventHandler(async (event) => {
  const { path } = getQuery(event)
  if (typeof path !== 'string' || !path.startsWith('/'))
    throw createError({ statusCode: 400, statusMessage: 'Provide a page path starting with /.' })

  const page = await queryPages(event, { route: path, includeMarkdown: true })
  if (!page)
    throw createError({ statusCode: 404, statusMessage: 'Page not found.' })
  return page
})
```

**QueryPagesOptions:**

| Option            | Type                       | Description               |
| ----------------- | -------------------------- | ------------------------- |
| `route`           | `string`                   | Get single page by route  |
| `includeMarkdown` | `boolean`                  | Include markdown content  |
| `where.pending`   | `boolean`                  | Filter by indexing status |
| `where.hasError`  | `boolean`                  | Filter by error status    |
| `where.source`    | `'prerender' \| 'runtime'` | Filter by source          |
| `limit`           | `number`                   | Max pages to return       |
| `offset`          | `number`                   | Skip first N pages        |

**PageEntry:**

| Property      | Type                            | Description                                  |
| ------------- | ------------------------------- | -------------------------------------------- |
| `route`       | `string`                        | Page route (e.g., `/about`)                  |
| `title`       | `string`                        | Page title                                   |
| `description` | `string`                        | Meta description                             |
| `headings`    | `Array<Record<string, string>>` | Parsed headings, such as `[{ h1: "Title" }]` |
| `keywords`    | `string[]`                      | Extracted keywords                           |
| `updatedAt`   | `string`                        | ISO timestamp                                |
| `isError`     | `boolean`                       | Whether the stored page is an error page     |
| `locale`      | `string`                        | Stored locale, or an empty string            |

**PageData:** Extends PageEntry with `markdown: string`.

## `searchPages`

**Type:** `(event: H3Event, query: string, options?: SearchPagesOptions) => Promise<SearchResult[]>`{lang="ts"}

SQLite-compatible databases use FTS5 across title, description, route, headings, keywords, and Markdown.
[PostgreSQL](https://postgresql.org) uses case-insensitive matching across title, description, headings, and Markdown.
Search returns an empty array during development and prerendering.

```ts [server/api/search.get.ts]
import { searchPages } from '#ai-ready'

export default defineEventHandler(async (event) => {
  const { q } = getQuery(event)
  if (typeof q !== 'string' || !q.trim())
    return []
  return searchPages(event, q, { limit: 10 })
})
```

**SearchResult:**

| Property      | Type     | Description                                                |
| ------------- | -------- | ---------------------------------------------------------- |
| `route`       | `string` | Page route                                                 |
| `title`       | `string` | Page title                                                 |
| `description` | `string` | Meta description                                           |
| `score`       | `number` | FTS5 BM25 score, lower ranks first; PostgreSQL returns `0` |

## `countPages`

**Type:** `(event?: H3Event, options?: CountPagesOptions) => Promise<number>`{lang="ts"}

Count pages matching criteria.

```ts [server/api/stats.get.ts]
import { countPages } from '#ai-ready'

export default defineEventHandler(async (event) => {
  const total = await countPages(event)
  const pending = await countPages(event, { where: { pending: true } })
  return { total, pending, indexed: total - pending }
})
```

## `streamPages`

**Type:** `(event?: H3Event, options?: StreamPagesOptions) => AsyncGenerator<PageData>`{lang="ts"}

Read pages in batches using cursor-based pagination. This does not stream the HTTP response by itself.
The example below returns only a count, so it does not collect every Markdown body in memory.

```ts [server/api/export.get.ts]
import { streamPages } from '#ai-ready'

export default defineEventHandler(async (event) => {
  let withMarkdown = 0
  for await (const page of streamPages(event, { batchSize: 50 })) {
    if (page.markdown)
      withMarkdown++
  }
  return { withMarkdown }
})
```

## `indexPage`

**Type:** `(route: string, html: string, options?: IndexPageOptions, event?: H3Event) => Promise<IndexPageResult>`{lang="ts"}

Index HTML you already trust. Call this from your authenticated publishing flow:

```ts [server/utils/index-about.ts]
import type { H3Event } from 'h3'
import { indexPage } from '#ai-ready'

export function indexAbout(html: string, event: H3Event) {
  return indexPage('/about', html, { force: true }, event)
}
```

Database, conversion, or hook failures can throw. A successful write returns `success: true`.
The `indexPageByRoute` wrapper converts fetch and indexing failures into a result with `success: false`.

**Options:**

| Option     | Type      | Default      | Description                       |
| ---------- | --------- | ------------ | --------------------------------- |
| `ttl`      | `number`  | config value | Override TTL check                |
| `force`    | `boolean` | `false`      | Re-index even if fresh            |
| `skipHook` | `boolean` | `false`      | Skip `ai-ready:page:indexed` hook |

**Result:**

| Property         | Type      | Description                                                            |
| ---------------- | --------- | ---------------------------------------------------------------------- |
| `success`        | `boolean` | Whether indexing succeeded                                             |
| `skipped`        | `boolean` | `true` if page was fresh                                               |
| `isUpdate`       | `boolean` | `true` if updating existing entry                                      |
| `contentChanged` | `boolean` | `true` if content hash differs                                         |
| `data`           | `object`  | Page data if successful; its `headings` field contains serialized JSON |
| `error`          | `string`  | Error message if failed                                                |

## `indexPageByRoute`

**Type:** `(route: string, event: H3Event | undefined, options?: IndexPageOptions) => Promise<IndexPageResult>`{lang="ts"}

Fetch HTML and index it in one call. Pass the current request event:

```ts [server/utils/refresh-about.ts]
import type { H3Event } from 'h3'
import { indexPageByRoute } from '#ai-ready'

export function refreshAbout(event: H3Event) {
  return indexPageByRoute('/about', event, { force: true })
}
```

Call this helper from an authenticated publishing flow. For HTTP triggers, use the module's
[authenticated reindex endpoint](/docs/ai-ready/guides/runtime-indexing#control-endpoints-reindex-endpoint-options).

## `useDatabase`

The current export is `useRawDb`. `useDatabase` is not exported.
Use an explicit import for raw SQL access:

**Type:** `(event?: H3Event) => Promise<RawExecutor>`{lang="ts"}

```ts [server/api/custom.get.ts]
import { useRawDb } from '#ai-ready'

export default defineEventHandler(async (event) => {
  const db = await useRawDb(event)
  return db.all('SELECT route, title FROM ai_ready_pages WHERE indexed = 1 AND is_error = 0 LIMIT 20')
})
```

Prefer `queryPages` when it covers the query. Raw SQL depends on the database schema.

## Data Sources

| Context     | Source                                                                             |
| ----------- | ---------------------------------------------------------------------------------- |
| Development | Query helpers return empty results                                                 |
| Prerender   | Temporary [SQLite](https://sqlite.org) database; full-text search is unavailable   |
| Runtime     | Configured SQLite, [Bun](https://bun.sh), D1, LibSQL, Neon, or PostgreSQL database |

A database-backed feature must enable storage. See [database configuration](/docs/ai-ready/api/config#database-false-type-string-filename-string-bindingname-string-url-string-authtoken-string-no-database).

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
