Custom docs
Make the built-in docs site yours: identity and README, a Bootstrap skin, and finally your own landing page.
Every plugin serves a docs site at /: the About page with your README, the Tools index with
every REST route and MCP tool name, the skills, agents, widgets and prompts, Swagger UI and ReDoc,
and the MCP connection details. It is rendered from the plugin’s own metadata, so it is always
complete and always current.
You customize it in three layers, from the lightest to the heaviest. Start at the top: most plugins never need the last one.
| Layer | What changes | Where |
|---|---|---|
| Identity | Title, description, logo, icons, the About page text | qrtl.config.ts, README.md |
| Skin | Colors, fonts, rounding, spacing, component looks | style.skin |
| Your own landing page | The page at / itself |
src/pages/index.astro |
1. Identity: title, description, logo and README
The About page and the top bar show what qrtl.config.ts says about the plugin (see
Configuration for every option):
// qrtl.config.ts
import { defineQrtlConfig } from "@quartal/plugin";
export default defineQrtlConfig({
title: "PRH Open Data",
description: "Company register, financial statements and registered notices from PRH.",
style: {
logo: "https://cdn.example.com/prh-logo.png",
icons: [{ src: "https://cdn.example.com/prh-icon.png", mimeType: "image/png", sizes: ["192x192"] }],
},
});
| Option | Shown as |
|---|---|
title |
The heading of the About page and the brand text of the top bar. Falls back to package.json#name. |
description |
The lead paragraph of the About page. Also the MCP server description, so write it for the agent as much as for the reader. |
style.logo |
The picture next to the heading on the About page. At least 300 px wide; defaults to the Quartal logo. |
style.icons |
The first icon is the top-bar icon; all of them are advertised to MCP clients. Icon bytes are served from /icons/{index} on your host, so a client never needs to reach your CDN. |
README.md in the plugin root is the body of the About page. It is first-party content, so its
HTML (layout <div>s, images under public/) is rendered rather than escaped. Write it for the
person deciding whether to connect the plugin: what the tools do, what access they need, an example
conversation. The same file is served raw at /readme.md.
2. Skin: your Bootstrap CSS
The docs site is built on Bootstrap 5.3: every view uses Bootstrap components (list-group,
table, badge, btn, alert) and Bootstrap utility classes, and nothing else. The look is
therefore one stylesheet, the skin, which style.skin points at:
export default defineQrtlConfig({
style: {
skin: "https://cdn.example.com/skins/acme.css",
},
});
The server injects the skin as the first stylesheet of the docs page
(<link id="plugin-docs-web-skin">), so it loads before the app mounts and nothing flashes. The
default is https://cdn.quartal.com/skins/default.css.
What a skin must contain
A skin is one self-contained CSS file on a CDN or on your own host, with two parts:
-
Bootstrap 5.3, compiled with your variables. The docs site relies on the Bootstrap 5.3 class names and on the Bootstrap CSS variables (
--bs-primary,--bs-body-color, …); a Bootstrap 4 build, Tailwind or a bare reset will not style it. -
Bootstrap Icons 1.13.1. The views draw their chevrons, the hint icons on the Tools index and the widget check marks with
bi-*classes, so the icon font must come along, either bundled or imported:@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css");The default skin does exactly this, so copying it is a good starting point.
The usual way to build one is the Bootstrap Sass toolchain: set the variables, import Bootstrap, add the icons, compile once and publish the file.
// acme.scss → acme.css
$primary: #1a73e8;
$font-family-sans-serif: "Inter", system-ui, sans-serif;
$border-radius: 0.25rem;
@import "bootstrap/scss/bootstrap";
@import "bootstrap-icons/font/bootstrap-icons.css";
Component overrides
Every component of the docs site carries a root class with a q- prefix, so a skin can restyle one
component without touching Bootstrap itself. Use the Bootstrap CSS variables for colors, so the rule
follows the rest of the skin:
.q-plugin-tools-index .table > thead th {
color: var(--bs-primary);
text-transform: uppercase;
}
.q-plugin-left-navi .list-group-item.active {
border-left: 4px solid var(--bs-primary);
}
| Root class | Component |
|---|---|
q-plugin-about |
The About page: logo, title, summary cards, README |
q-plugin-summary |
The summary cards of the About page |
q-plugin-left-navi |
The left navigation |
q-plugin-login, q-plugin-login-error |
The login button and its error alert in the top bar |
q-plugin-tools-index |
The Tools index (every tool with its REST route and MCP name) |
q-plugin-tool-group |
One class of tools |
q-plugin-tool-detail, q-plugin-type-doc |
One tool, and the types it uses |
q-plugin-prompt-detail |
One MCP prompt |
q-plugin-swagger-view, q-plugin-redoc-view |
The embedded Swagger UI and ReDoc |
The same skin can serve your widgets: they are your own pages, so link the stylesheet from their layout and the agent’s chat shows the plugin’s look there too.
3. Your own landing page
When the docs site itself is not what you want at /, give the plugin a page of its own. Create
src/pages/index.astro (or index.md, .mdx, .html, .ts, .js) in the project and Astro
renders it instead of the docs site. Everything the machines use keeps working unchanged:
/plugin.json, /mcp, /api/*, OAuth, /skills/*, /agents/* and the generated JSON files all
stay with the plugin runtime. Only / and the convenience redirects (/mcp.html, /swagger.html,
/docs.html, /skills.html, /agents.html) are released to your page.
The page is detected when the server starts, so restart astro dev after adding or removing it.
Keep the docs UI inside your page
The docs site is published as the npm package
@quartal/plugin-docs-web: one ES module
with Vue, the router and the lazy Swagger UI and ReDoc views bundled. Mount it on your page and you
have your own header, navigation and content around the complete docs UI:
npm install @quartal/plugin-docs-web
---
// src/pages/index.astro
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Acme Plugin</title>
<link rel="stylesheet" href="https://cdn.example.com/skins/acme.css" />
</head>
<body>
<header class="container py-3">Acme: your own branding and navigation.</header>
<div id="docs"></div>
<script>
import "@quartal/plugin-docs-web/style.css";
import { mountPluginDocs } from "@quartal/plugin-docs-web";
mountPluginDocs(document.getElementById("docs")!);
</script>
</body>
</html>
Two things change when the page is yours:
- The skin is yours to load.
style.skinis injected into the built-in page only; on your own page, link the skin in<head>as above. It still has to be a Bootstrap 5.3 skin with Bootstrap Icons, for the same reasons as in layer 2. - The docs UI version is yours to choose. The built-in page always loads the UI published on plugin.quartal.com; a mounted one is the package version your plugin installs, so update the dependency to pick up new views.
The data the UI shows is fetched from the plugin’s own origin (/com.quartal.plugin/contents.json
and the other generated files), so the mounted UI needs no
configuration.
Self-hosting the built-in page
For a plugin without an index page of its own, the page at / is a copy of the shell published on
plugin.quartal.com, fetched and cached by the server. A deployment can point it elsewhere with the
QRTL_DOCS_WEB_URL environment variable, or the docsWebUrl option of the integration. That is a
hosting choice, for example an air-gapped network or a pinned UI version, not a way to customize the
page: for that, use the layers above.