---
title: "Nitro Hooks"
description: "Nitro runtime hooks for modifying markdown output."
canonical_url: "https://nuxtseo.com/docs/ai-ready/nitro-api/nitro-hooks"
last_updated: "2026-09-25T20:11:53.587Z"
---

These hooks run in the Nitro server. Use [Nuxt hooks](/docs/ai-ready/api/nuxt-hooks) for build-time output.

## `'ai-ready:page:markdown'`{lang="ts"}

**Type:** `(ctx: MarkdownContext) => void | Promise<void>`{lang="ts"}

Runs when the runtime handler converts HTML to Markdown. Change `ctx.markdown` before the response.
A supplied Markdown source bypasses this conversion hook. Manual indexing also bypasses it.

```ts [server/plugins/markdown-footer.ts]
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('ai-ready:page:markdown', (ctx) => {
    ctx.markdown += '\n\n---\n*Generated with mdream*'
  })
})
```

**MarkdownContext:**

| Property      | Type      | Description                        |
| ------------- | --------- | ---------------------------------- |
| `html`        | `string`  | Original HTML                      |
| `markdown`    | `string`  | Generated markdown (modify this)   |
| `route`       | `string`  | The route the module is processing |
| `title`       | `string`  | Page title                         |
| `description` | `string`  | Page description                   |
| `isPrerender` | `boolean` | Whether during prerendering        |
| `event`       | `H3Event` | H3 event object                    |

## `'ai-ready:mdreamConfig'`{lang="ts"}

**Type:** `(config: MdreamOptions) => void | Promise<void>`{lang="ts"}

Runs before the HTTP handler converts HTML to Markdown. Change mdream options for that conversion.
Source Markdown and manual indexing do not call this hook.

```ts [server/plugins/mdream-config.ts]
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('ai-ready:mdreamConfig', (config) => {
    config.filter ??= {}
    config.filter.exclude = [...(config.filter.exclude || []), '.author-bio']
  })
})
```

The hook receives conversion options, without a route field. `config.origin` contains only the origin.
If you need to change a particular page's Markdown, use `ctx.route` in `ai-ready:page:markdown`.

## `'ai-ready:page:indexed'`{lang="ts"}

**Type:** `(ctx: PageIndexedContext) => void | Promise<void>`{lang="ts"}

Runs after runtime indexing writes a page to the database. Check `contentChanged` before updating another system.

```ts [server/plugins/indexed-pages.ts]
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('ai-ready:page:indexed', (ctx) => {
    if (!ctx.contentChanged)
      return

    console.info(`Changed page: ${ctx.route}`)
  })
})
```

Replace the log with your integration. First indexing counts as a change.
`isUpdate` only tells you whether an indexed row already existed.
A forced reindex of unchanged content can run the hook with `contentChanged: false`.

If an external call fails, retry that call separately. The stored hash already changed before the hook ran.
A freshness skip or `skipHook: true` prevents this hook from running.
The [IndexNow recipe](/docs/ai-ready/advanced/indexnow) shows a changed-URL submission.

**PageIndexedContext:**

| Property         | Type                            | Description                                  |
| ---------------- | ------------------------------- | -------------------------------------------- |
| `route`          | `string`                        | Page route                                   |
| `title`          | `string`                        | Page title                                   |
| `description`    | `string`                        | Page description                             |
| `headings`       | `Array<Record<string, string>>` | Parsed headings, such as `[{ h1: "Title" }]` |
| `keywords`       | `string[]`                      | Extracted keywords from content              |
| `markdown`       | `string`                        | Full markdown content                        |
| `updatedAt`      | `string`                        | ISO timestamp                                |
| `isUpdate`       | `boolean`                       | `true` if an indexed row existed             |
| `contentChanged` | `boolean`                       | Whether the converted Markdown hash differs  |

## Manual Indexing Utils

Use `indexPage` when you have HTML, or `indexPageByRoute` when the module should fetch it.
These functions belong in trusted server code. See [Composables](/docs/ai-ready/nitro-api/composables#indexpage) for complete examples.

| Option     | Type      | Description                              |
| ---------- | --------- | ---------------------------------------- |
| `ttl`      | `number`  | Override the freshness window in seconds |
| `force`    | `boolean` | Reindex even if fresh                    |
| `skipHook` | `boolean` | Skip `ai-ready:page:indexed`             |

`indexPageByRoute` takes the event as its second argument. `indexPage` takes it as the fourth argument.

## `'ai-ready:markdown:source'`{lang="ts"}

Supply Markdown you already hold instead of converting the rendered HTML.
The route has no `.md` suffix. Leave `ctx.source` as `null` to use normal conversion.

```ts [server/plugins/markdown-source.ts]
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('ai-ready:markdown:source', (ctx) => {
    if (ctx.route === '/about') {
      ctx.source = {
        title: 'About',
        markdown: '# About\n\nOur documentation site.',
      }
    }
  })
})
```

The payload contains `route: string`, `event: H3Event`, and `source: MarkdownSource | null`.
A source requires `markdown`. It may also provide `title`, `description`, and `updatedAt`.
The module adds its frontmatter to the supplied body. This hook serves Markdown; it does not update the runtime page index.

## Sitemap

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