Choosing which API documentation viewers to serve (Atlas, Swagger UI, ReDoc, Scalar), configuring them, self-hosting their assets, and writing your own.
Documentation UI
Section titled “Documentation UI”sillo generates one OpenAPI document and can render it through any number of viewers. Which ones you get is a list you pass at construction:
from sillo import SilloAppfrom sillo.openapi.ui import Atlas, Swagger, ReDoc, Scalar
app = SilloApp( title="Myapp", docs=[ Atlas(path="/docs"), ReDoc(path="/redoc"), Scalar(path="/reference", theme="purple"), ],)Leave docs unset and you get Atlas at /docs and ReDoc at /redoc.
Turning documentation off
Section titled “Turning documentation off”app = SilloApp(docs=[])No viewer is mounted and /docs is a 404.
The document itself is still served at openapi_url. Presenters
render the document; they do not produce it. If you want the schema gone
too, that is a separate decision:
app = SilloApp(docs=[], openapi_url="/internal/openapi.json")The viewers
Section titled “The viewers”Atlas(path="/docs", theme="auto")Atlas is sillo’s own reference, and what
docs defaults to, so this line is what you get for free.
Three panes: operations on the left, detail in the middle, a request builder on
the right. It carries sillo’s own palette, so your API’s reference looks like
the rest of sillo rather than like a viewer bolted on. Method badges are the
exception and stay a functional set, telling GET from DELETE at a glance
beats brand consistency.
⌘Ksearch that ranks rather than filters. TypinguserputsGET /usersabove a passing mention of “user” twelve operations down.- A request builder that sends. The form is seeded from the schema, so an operation is runnable the moment you open it. Real timing, status, size, headers, and a response viewer. Credentials persist across reloads.
- Snippets in nine languages: cURL, HTTPie, Python (httpx and requests), JavaScript fetch, Node axios, Go, PHP, Ruby, generated from the same request the Send button makes, so a copied snippet cannot describe something else.
- The whole
infoblock. Licence, terms, contact and external documentation as links, every base URL, and every security scheme with its OAuth scopes. - Light and dark, following the operating system.
79 KB with no dependencies and its styles inlined, so the page is one script tag, against roughly 1.4 MB for Swagger UI.
Pinning and self-hosting
Section titled “Pinning and self-hosting”The bundle is served from a pinned tag on jsDelivr, never a branch. An unpinned URL would mean every sillo application’s documentation changes the moment Atlas does, a bad surprise in production, and an unreproducible bug report.
from sillo.openapi.ui import ATLAS_VERSION
print(ATLAS_VERSION) # the tag this sillo release points atTo serve it yourself (which a deployment with no outbound network or a strict
Content-Security-Policy needs) download dist/atlas.standalone.js from that
tag and point at your own copy:
Atlas(js_url="/static/atlas.standalone.js")The failure without this is a blank page rather than an error, because the script never loads.
Swagger UI
Section titled “Swagger UI”Swagger(path="/docs")Interactive, with a Try it out button per operation. The previous default, still shipped, and one line away if you prefer it.
ReDoc(path="/redoc")Three-column reference layout. Read-only (no request execution) which makes it the better choice for a published reference.
Scalar
Section titled “Scalar”Scalar(path="/reference", theme="purple")A modern reference with a built-in client. theme takes Scalar’s palette
names: "default", "alternate", "moon", "purple", "solarized".
Configuring a viewer
Section titled “Configuring a viewer”Each presenter takes a ui_config dict that is passed through to the
viewer’s own initialization, so options sillo has never heard of still
work:
Atlas(theme="dark", ui_config={"deepLinking": False})
Swagger(ui_config={ "persistAuthorization": True, # keep the bearer token across reloads "docExpansion": "none", # collapse everything by default "filter": True, # show the search box "tryItOutEnabled": True,})
ReDoc(ui_config={"hideDownloadButton": True, "expandResponses": "200,201"})
Scalar(theme="moon", ui_config={"hideDownloadButton": True})url and dom_id are set by the presenter and cannot be overridden, replacing
them only ever produces a page that loads the viewer and shows nothing.
Title and favicon
Section titled “Title and favicon”Atlas(title="Myapp — Internal API", favicon_url="/static/favicon.svg")Atlas(favicon_url=None) # no icon at alltitle defaults to the API title, so SilloApp(title="Myapp") already names
the tab correctly. The favicon defaults to sillo’s own, and its media type
follows the file extension. An .svg is labelled image/svg+xml rather than
handed to the browser as a PNG.
Self-hosting the assets
Section titled “Self-hosting the assets”Every viewer loads its JavaScript from a public CDN by default. Override the URLs to serve them yourself:
Atlas(js_url="/static/atlas.standalone.js")Swagger( js_url="/static/swagger-ui-bundle.js", css_url="/static/swagger-ui.css",)ReDoc(js_url="/static/redoc.standalone.js")Scalar(js_url="/static/scalar.js")This is what a deployment with no outbound network needs, and what a strict
Content-Security-Policy needs. A policy without script-src https://unpkg.com blocks the default page, and the symptom is a blank viewer
rather than an error.
It also pins the version. redoc/latest is whatever ReDoc shipped this
morning.
Several viewers, or the same one twice
Section titled “Several viewers, or the same one twice”Nothing stops you mounting one presenter more than once with different configuration:
app = SilloApp(docs=[ Swagger(path="/docs", title="API"), Swagger(path="/internal/docs", ui_config={"tryItOutEnabled": True}), ReDoc(path="/reference"),])Two presenters claiming the same path raise ValueError at construction
rather than letting one silently shadow the other.
Writing your own
Section titled “Writing your own”A presenter is a class with a path and a render(ctx). That is the entire
contract, no registration call, no entry point:
from sillo.openapi.ui import DocsUI, DocsContext
class RapiDoc(DocsUI): path = "/rapidoc" name = "rapidoc"
def render(self, ctx: DocsContext) -> str: return f"""<!DOCTYPE html><html><head> <meta charset="utf-8"> <title>{ctx.title}</title> <script src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script></head><body> <rapi-doc spec-url="{ctx.openapi_url}" theme="dark"></rapi-doc></body></html>"""
app = SilloApp(docs=[RapiDoc()])Subclassing DocsUI gets you the path/title/favicon_url handling for
free, but it is not required, any object with those two attributes is accepted.
Anything else raises TypeError at construction, naming what it got.
What render receives
Section titled “What render receives”| Field | |
|---|---|
ctx.openapi_url | Path to the document, already prefixed with root_path |
ctx.title | API title from the OpenAPI info block |
ctx.version | API version |
ctx.description | API description, or "" |
ctx.config | The full OpenAPIConfig, for anything else |
Looking a viewer up
Section titled “Looking a viewer up”swagger = app.get_docs_ui("swagger")if swagger is not None: print(swagger.path)Returns None when that viewer is not mounted, which is the useful case, a
health check or a startup log that reports where the docs are.
Migrating from swagger_docs and redoc_docs
Section titled “Migrating from swagger_docs and redoc_docs”The old arguments still work and still move the pages:
app = SilloApp(swagger_docs="/api-docs", redoc_docs="/api-redoc")They are deprecated in favour of docs. Combining the two raises
TypeError rather than silently preferring one:
SilloApp(swagger_docs="/api-docs", docs=[Scalar()])# TypeError: docs= cannot be combined with swagger_docs; set the path on# the presenter instead, e.g. docs=[Swagger(path='/api-docs')]The argument is still named swagger_docs because it predates there being
a choice of viewer; it now sets the path of whatever sits at /docs.
The translation is direct:
| Before | After |
|---|---|
swagger_docs="/api-docs" | docs=[Atlas(path="/api-docs"), ReDoc()] |
redoc_docs="/api-redoc" | docs=[Swagger(), ReDoc(path="/api-redoc")] |
| no equivalent | docs=[] |
Note the last row: turning the viewers off is something the old arguments could not express at all.
Things that will bite you
Section titled “Things that will bite you”-
docs=[]does not hide/openapi.json. The schema is a separate route with a separate setting. -
A strict CSP blocks the default CDN scripts, and the failure is a blank page. Self-host the assets, or allow the CDN explicitly.
-
Build the document URL from
ctx.openapi_urlin a custom presenter, or the page breaks under a mount prefix and nowhere else. -
Duplicate paths raise at construction, so a typo in one presenter’s path surfaces at import rather than as the wrong viewer at runtime.
-
redoc/latestandswagger-ui-dist@5float. Pin them throughjs_urlif you need the page to look the same next month. Atlas is already pinned to a released tag.
Related
Section titled “Related”- OpenAPI Overview: how the document is generated
- OpenAPI Customization: title, servers, security schemes
- Static Files: serving self-hosted viewer assets
- Protecting Routes: putting the schema behind auth