{"result":{"depiction_url":null,"edges":{"in_module":{"objects":[{"created":"2026-11-11T10:48:13Z","object_id":{"id":3184,"is_a":["text","documentation","reference","module"],"name":"doc_module_mod_agent_discovery","title":"mod_agent_discovery","uri":"https:\/\/zotonic.com\/id\/3184"},"seq":1}],"predicate":{"id":333,"is_a":["meta","predicate"],"name":"in_module","title":{"_type":"trans","tr":{"en":"In module"}},"uri":"https:\/\/zotonic.com\/id\/in_module"}},"subject":{"objects":[{"created":"2026-11-11T10:48:13Z","object_id":{"id":2554,"is_a":["categorization","keyword","keyword_information_type"],"name":"zotonic_topic_how_to_guide","title":"How-to guide","uri":"https:\/\/zotonic.com\/id\/2554"},"seq":1},{"created":"2026-11-11T10:48:13Z","object_id":{"id":2564,"is_a":["categorization","keyword","keyword_audience"],"name":"zotonic_topic_backend_developer","title":"Backend developer","uri":"https:\/\/zotonic.com\/id\/2564"},"seq":2},{"created":"2026-11-11T10:48:13Z","object_id":{"id":2676,"is_a":["categorization","keyword","keyword_technology"],"name":"zotonic_topic_http","title":"HTTP","uri":"https:\/\/zotonic.com\/id\/2676"},"seq":3}],"predicate":{"id":308,"is_a":["meta","predicate"],"name":"subject","title":{"_type":"trans","tr":{"en":"Keyword"}},"uri":"http:\/\/purl.org\/dc\/elements\/1.1\/subject"}}},"export_revision":14,"id":3190,"is_a":["text","documentation","developerguide"],"links":[{"rel":"self","target":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/3190"},{"rel":"hub","target":"https:\/\/zotonic.com\/.zotonic\/websub"}],"medium":null,"medium_url":null,"name":"doc_guide_agent_discovery","page_url":{"en":"https:\/\/zotonic.com\/docs\/3190\/help-agents-discover-a-site-s-public-apis","x-default":"https:\/\/zotonic.com\/docs\/3190\/help-agents-discover-a-site-s-public-apis"},"preview_url":null,"resource":{"version":6,"pivot_location_lat":null,"title":"Help agents discover a site&#39;s public APIs","is_authoritative":true,"body":"<h1>Help agents discover a site&#39;s public APIs<\/h1>\n<p>Enable <code>mod_agent_discovery<\/code> on a public publishing site, using the Modules\npage in the admin. It depends on <code>mod_base<\/code> and <code>mod_search<\/code>; it does not require\n<code>mod_seo<\/code>. Activation adds these routes:<\/p>\n<table class=\"table\"><thead><tr><th>URL<\/th><th>Purpose<\/th><\/tr><\/thead><tbody><tr><td><code>\/api-help<\/code><\/td><td>Readable API explanation with a clickable public search example<\/td><\/tr><tr><td><code>\/openapi.json<\/code><\/td><td>OpenAPI 3.1.0 description of concise reads and compatibility search\/export<\/td><\/tr><tr><td><code>\/.well-known\/api-catalog<\/code><\/td><td>Linkset catalog pointing to the public API and its descriptions<\/td><\/tr><tr><td><code>\/llms.txt<\/code><\/td><td>Concise site guidance and links for agents<\/td><\/tr><\/tbody><\/table>\n<p>Set <code>mod_agent_discovery.description<\/code> to an optional public description of the\nsite&#39;s information. The site title comes from the existing site configuration.\nNo API key is needed to read public content. The existing APIs continue to apply\nthe caller&#39;s permissions; this module does not grant access or change query limits.<\/p>\n<h2>Link from pages<\/h2>\n<p>Sites using the normal head hook automatically get discovery links:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-django\">{% all include &quot;_html_head.tpl&quot; %}\n<\/code><\/pre>\n<p>Keep discovery out of human-facing page bodies and footers. Do not add visible\nAPI-help\/Markdown navigation or hidden body links. Agents can discover the APIs\nthrough HTTP headers, the HTML head and dedicated discovery documents. Clients\nthat only expose body text may not discover them; that is an accepted constraint.<\/p>\n<p>Override <code>agent_api_help.tpl<\/code> to customize the explanation for the site.\nIts <code>discovery<\/code> variable contains public metadata and documented operations.\nThe sitemap is linked when <code>mod_seo_sitemap<\/code> is active.<\/p>\n<p>Standard resource responses receive HTTP discovery links as well. Custom\ncontrollers must call <code>z_context:set_resource_headers\/2<\/code> to receive them.\nExisting Link values, including WebSub advertisements, are preserved when the\ndiscovery observer runs. Later custom observers must also merge Link values.<\/p>\n<p>JSON export and Markdown alternates are advertised only when the resource is visible to an\nanonymous visitor and is not excluded by site, resource or category noindex\nsettings. Visibility is checked even when the visitor is an administrator.\nThese exclusions affect discovery, not the permissions of existing API routes.<\/p>\n<h2>Verify the site<\/h2>\n<p>Visit an ordinary public resource while signed out. Check its HTML links and\nHTTP <code>Link<\/code> header, follow the API-help link, run the search example, and use an\nID from the result to retrieve a full export. Check the export&#39;s language-specific\n<code>page_url<\/code> map when citing it. Compare a private and a noindex resource to confirm\nthat no JSON alternate is advertised for either.<\/p>\n<p>Check site-specific routes and proxy rules if a discovery URL is unavailable.\nThe module generates URLs through dispatch and uses anonymous metadata. Discovery\ndocuments currently use no-cache headers; existing APIs keep their cache and\n<code>X-Robots-Tag: noindex,nofollow<\/code> behavior. No robots.txt or CORS settings are changed.\nDo not infer reuse licenses from public accessibility; retain the site&#39;s stated\ncontent and media attribution and licensing terms.<\/p>\n<h2>Read concise public content<\/h2>\n<p>The following routes always use anonymous permissions, including for signed-in\nadministrators. JSON uses the <code>status<\/code>\/<code>result<\/code> envelope.<\/p>\n<table class=\"table\"><thead><tr><th>URL<\/th><th>Purpose<\/th><\/tr><\/thead><tbody><tr><td><code>\/api\/agent\/search<\/code><\/td><td>Browse or full-text search localized summaries<\/td><\/tr><tr><td><code>\/api\/agent\/resource\/{id}<\/code><\/td><td>Read localized Markdown with citation and media links<\/td><\/tr><tr><td><code>\/api\/agent\/related\/{id}<\/code><\/td><td>Follow one hop of outgoing or incoming relationships<\/td><\/tr><tr><td><code>\/api\/agent\/schema<\/code><\/td><td>Discover category or predicate resources<\/td><\/tr><tr><td><code>\/agent\/resource\/{id}\/markdown<\/code><\/td><td>Read a resource as <code>text\/markdown<\/code><\/td><\/tr><\/tbody><\/table>\n<p>Request <code>Accept: text\/markdown<\/code> on <code>\/id\/{id}<\/code> to negotiate the Markdown URL.\nMarkdown alternates are advertised in HTML head and HTTP Link metadata on public\nindexable pages.<\/p>\n<p>All reads accept <code>language<\/code>, defaulting to the configured site language. Resources\nreport <code>requested_language<\/code>, actual <code>language<\/code>, <code>available_languages<\/code> and\n<code>is_fallback<\/code>. Missing requested translations fall back to the site language,\nthen the resource&#39;s first language. The human <code>page_url<\/code> is the citation URL;\n<code>uri<\/code> is the entity identifier.<\/p>\n<p>Search accepts <code>text<\/code> (up to 512 bytes) and <code>cat<\/code> (category name or ID).\n<code>sort<\/code> accepts <code>id<\/code> (resource ID ascending, the default), <code>newest<\/code> (publication\nstart descending), <code>oldest<\/code> (publication start ascending) or <code>modified<\/code>\n(modification time descending). All orders use resource ID ascending as a stable\ntie-breaker. <code>publication_after<\/code> and <code>publication_before<\/code>\naccept valid <code>YYYY-MM-DD<\/code> dates and filter publication start inclusively in UTC.\nThe before date includes the whole day. Reversed ranges are rejected.\nSummaries include the resource&#39;s UTC <code>modified<\/code> timestamp.<\/p>\n<p>Relationships accept <code>direction=in<\/code> or <code>direction=out<\/code> (default) and an optional\n<code>predicate<\/code> ID or name. Unknown or inaccessible predicates return 404.\nSchema accepts <code>kind=category<\/code> (default) or <code>kind=predicate<\/code>. Categories include\ntheir visible parent. Predicates include up to 100 candidate subject\/object\ncategory constraints, filtered by public visibility and noindex. These describe\npublic vocabulary, not validation or access rules; hidden constraints are never\nexported. <code>constraints_truncated<\/code> marks the bound. Names, localized descriptions\nand resource URIs identify the site&#39;s vocabulary without exposing database fields.\nPaged reads accept <code>page<\/code> and <code>pagelen<\/code>: default 20, maximum 100 per page,\nwith <code>page * pagelen &lt;= 10000<\/code>. Unknown, duplicate and malformed query parameters\nare rejected. Queries have a five-second execution budget.<\/p>\n<p>Follow <code>next_url<\/code> until <code>next<\/code> is false. These are <strong>candidate pages<\/strong>: public\nvisibility and site\/resource\/category noindex filtering can leave a page empty\nwhile later pages still contain results. No totals are exposed. <code>limit_reached<\/code>\nis true if exploration stops at the 10,000-candidate limit; refine the search.\nDirect reads allow public noindex content. Missing, private and unpublished\nresources all return 404. The facade never returns arbitrary resource properties.<\/p>\n<p>Relationship pages fetch only <code>pagelen + 1<\/code> candidate edges from PostgreSQL,\nthrough <code>m_edge:get_page\/6<\/code>, before visibility\/noindex filtering. An optional\npredicate is applied in SQL. Ordering is by predicate ID and endpoint resource ID,\nnot editorial edge sequence. This matches the existing outgoing\/incoming unique\nedge indexes and avoids sorting all incoming edges of a popular keyword.\nOffsets still cost work proportional to the skipped candidates; the existing\n10,000-candidate traversal limit remains. Concurrent edge edits can shift pages.<\/p>\n<p>Other callers can use <code>\/api\/model\/edge\/get\/o\/{id}\/{predicate}<\/code>, <code>s<\/code>, <code>o_props<\/code>\nor <code>s_props<\/code> with optional <code>offset<\/code> and <code>limit<\/code> query parameters. Without either,\nthese methods retain their full-list behavior. With either, offset defaults to\n0 and limit to 20 (maximum 100, offset plus limit at most 10,000). They return the\nexisting edge tuples or property lists, in editorial <code>seq, id<\/code> order, with the\nsame caller ACL semantics and no continuation metadata. The predicate is required.\nSQL selects the requested subset, but sequence ordering may require a sort.\nThe agent facade uses the separate Erlang <code>get_page\/6<\/code> helper for indexed\nendpoint ordering, filtered candidate pages and continuation metadata.<\/p>\n<p>Markdown includes body HTML, text and header blocks, and one level of page-reference content. All <code>survey_...<\/code> blocks are intentionally skipped without marking the content\nincomplete. Unsupported blocks and oversized\ncontent are explicitly marked; check <code>content_complete<\/code> and follow the source\npage for omitted material. Inline images use the existing Markdown conversion.\nThe Markdown response ends with a concise “API access” section linking to API\nhelp, the OpenAPI specification and this resource&#39;s JSON endpoint in the content&#39;s\nactual language. These links are not added to human-facing HTML or to the JSON\nresource&#39;s editorial <code>markdown<\/code> field. They assist clients that already fetch\nthe Markdown representation; header\/head discovery remains unchanged.\nIf an editorial template cannot render, the representation returns HTTP 503 with\n<code>Cache-Control: no-store<\/code>, rather than successful empty content. This can occur\nwhile the site&#39;s templates are becoming available after startup; retry later.\nLegitimate empty content still renders successfully.\nJSON includes a resource&#39;s own downloadable medium when available, exposing only\nMIME type, dimensions, size and download URL. Preserve supplied licensing and\nattribution. Page references include a source link, language and supplied licensing\/attribution;\nprivate or unpublished references are omitted. Nested non-survey blocks are not\nexpanded recursively and mark the representation incomplete. This prevents cycles\nand large graphs from turning a resource read into an unbounded export.<\/p>\n<p>Content responses use ETags and <code>Cache-Control: public, max-age=0, must-revalidate<\/code>.\nEach conditional request rebuilds the representation after anonymous permission\nand publication checks. No server-side representation cache is introduced.\nErrors use no-store: 400 for invalid requests, 404 for unavailable resources,\n413 for responses over 1 MiB, 503 for backend failures and 504 for timeouts.\nBoth GET and HEAD are supported; no credentialed CORS policy is added.<\/p>\n<p>The initial pilot is complete. Implementation decisions, pilot findings and\nfuture work are recorded in the\n<a href=\"https:\/\/github.com\/zotonic\/zotonic\/blob\/master\/doc\/technotes\/agent-discovery\/agent-discovery-and-access.md\">design notes<\/a>.\nAdditional JSON-LD and Turtle work waits for the SPARQL merge. No MCP endpoint is\nadvertised.<\/p>\n<h2>Customize Markdown composition<\/h2>\n<p>The model renders editorial HTML under anonymous permissions, then converts it\nwith Zotonic&#39;s faithful Markdown converter. Override <code>_agent_content.tpl<\/code> in the\nsite for site-wide composition, or <code>_agent_content.article.tpl<\/code> (and other category\nsuffixes) for category-specific composition. Variables are <code>id<\/code>, <code>body_html<\/code> and\n<code>blocks_html<\/code>: the latter contains already-rendered block HTML, with all\n<code>survey_...<\/code> blocks removed before template selection. Do not re-read <code>id.blocks<\/code>\nor introduce cached privileged fragments. Custom fields can be read through\n<code>m.rsc[id]<\/code>, which retains the anonymous property checks.<\/p>\n<p>To support a custom editorial block, provide <code>_agent_block_&lt;type&gt;.tpl<\/code>; category\nvariants such as <code>_agent_block_example.article.tpl<\/code> work too. Block type names\nmust contain lowercase ASCII letters, digits or underscores, starting with a\nletter. Variables are <code>id<\/code> (the owning resource), <code>blk<\/code> (the ACL-filtered block,\nwith localized <code>body<\/code> and <code>header<\/code>) and <code>reference<\/code> (for page blocks). Missing\nrenderers produce an explicit omission marker and <code>content_complete=false<\/code>.\nSurvey renderers are never selected, even if one exists in the site.<\/p>\n<p>Custom renderers are responsible for representing their block&#39;s meaning faithfully.\nEscape non-HTML custom values and attributes, preserve licensing, and link to the\nsource for interactive content. The rendering budget and size limits also apply\nto overrides. Existing templates for normal HTML pages are not used implicitly.<\/p>","slug":"help-agents-discover-a-site-s-public-apis","is_protected":false,"visible_for":0,"tz":"UTC","language":["en"],"doc_source_hash":"529eeff80e94fe7a8f97d4c019cd882295a1345d4ed8c50d6a0e43ad5edb4aa8","is_featured":false,"content_group_id":{"id":2551,"is_a":["meta","content_group"],"name":"content_group_imported_docs","title":"Imported documentation","uri":"https:\/\/zotonic.com\/id\/content_group_imported_docs"},"category_id":{"id":317,"is_a":["meta","category"],"name":"developerguide","title":"Developer guide","uri":"https:\/\/zotonic.com\/id\/developerguide"},"doc_source_path":"doc\/guides\/agent-discovery.md","publication_start":"2026-11-11T10:48:13Z","github_url":"https:\/\/github.com\/zotonic\/zotonic\/blob\/master\/doc\/guides\/agent-discovery.md","pivot_location_lng":null,"doc_source_kind":"guide","name":"doc_guide_agent_discovery","is_unfindable":false,"is_published":true,"pivot_geocode":null,"created":"2026-11-11T10:48:12Z","uri":null,"doc_status":"current","is_dependent":false,"publication_end":"9999-06-01T00:00:00Z","modifier_id":{"id":1,"is_a":["person"],"name":"administrator","title":"Site Administrator","uri":"https:\/\/zotonic.com\/id\/1"},"privacy":0,"doc_source_commit":"8e6aad17c4d86dee983db46a656ed6f6a930bcbb\n","creator_id":{"id":1,"is_a":["person"],"name":"administrator","title":"Site Administrator","uri":"https:\/\/zotonic.com\/id\/1"},"modified":"2026-11-11T14:07:34Z","title_slug":"help-agents-discover-a-site-s-public-apis"},"uri":"https:\/\/zotonic.com\/id\/3190","uri_template":"https:\/\/zotonic.com\/id\/:id","version":14,"websub":{"hub":"https:\/\/zotonic.com\/.zotonic\/websub","topic":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/3190"}},"status":"ok"}