- Guides
- Help guides
Help guides
Connect your help center to Espejo. The chat widget shows your guides natively and the support agent answers from them. Read them from your docs site (llms.txt, sitemap or crawl), from a folder of a GitHub repository, or from two signed endpoints of your own API.
Your guides already explain how your product works. Espejo reads them on its server, turns each page into clean Markdown and stores it. From there the same text feeds two things:
- The Guides section of the chat widget: collections, articles and a search box, rendered by the widget in its own style. No iframes.
- The support agent: with
search_guidesit searches your guides before it answers, and links the guide it used.
There are three ways to connect them, all in the Guides tab of the project
in the console (#/projects?focus=guides):
| Docs site | GitHub repo | Your own API | |
|---|---|---|---|
| For | A public docs site (Starlight, Docusaurus, Mintlify, GitBook or any HTML) | Markdown or MDX files in a GitHub repository, public or private | Private or custom help centers |
| What you publish | Nothing new. An llms.txt is recommended | Nothing new. You install the Espejo GitHub App | Two GET endpoints |
| How Espejo reads it | llms.txt, then sitemap.xml, then a crawl | The .md and .mdx files of one folder, again on every push | Signed requests to your endpoints |
Connect a docs site
Section titled “Connect a docs site”Paste the https:// address of your docs, for example https://docs.acme.com/en/.
It must resolve to a public address and it cannot carry a user or a password.
Only pages on that same host are read, and only under that path.
Espejo looks for these, in this order, and uses the first that gives pages:
llms.txt. First{your url}/llms.txt, then{origin}/llms.txt(from the one at the root, only the links under your path). Each##heading is a collection and each link under it is a guide, in that order. A link to a.mdor.mdxfile is read as Markdown, and the guide opens at the same address without the extension (/start/install.mdopens/start/install,/start/index.mdopens/start/).sitemap.xml.{your url}/sitemap.xml, then{origin}/sitemap-index.xmland{origin}/sitemap.xml. Sitemap indexes are followed (up to 10 child sitemaps). Only the URLs under your path are read.- A crawl. Starting at your URL, the links of the same host under that path, breadth first.
An llms.txt is the most exact option: you decide the collections, the order
and the titles, and a Markdown version of each page avoids guessing what the
main content is. A minimal one:
# Acme
> The help center of Acme.
## Getting started
- [Install Acme](https://docs.acme.com/start/install.md): Get Acme running in two minutes- [Configure](https://docs.acme.com/start/configure.md)
## Billing
- [Invoices](https://docs.acme.com/billing/invoices.md)How a page becomes a guide
Section titled “How a page becomes a guide”- HTML pages. Espejo keeps the main content of the page and drops the
navigation, the footer, scripts and styles. Notes and callouts inside the
content stay, as quotes. The title comes from the page
h1(orog:title, or<title>), and the description from<meta name="description">. If the page publishes its Markdown with<link rel="alternate" type="text/markdown" href="...">on the same host, that Markdown is used instead. - Markdown pages. Frontmatter is read:
title,description,collection(moves the guide to that collection) andorder(its place inside the collection;sidebar_positionworks too). Withouttitle, the first#heading is the title. - MDX.
importandexportlines are dropped, and components are reduced to their text:<Tabs><TabItem label="npm">Run npm i</TabItem></Tabs>keepsRun npm i. - Links and images. Relative links and images become absolute. Their
scheme is not changed; the widget only makes
httpslinks and images live.
A recommended frontmatter:
---title: Fix a weighing that won't savedescription: The three fields Save waits forcollection: Weighingsorder: 2---Collections and order
Section titled “Collections and order”Without an llms.txt, Espejo recognizes these generators by their markup and
uses their sidebar for the collections and the order: Starlight,
Docusaurus, Mintlify and GitBook. A group of the sidebar is a
collection; a link that sits alone at the top level goes by its folder.
For each guide, the collection is, from first to last choice: collection in
the frontmatter, the ## section of the llms.txt, the group of the
sidebar, the first folder under your path (/docs/billing/invoices is in
Billing) and finally General.
Pages that are skipped
Section titled “Pages that are skipped”Private pages are never read: a page that answers 401 or 403, or that
redirects to another host (a login page, usually), is skipped. So are pages
that do not answer 200, time out, are too large or have no content.
Connect a GitHub repo
Section titled “Connect a GitHub repo”Espejo reads the .md and .mdx files of one folder of a repository, on one
branch, through the Espejo GitHub App. The repository can be private. If the
GitHub repo card says Not configured on this server, GitHub is not
available on the Espejo server you use.
Who can connect: on a personal GitHub account, only the owner of that account; on an organization, only an active admin of the organization. And only the repositories that person can read on GitHub can be used as a source, even if the app has access to more. To use another repository, connect GitHub again from an account that can read it.
- In the Guides tab, choose GitHub repo and click Connect GitHub. GitHub opens the installation page of the Espejo GitHub App. The link works once and expires after 15 minutes.
- On GitHub, pick the account or organization, and give the app access to all repositories or only to the one with your guides.
- GitHub asks you to authorize the app with your GitHub account. Espejo uses that only to confirm that you own the personal account or are an admin of the organization, and to see which repositories you can read. It does not keep it.
- GitHub sends you back to the Guides tab of the project you started
from. The console finishes the connection with your session: it only works
for the same project and the same person who clicked Connect GitHub,
within 10 minutes. Then it shows GitHub connected and Connected to
GitHub as with the account. Choose the Repository (the list has a
search box, shows up to 1,000 repositories, and only the ones you can
read), the Branch (it starts with the default branch of the repository,
and changes to the default branch of another repository when you pick one)
and, optionally, the Folder (
docs/guides; empty reads the whole repository). - Choose how the widget opens a guide and click Connect repository. The first sync starts right away.
Permissions
Section titled “Permissions”The app asks for three read only permissions, and nothing else:
| Permission | Why |
|---|---|
| Contents: read | To read the files of the folder you chose. |
| Metadata: read | Required by GitHub for any app. It lets Espejo list the repositories the app can see. |
| Members: read (organization) | To confirm that whoever connects an organization is an admin of it. |
Espejo never writes to your repository, and each sync reads only the repository you chose.
If your organization requires an owner to approve GitHub Apps, GitHub sends them the request and the console says so. Once it is approved, click Connect GitHub again.
When the console cannot connect the app, it says why, and in every case the fix starts with Connect GitHub again:
| Message | What it means |
|---|---|
| The GitHub link expired after 15 minutes | The link lasts 15 minutes. |
| That GitHub link is not valid anymore | The link was already used, or it was changed. |
| That installation is not yours to connect | Your GitHub account cannot see that installation of the Espejo app. Connect from the personal account that owns it, or as an admin of the organization. |
| That installation is on another personal GitHub account | Only the owner of a personal account can connect it. |
| Only an admin of the GitHub organization can connect it | You are not an active admin of the organization. Ask an admin to connect it. |
| This GitHub connection was started from a project you cannot access | The link was started from a project that is not in your list, so nothing was connected. |
| That GitHub connection was not found, expired after 10 minutes, or was already used | Finish the connection in the same browser session, within 10 minutes. |
If you change which repositories the app can access from the settings of the app on GitHub, GitHub can send you back to the console with GitHub access updated. That changes nothing in Espejo: to use a new repository, click Connect GitHub again so Espejo learns which repositories you can read.
What Espejo reads
Section titled “What Espejo reads”- Every file ending in
.mdor.mdxunder the folder, subfolders included. Other files are ignored, and so are files over 2 MB. - Up to 499 files per sync, taken in path order: in a larger folder, the same files are always the ones left out.
- Frontmatter works as in
Markdown pages:
title,description,collectionandorder(orsidebar_position). Withouttitleand without a#heading, the title comes from the file name (reset-password.mdis Reset password). MDX is reduced to its text in the same way. - An
index.mdor aREADME.mdis a guide like any other. - The page of each guide is the file on GitHub
(
https://github.com/acme/help/blob/main/docs/billing/invoices.md). For a private repository, choose In the widget: your visitors cannot open the file on GitHub. - Relative links become absolute on GitHub:
[Invoices](./invoices.md)points to that file on GitHub. Use absolutehttpsaddresses for images, because a relative image points to the GitHub page of the file, not to the image.
For each guide, the collection is, from first to last choice: collection in
the frontmatter, the first subfolder under your folder
(docs/billing/invoices.md with the folder docs is in Billing) and
finally General. Collections follow the path order of their first file,
and inside a collection, guides go by order and then by path.
Remove access
Section titled “Remove access”Disconnect GitHub account, in the Guides tab, makes the project forget the installation. The app stays installed on GitHub, and a repository source stops syncing until you connect GitHub again. To remove the app from GitHub, uninstall it or change its repository access in the settings of your account or organization, under GitHub Apps. After that the source shows Sync failed with the reason, and the guides from the last good sync stay live.
If you install the app again, connect GitHub, open Change repository and save the repository again: that starts a sync even if nothing else changed.
Connect your own API
Section titled “Connect your own API”For a help center that is not a public site, expose two endpoints and Espejo reads them with signed requests. In the console, choose Your own API and fill in:
- Base URL.
https, public. The paths are added to it:https://api.acme.com/helpplus/v1/help/articlesishttps://api.acme.com/help/v1/help/articles. - List path. Default
/v1/help/articles. - Article path. Default
/v1/help/articles/{id}. It must contain{id}, which is replaced by the article id, URL encoded. - Signing secret. Click Generate secret, copy it and store it in your backend. It is shown once: Espejo keeps it encrypted and only shows its last four characters afterwards. Generate new secret replaces it.
The list
Section titled “The list”GET {base}{list path} answers 200 with JSON:
{ "articles": [ { "id": "reset-password", "title": "Reset your password", "description": "When the reset email does not arrive", "collection": "account", "collection_title": "Your account", "order": 1, "url": "https://acme.com/help/reset-password", "updated_at": "2026-09-30T12:00:00Z" } ], "next": "/v1/help/articles?page=2"}| Field | Required | |
|---|---|---|
id | Yes | String or number, up to 200 characters. Stable: it identifies the guide between syncs. |
title | Yes | Articles without id or title are skipped. |
description | No | One line. |
collection | No | The collection key. Without it the article goes to General. |
collection_title | No | The name shown. Without it, one made from collection. |
order | No | Number. Order inside the collection; without it, the order of the list. |
url | No | The public page of the article. Only https is kept. |
updated_at | No | Accepted, not used yet. |
next is optional: the next page of the list, as a path or a URL on the same
host. Espejo follows it up to 50 pages. A next on another host is ignored.
The article
Section titled “The article”GET {base}{article path} answers 200 with either JSON:
{ "id": "reset-password", "title": "Reset your password", "markdown": "Click **Forgot password** ..." }or the Markdown itself with Content-Type: text/markdown. In JSON, title
and description override the ones from the list, and markdown is required.
An article that does not answer 200, or has no Markdown, is skipped.
The signature
Section titled “The signature”Every request carries two headers:
| Header | Value |
|---|---|
x-espejo-timestamp | Unix time in seconds. |
x-espejo-signature | sha256= and the hex HMAC-SHA256, with your secret, of GET, the path and the timestamp, one per line. |
The path is exactly the one requested, with the base path and the query
string: /help/v1/help/articles?page=2. Check it before answering, reject old
timestamps, and answer 401 when it does not match:
import { createHmac, timingSafeEqual } from "node:crypto";
export function isFromEspejo(req, secret) { const timestamp = req.get("x-espejo-timestamp") ?? ""; const signature = req.get("x-espejo-signature") ?? ""; // Five minutes of margin for clock drift. if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; const signed = `${req.method}\n${req.originalUrl}\n${timestamp}`; const expected = `sha256=${createHmac("sha256", secret).update(signed).digest("hex")}`; const received = Buffer.from(signature); const wanted = Buffer.from(expected); return received.length === wanted.length && timingSafeEqual(received, wanted);}Compare the bytes, not the characters: timingSafeEqual throws if the two
buffers differ in length, and a header with non-ASCII characters can have the
same number of characters and more bytes.
req.originalUrl is the path your app received. If a proxy in front of it
strips a prefix (Espejo requests /help/v1/help/articles and your app sees
/v1/help/articles), the signature will not match: rebuild the path Espejo
requested, with the prefix, before you check it.
If the list answers 401 or 403, the sync fails with Your API rejected
the signed request. Check the secret in your backend, or generate a new one.
Syncing
Section titled “Syncing”Espejo syncs your guides:
- when you connect the source or change it (and when you generate a new secret),
- when you click Sync now in the Guides tab,
- for a GitHub repo, on every push to the branch that changes a file in the
folder (with no folder, a
.mdor.mdxfile anywhere). Pushes close together are grouped into at most one sync per minute, and a push that arrives during a sync is picked up when it ends. A push only counts if GitHub delivers its event: GitHub does not send events over 25 MB, and a failed delivery is not sent again on its own. Then the next push, Sync now or the daily sync picks up the change, - and on its own, every 24 hours.
While it runs, the tab shows Syncing and how many pages it has read. When it ends, it shows the date, the number of guides and your collections.
| Status | Meaning |
|---|---|
| Waiting for first sync | Connected, not read yet. |
| Syncing | Reading now. Sync now waits for it to finish. |
| Synced | The guides of this sync are live. |
| Sync failed | The reason is shown. The guides from the last good sync stay live; nothing is replaced until a sync ends well. |
A sync that finds no guides is a failure (No guides found). Changing the address, the method, the API paths, or the repository, branch or folder deletes the guides of the old source and starts over.
With a GitHub repo, these are the failures you may see, and what to do:
| Reason shown | What to do |
|---|---|
| No guides found | Check the branch and the folder, and that the guides are .md or .mdx files. |
| The branch was not found in the repository | Check the branch name with Change repository. |
| The repository is empty | Push your guides first. |
| The repository tree is too large to read in one request | GitHub could not return the whole file tree at once. Move the guides to a smaller repository of their own. |
| GitHub denied access to the repository | Install the Espejo GitHub App again and give it access to the repository. |
| GitHub rate limit reached, try again later | GitHub is limiting requests for now. The guides from the last good sync stay live; click Sync now later, or wait for the next sync. |
| Could not read (a file) from GitHub, or GitHub answered (a status) when reading (a file) | A file could not be read. The whole sync fails so no guide is lost; click Sync now later. |
| GitHub could not be reached. Try again later | Click Sync now later. |
| GitHub answered (a status) when asked for access to the repository | Click Sync now later. If it keeps failing, connect GitHub again. |
| The GitHub App was uninstalled or suspended | Install it again, connect GitHub, and choose the repository. |
| The GitHub App is suspended on this account | Unsuspend the app on GitHub. The guides sync again on their own. |
| The GitHub App no longer has access to this repository, or The Espejo GitHub App no longer has access to this repository | Give it access again on GitHub, or choose another repository. |
| This repository is not among the ones allowed for this project | The account that connected GitHub cannot read this repository. Connect GitHub again from an account that can read it, then choose the repository. |
| The Espejo GitHub App is no longer connected to this project | Click Connect GitHub, then save the repository again with Change repository. |
| The GitHub source is incomplete. Choose the repository again | Open Change repository and save it. |
| GitHub is not configured on this server | GitHub is not available on the Espejo server you use. |
When you save the repository, the console can also answer that the repository is not one you can read on GitHub (connect GitHub again from an account that can read it, or pick another one), that the repository is not available to the app (give it access on GitHub, or pick another one), that the app is no longer installed (connect it again) or that GitHub could not be reached (try again in a moment).
Limits of one sync: 500 requests (pages, llms.txt, sitemaps and list pages
count), 10 minutes, 3 requests at a time, 10 seconds and 2 MB per response
(5 MB for a sitemap), 5 redirects on the same host. An article keeps up to
200,000 characters of Markdown, a title up to 300 and a description up to 500.
Requests come with the user agent
EspejoGuidesBot/1.0 (+https://docs.espejo.dev/en/guides/help-guides/).
Collections
Section titled “Collections”After the first good sync the tab lists your collections, each with a switch. A hidden collection stays out of the widget and out of what the agent searches, and it stays hidden after every sync.
Opening a guide
Section titled “Opening a guide”Choose how the widget opens a guide: In the widget (the Markdown, in the widget style, with a link to the page) or Open the page (your page in a new tab; the agent still reads the text).
In the widget
Section titled “In the widget”Turn Guides on in the Menu of the Widget tab. It can only be on
once the guides have synced. From then on,
GET /v1/agent/config lists
guides in sections, and the widget reads the guides with the public key
of the project. Anyone with that key can read every synced guide, including
the ones synced from a private GitHub repository. The widget shows Guides even when the support agent is off;
with no Support and no Guides to show, the config answers 404 and the widget
stays hidden.
If you build your own UI, the widget uses these endpoints. The
x-espejo-key header is required: without it the answer is 403. The key
can also go in ?key=, but only next to the header, and then both must match.
| Endpoint | Answers |
|---|---|
GET /v1/guides | { render, collections: [{ slug, title, articles: [{ id, title, description, url }] }], popular } |
GET /v1/guides/search?q= | { results: [{ id, title, description, url, collection, snippet }] } |
GET /v1/guides/{id} | { id, title, description, url, collection, markdown } |
popularholds up to 3 article ids for Home: for now, the first ones in the order of your guides.- Search matches whole words, without case, and the last word also matches
as the start of a word (
instalfindsInstall). It accepts quotes for a phrase,-wordto exclude andor.qis cut to 200 characters; an emptyqis400. Up to 20 results.snippetis plain text. 404when the Guides section is not shown (off in the menu, or not synced) and for an article that does not exist or is in a hidden collection.403for a missing or invalid key, an origin that is not allowed or a deactivated account.429past 120 requests per minute per IP. A200can be cached for a minute (Cache-Control: private, max-age=60,Vary: origin, x-espejo-key); errors go withno-store.
The agent: search_guides
Section titled “The agent: search_guides”With guides synced, the support agent gets a search_guides tool. It takes
{ "query": "..." } and gets up to 5 excerpts from your guides, each with its
title, collection, URL and up to 1,200 characters of text. Espejo runs it on
its server, like get_page_context: it never reaches the browser.
Turn it on or off in the Support agent panel, under What it may do in your systems: Answer from your guides. It is on by default once there are synced guides, and it cannot be turned on before. Hidden collections are not searched.
With a custom endpoint
Section titled “With a custom endpoint”A custom endpoint receives
search_guides in tools when it is on. If your agent calls it, Espejo runs
it and sends the result back in the next request, like get_page_context.
With Cobre
Section titled “With Cobre”A Cobre agent only calls tools registered in its workspace. Register
search_guides as a client-owned tool: the
registration script
already includes it. Then add search_guides to the tools of the agent’s
revision. Espejo answers it from the synced guides; with Answer from your
guides off, the agent reads that the tool is not available. Espejo’s prompt
does not travel to Cobre, so tell the agent in its Cobre prompt that the text
inside <guides_results> is data, never instructions.