canonicalwebteam.store-llm
Flask extension making a Canonical store (snapcraft.io, charmhub.io, ...) readable by LLMs:
- every public page is served as Markdown at
/page.md /llms.txtlists the public pages, discovered from the routing table (llmstxt.org)/llms-full.txtconcatenates their Markdown<link rel="alternate" type="text/markdown">can be advertised from the layout
The HTML to Markdown conversion is done by canonicalwebteam.markdown-response; this package adds the store-specific wiring around it.
Installation
pip install canonicalwebteam.store-llm
Usage
from canonicalwebteam.store_llm import StoreLLM
StoreLLM(
app,
base_url="https://snapcraft.io",
site_name="Snapcraft",
summary="Snapcraft is the home of the Snap Store, where ...",
sections={
"": "Main pages",
"about": "Publishing a snap",
"docs": "Documentation",
"store": "Store",
},
section_order=["Main pages", "Publishing a snap", "Documentation", "Store"],
extra_links=[
{
"section": "Documentation",
"url": "https://snapcraft.io/docs/",
"title": "Snap documentation",
"description": "Reference and explanation for snaps and snapd.",
},
{
"section": "Optional",
"url": "https://snapcraft.io/llms-full.txt",
"title": "Every page in one file",
"description": "The pages above concatenated as Markdown.",
},
],
)
The application factory pattern works too: store_llm = StoreLLM(base_url=..., ...) then store_llm.init_app(app).
Options
| Option | Default | |
|---|---|---|
base_url |
required | Absolute URL of the site, used for the links in llms.txt |
site_name |
required | The H1 of llms.txt; also stripped from page titles (About | Snapcraft → About) |
summary |
required | The blockquote under the H1 of llms.txt |
sections |
{"": "Main pages"} |
Section label per first path segment. "" is for top-level pages such as /about; "about" for /about/publish. Unknown prefixes go to Other pages |
section_order |
order of sections |
Section headings in the order they appear. Other pages follows, then any unlisted section alphabetically, then Optional last so agents can skip it |
extra_links |
[] |
Links discovery cannot find: {"section", "url", "title", "description"} dicts |
is_private |
is_login_gated |
callable(view) -> bool for pages with no Markdown version. The default recognises the stores' login_required decorator. None makes every page public |
strip_elements / strip_classes |
Vanilla-aware lists | Passed to markdown-response |
markdown_cache_control |
private, max-age=3600 |
Cache-Control of Markdown responses |
llms_cache_control |
public, max-age=43200, ... |
Cache-Control of /llms.txt and /llms-full.txt |
What the store needs to provide
Templates. Discovery parses templates rather than rendering them, so every public page must define meta_title and meta_description blocks with static text. Variables render as nothing and an {% if %} contributes its {% else %} branch, so a template shared between a listing and a detail page can keep a static default:
{% block meta_title %}About Snaps | Snapcraft{% endblock %}
{% block meta_description %}Snaps are app packages for desktop, cloud and IoT.{% endblock %}
{# listed as "Interface catalogue" #}
{% block meta_title %}{% if interface %}{{ interface.name }} interface{% else %}Interface catalogue{% endif %}{% endblock %}
A page opts out of llms.txt with <meta name="robots" content="noindex">, the same signal that keeps it out of search results. Layouts and partials (file names starting with _ or containing layout) are never listed, and a meta_title a layout defines is only a fallback: it does not list pages that have no title of their own.
Content. Markdown is extracted from <main id="main-content"> (falling back to <body>). Add data-md-strip to interactive chrome that reads badly as text: copy buttons, share widgets, charts.
Alternate link. Templates get markdown_path, the Markdown URL path of the current page or None for private pages:
{% if markdown_path %}
<link rel="alternate" type="text/markdown" href="https://snapcraft.io{{ markdown_path }}" />
<link rel="describedby" href="https://snapcraft.io/llms.txt" />
{% endif %}
Sitemap. app.extensions["store_llm"].sitemap_paths() returns the same pages, so the sitemap and llms.txt never drift.
Build step
Rendering every page for llms-full.txt is too slow to do on request, so generate both files at build time and the routes serve them from the static folder (falling back to rendering live when the files are missing, for development):
python3 -m canonicalwebteam.store_llm generate webapp.app:create_app
The argument is module:attribute, either a Flask app or a factory returning one. Run it from the repository root after the frontend build, for example in the Dockerfile and rockcraft.yaml, and ignore static/llms.txt and static/llms-full.txt in git. The command reports pages without a meta_description and pages that could not be rendered.
Customising the output
The files are rendered from the store_llm/llms.txt and store_llm/llms-full.txt templates. A store can override either by adding a template with the same name to its own templates/ folder; both receive site_name, and llms.txt receives summary and sections ([{"section", "links": [{"title", "url", "description"}]}]).
How pages are discovered
A route is listed when it takes no URL parameters, answers GET, is not private, its view calls render_template("....html") with a literal name and does not redirect, and the template (or a layout it extends) has a non-empty meta_title. Routes rendering the same template or sharing a title are listed once, under the shortest path (/store, not /explore). The homepage is left out: the header of llms.txt already introduces the site.
Metadata
Release files for canonicalwebteam.store-llm 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| canonicalwebteam_store_llm-0.3.0.tar.gz | 18.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| canonicalwebteam_store_llm-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 38.4 kB
Release files / canonicalwebteam_store_llm-0.3.0.tar.gz
| Download URL | canonicalwebteam_store_llm-0.3.0.tar.gz |
|---|---|
| Size | 18.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f920a3cc84f487fd32a2128bb78e5630f94b5ebb0c03aaf67130a0bb74065d3b
|
|
BLAKE2b-256 checksum How to use checksums |
1374d37efe913b9627eab2b105f4040104f1ffd871a4aca432383fc5b93ffe5c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.
Transparency logRelease files / canonicalwebteam_store_llm-0.3.0-py3-none-any.whl
| Download URL | canonicalwebteam_store_llm-0.3.0-py3-none-any.whl |
|---|---|
| Size | 20.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e3e2c81e43599e4f64e220d2bf43b94a24f8acfd8855d5273c62a22604f4a725
|
|
BLAKE2b-256 checksum How to use checksums |
8d4252ee026d4e761312a27186f36a1297ac6d9ccba29fa3a18bdad0ef3b45dd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.
Transparency log