{"result":{"depiction_url":null,"edges":{"observes":{"objects":[{"created":"2026-09-09T13:47:42Z","object_id":{"id":1781,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_export_resource_content_disposition","title":"observe_export_resource_content_disposition\/2","uri":"https:\/\/zotonic.com\/id\/1781"},"seq":1},{"created":"2026-09-09T13:47:42Z","object_id":{"id":2017,"is_a":["text","documentation","reference","notification"],"name":"doc_notification_content_types_dispatch","title":"observe_content_types_dispatch\/3","uri":"https:\/\/zotonic.com\/id\/2017"},"seq":2}],"predicate":{"id":2550,"is_a":["meta","predicate"],"name":"observes","title":{"_type":"trans","tr":{"en":"Observes"}},"uri":"https:\/\/zotonic.com\/id\/observes"}},"references":{"objects":[{"created":"2020-05-30T05:48:01Z","object_id":{"id":1279,"is_a":["text","documentation"],"name":"doc_glossary","title":"Glossary","uri":"https:\/\/zotonic.com\/id\/1279"},"seq":1000000},{"created":"2020-05-30T05:48:01Z","object_id":{"id":1353,"is_a":["text","documentation","developerguide"],"name":"doc_developerguide_modules","title":{"_type":"trans","tr":{"en":"Create a reusable module"}},"uri":"https:\/\/zotonic.com\/id\/1353"},"seq":1000000},{"created":"2020-05-30T05:48:01Z","object_id":{"id":1638,"is_a":["text","documentation","developerguide"],"name":"doc_developerguide_search","title":{"_type":"trans","tr":{"en":"Search"}},"uri":"https:\/\/zotonic.com\/id\/1638"},"seq":1000000}],"predicate":{"id":332,"is_a":["meta","predicate"],"name":"references","title":{"_type":"trans","tr":{"en":"References"}},"uri":"https:\/\/zotonic.com\/id\/references"}},"subject":{"objects":[{"created":"2026-09-15T12:29:56Z","object_id":{"id":2555,"is_a":["categorization","keyword","keyword_information_type"],"name":"zotonic_topic_reference","title":"Reference","uri":"https:\/\/zotonic.com\/id\/2555"},"seq":1},{"created":"2026-09-15T12:29:56Z","object_id":{"id":2565,"is_a":["categorization","keyword","keyword_audience"],"name":"zotonic_topic_integrator","title":"Integrator","uri":"https:\/\/zotonic.com\/id\/2565"},"seq":2},{"created":"2026-09-15T12:29:56Z","object_id":{"id":2601,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_export_and_syndication","title":"Export and syndication","uri":"https:\/\/zotonic.com\/id\/2601"},"seq":3},{"created":"2026-09-15T12:29:56Z","object_id":{"id":2635,"is_a":["categorization","keyword","keyword_architecture"],"name":"zotonic_topic_module","title":"Module","uri":"https:\/\/zotonic.com\/id\/2635"},"seq":4},{"created":"2026-09-15T12:29:56Z","object_id":{"id":2648,"is_a":["categorization","keyword","keyword_task"],"name":"zotonic_topic_serialize","title":"Serialize","uri":"https:\/\/zotonic.com\/id\/2648"},"seq":5},{"created":"2026-09-15T12:29:56Z","object_id":{"id":2651,"is_a":["categorization","keyword","keyword_task"],"name":"zotonic_topic_export","title":"Export","uri":"https:\/\/zotonic.com\/id\/2651"},"seq":6},{"created":"2026-09-15T12:29:56Z","object_id":{"id":2665,"is_a":["categorization","keyword","keyword_data_type"],"name":"zotonic_topic_structured_data","title":"Structured data","uri":"https:\/\/zotonic.com\/id\/2665"},"seq":7}],"predicate":{"id":308,"is_a":["meta","predicate"],"name":"subject","title":{"_type":"trans","tr":{"en":"Keyword"}},"uri":"http:\/\/purl.org\/dc\/elements\/1.1\/subject"}}},"id":1720,"is_a":["text","documentation","reference","module"],"links":[{"rel":"self","target":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/1720"},{"rel":"hub","target":"https:\/\/zotonic.com\/.zotonic\/websub"}],"medium":null,"medium_url":null,"name":"doc_module_mod_export","page_url":{"en":"https:\/\/zotonic.com\/docs\/1720\/mod_export","x-default":"https:\/\/zotonic.com\/docs\/1720\/mod_export"},"preview_url":null,"resource":{"version":3434,"pivot_location_lat":null,"title":"mod_export","is_authoritative":true,"body":"<p>Provides a generic framework for exporting\n<a href=\"\/id\/doc_glossary#term-resource\">resources<\/a>, query results, and application-defined\ndata. An export consists of a data provider and an encoder:<\/p>\n<ul><li>The data provider supplies a header and rows. It can be a module named in the\ndispatch rule, or a set of observers for the export notifications.<\/li><li>The encoder turns those values into CSV, XLSX, JSON, Atom, RSS, JSON Feed, iCalendar, BERT, or\nUBF output.<\/li><\/ul>\n<p>The response is streamed by <code>controller_export_resource<\/code> or <code>controller_export<\/code>.\nMost encoders can emit rows immediately. XLSX and BERT collect their rows and\ncreate the final document in the footer phase.<\/p>\n<h2>Syndication feeds<\/h2>\n<p>Atom and RSS entries include their canonical resource URI as the ID or GUID and\na JSON resource link with relation <code>https:\/\/zotonic.com\/rel\/resource<\/code>. The link points to\nthe <code>rsc_export<\/code> model API, allowing importers to recognize Zotonic resources and\nuse the native importer for properties, media and connections. Normal resource\nvisibility checks apply to the export endpoint.<\/p>\n<p>JSON Feed 1.1 items use the same canonical URI as <code>id<\/code> and advertise the native\nresource endpoint in <code>_zotonic.resource_url<\/code>. <code>mod_import_feed<\/code> consumes all three\nformats and uses these explicit links for native resource imports.<\/p>\n<p>Use <code>\/export\/query\/rss\/&lt;id&gt;<\/code> or <code>\/export\/query\/jsonfeed\/&lt;id&gt;<\/code> for a saved query,\nand <code>\/export\/rsc\/rss\/&lt;id&gt;<\/code> or <code>\/export\/rsc\/jsonfeed\/&lt;id&gt;<\/code> for one resource.\nQuery feeds are served inline. JSON Feed uses <code>application\/feed+json<\/code>; the existing\n<code>json<\/code> export remains the generic <code>application\/json<\/code> array format.<\/p>\n<p>RSS supports category-specific <code>rss\/entry.tpl<\/code> templates, full HTML content,\nUTC publication dates and file enclosures with their MIME type and byte size.\nJSON Feed streams its items and includes content, authors, dates and attachments.\nIts textual fields are plain text, while <code>content_html<\/code> retains HTML markup.<\/p>\n<p>Built-in Atom feeds use their canonical export URL as the feed ID and self link.\nThe title defaults to the site title followed by the exported resource&#39;s title;\nan export header overrides the resource title. File attachments are enclosures,\nwith a single text or HTML content element for each entry.<\/p>\n<p>The feed update time is the latest modification time of the exported resource\nand its visible entries. It is emitted by <code>atom\/feed_end.tpl<\/code>, after the entries,\nso the response can still be streamed. An empty query uses the query resource&#39;s\nmodification time. Exports without any modification time use the Unix epoch.\nThis timestamp does not track removals from query results. Sites overriding the\nAtom templates should keep the feed update element in the footer.<\/p>\n<h2>Conditional feed requests<\/h2>\n<p>Atom, RSS, JSON Feed and iCalendar responses on the <code>feed<\/code> dispatch include\n<code>ETag<\/code> and <code>Last-Modified<\/code>. Clients should save both values and send them back as\n<code>If-None-Match<\/code> and <code>If-Modified-Since<\/code>. An unchanged representation returns\nHTTP 304 with no body. <code>If-None-Match<\/code> takes precedence over the date condition.<\/p>\n<p>The weak ETag fingerprints the ordered resource IDs, modification dates and\nversions, including the query resource. Changes in membership, ordering and\nresource revisions are detected without rendering the body. The selected IDs\nare reused by the encoder, preserving streaming and avoiding a second search.\nTemplate changes and changes to related resources that do not update an\nexported resource&#39;s version are not included in this fingerprint.<\/p>\n<p>Only <code>controller_feed<\/code> performs this check, on at most 100 selected entries.\n<code>controller_export<\/code> and <code>controller_export_resource<\/code>, including the\n<code>export_rsc_query<\/code> dispatch, stream normally without precomputing resource\nfingerprints or handling conditional feed requests.<\/p>\n<p><code>Last-Modified<\/code> records when a change was observed, rather than the newest\nentry timestamp. Fingerprints are cached per URL, format, user and language\nfor 24 hours. A cache miss or a same-second change can force a full response\nfor date-only requests; ETag validation remains available. Responses use\n<code>Cache-Control: private, no-cache<\/code> and still enforce normal access checks.<\/p>\n<p>Conditional requests validate one URL&#39;s entire response. They do not request\nonly new entries, follow subsequent pages, or bypass the limits below.<\/p>\n<h2>Export limits<\/h2>\n<p>Query exports (<code>export_rsc_query<\/code> and other exports using <code>is_query<\/code>) have\nseparate site settings according to how the encoder uses memory:<\/p>\n<table class=\"table\"><thead><tr><th>Configuration<\/th><th>Default<\/th><th>Formats<\/th><\/tr><\/thead><tbody><tr><td><code>mod_export.query_max_rows_streaming<\/code><\/td><td>50,000<\/td><td>CSV, JSON, Atom, RSS, JSON Feed, ICS and UBF; emit rows incrementally.<\/td><\/tr><tr><td><code>mod_export.query_max_rows_non_streaming<\/code><\/td><td>1,000 anonymous \/ 20,000 logged in<\/td><td>XLSX and BERT; buffer the complete export before producing the final file.<\/td><\/tr><\/tbody><\/table>\n<p>Set positive integers in Admin&#39;s configuration editor. Missing, invalid, zero\nor negative values use the corresponding default. Request <code>page<\/code> selects a page and <code>pagelen<\/code> can\nlower its size, but cannot exceed the selected format&#39;s limit.<\/p>\n<p>The lower non-streaming default limits workbook\/resource accumulation. Row sizes\nand concurrent exports still affect memory use; a row cap is not a byte limit.\nCustom export callbacks remain responsible for bounding the rows they supply.<\/p>\n<p>The <code>feed<\/code> routes have a separate fixed cap of 100 items per page and default\nto 20. Larger <code>pagelen<\/code> values are clamped to 100, including <code>qpagelen<\/code> and\nrequests using the default stored query. Neither query export setting changes\nthis feed cap.<\/p>\n<p>Atom, RSS and JSON Feed responses advertise the next page using Atom <code>rel=next<\/code>,\nRSS <code>atom:link rel=next<\/code>, and JSON Feed <code>next_url<\/code>. Links preserve request filters\nand replace <code>page<\/code>. A full page advertises a successor, so the last request can\nreturn an empty page. For importing bulk query exports with <code>mod_import_feed<\/code>,\nuse <code>pagelen=100<\/code> (or less), matching its per-response entry limit.<\/p>\n<p>Publication start descending and resource ID descending provide the fallback\nordering; explicit query sorts take precedence. Pagination currently uses page\nnumbers, so concurrent additions or deletions can shift page boundaries. Query\nexpansion and cursor-based pagination are deferred until the SPARQL work merges.<\/p>\n<h2>Admin interface<\/h2>\n<p>When <a href=\"\/id\/doc_developerguide_modules#activating-modules\">enabled<\/a>, this module\nadds export content types to the View menu on admin edit pages and adds an Export\nblock. Both a single resource and a\n<a href=\"\/id\/doc_developerguide_search#guide-query-resources\">query resource<\/a> can be\nexported. Query export pages default to at most 50,000 resources for streaming\nformats; XLSX\/BERT default to 1,000 for anonymous users and 20,000 for logged-in\nusers. An explicit configuration overrides either default.<\/p>\n<h2>Built-in dispatch rules<\/h2>\n<p>The built-in rules are defined in <code>priv\/dispatch\/dispatch_export<\/code>:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">[\n    {export_rsc, [&quot;export&quot;, &quot;rsc&quot;, type, id],\n        controller_export_resource, []},\n    {export_rsc_query, [&quot;export&quot;, &quot;query&quot;, type, id],\n        controller_export_resource, [{is_query, true}]}\n].\n<\/code><\/pre>\n<p>Short feed URLs are also available:<\/p>\n<ul><li><code>\/feed<\/code> - the default Atom feed (saved query <code>query_feed<\/code>, or published articles).<\/li><li><code>\/feed\/ics<\/code> (or <code>\/feed\/ical<\/code>) - published, unfinished events, earliest start first.<\/li><li><code>\/feed\/rss?cat=article<\/code> - visible articles, newest publication first.<\/li><li><code>\/feed\/atom\/news<\/code> - results from the visible saved query named <code>news<\/code>.<\/li><li><code>\/feed\/jsonfeed\/news?text=Erlang<\/code> - the saved query with an extra text filter.<\/li><li><code>\/feed\/json?id=news&amp;qcat=article<\/code> - JSON Feed, using query-string selection.<\/li><\/ul>\n<p>The bare <code>\/feed<\/code> route uses the visible saved-query resource named <code>query_feed<\/code>.\nIf it is missing, is not a query, or is not visible, it searches the <code>article<\/code>\ncategory with <code>is_published=true<\/code> instead (including publication start\/end checks). Only pagination arguments are accepted on this fixed feed;\n<code>id<\/code>, <code>type<\/code>, and search filters cannot replace its selection. Link to it with\n<code>{% url feed %}<\/code>. The Atom, RSS and JSON Feed typed routes use the same default selection when no id or search terms\nare supplied, including requests with only pagination arguments. Explicit ids\nand search terms keep their request-defined selection.<\/p>\n<p>The <code>feed<\/code> dispatch accepts <code>type<\/code> and an optional <code>id<\/code>. Supported types are\n<code>atom<\/code>, <code>rss<\/code>, <code>jsonfeed<\/code>, <code>json<\/code> (a JSON Feed alias on this route only), <code>ics<\/code> and <code>ical<\/code>.\nAn id selects saved-query results, collection members, or a single ordinary\nresource. Additional search terms are combined with that selection. Missing ids\nreturn not found and invisible resources are denied; neither becomes an\nunrestricted search. Without an id or search terms, the feed uses <code>query_feed<\/code> or visible, published articles.<\/p>\n<p>Feed searches use <code>mod_search<\/code> and accept its normal bare or q-prefixed query\narguments, including repeated terms. <code>page<\/code> defaults to 1 and <code>pagelen<\/code> to 20,\nwith at most 100 items per page. Without a saved query or explicit sort, results\nsort by descending publication start. Calendar feeds instead default to ascending\n<code>date_start<\/code> with ascending <code>id<\/code> as a tie-breaker. Saved queries retain their own sort terms.\nThe feed URL\/self link includes the query string, and all shorthand feeds are\nserved inline. For template links, use <code>{% url feed type=&#39;rss&#39; id=id %}<\/code> and add\nsearch arguments as needed.<\/p>\n<h2>iCalendar feeds<\/h2>\n<p><code>\/feed\/ics<\/code> and <code>\/feed\/ical<\/code> use <code>text\/calendar<\/code>. With no id or search terms,\nthey select <code>cat=event<\/code>, <code>is_published=true<\/code> and <code>unfinished=true<\/code>; they do not\nuse the article query <code>query_feed<\/code>. Pagination alone retains this selection.\nExplicit ids and filters work as for other feed types.<\/p>\n<p>The encoder omits invisible resources and resources without a valid start date,\nmodification timestamp or UID. VEVENTs include DTSTAMP, use exclusive all-day\nend dates, and omit DTEND for instantaneous events. Output uses CRLF and folds\ncomplete lines at 75 octets without splitting UTF-8 characters.\n<code>X-ZOTONIC-RESOURCE<\/code> advertises the native JSON resource endpoint for import\nand optional WebSub subscriptions. <code>X-ZOTONIC-NEXT<\/code> advertises the next page\nfor <code>mod_import_feed<\/code>; it is a Zotonic extension, not standard calendar pagination.<\/p>\n<h2>Choosing between <code>export_rsc_query<\/code> and <code>feed<\/code><\/h2>\n<p>Use <code>export_rsc_query<\/code> to export a saved query in bulk. Use <code>feed<\/code> for a\npaginated syndication URL, a request-defined search, or the site&#39;s default feed.\nThey share the same encoders, but select and limit their results differently.<\/p>\n<table class=\"table\"><thead><tr><th>Behavior<\/th><th><code>export_rsc_query<\/code><\/th><th><code>feed<\/code><\/th><\/tr><\/thead><tbody><tr><td>URL<\/td><td><code>\/export\/query\/&lt;type&gt;\/&lt;id&gt;<\/code><\/td><td><code>\/feed<\/code>, <code>\/feed\/&lt;type&gt;<\/code>, <code>\/feed\/&lt;type&gt;\/&lt;id&gt;<\/code><\/td><\/tr><tr><td>Resource selection<\/td><td>Requires an id; reads that resource&#39;s stored <code>query<\/code> property. Intended for saved queries.<\/td><td>Optional id: saved-query results, collection members, or one ordinary resource.<\/td><\/tr><tr><td>No id or search terms<\/td><td>No default selection; an id must be supplied in the path or query string.<\/td><td>Uses the visible saved query <code>query_feed<\/code>, falling back to published <code>article<\/code> resources when unavailable. Calendar types use published, unfinished events.<\/td><\/tr><tr><td>Request filters<\/td><td>Does not automatically merge request search arguments into the stored query.<\/td><td>Typed routes accept bare or q-prefixed search terms, combined with any id selection.<\/td><\/tr><tr><td>Pagination<\/td><td>Page 1 by default, up to the format-specific limit (default 50,000 streaming \/ 1,000 anonymous or 20,000 logged-in non-streaming); <code>page<\/code> and a smaller <code>pagelen<\/code> are accepted.<\/td><td>Default page 1, 20 items; <code>page<\/code> and <code>pagelen<\/code> are accepted, with at most 100 items per page.<\/td><\/tr><tr><td>Sorting<\/td><td>Uses the stored query&#39;s sort terms.<\/td><td>Retains saved-query sorting; otherwise defaults to descending publication start (ascending start date for calendars) unless an explicit sort is supplied.<\/td><\/tr><tr><td>Formats<\/td><td>All registered export formats, including CSV, XLSX, Atom, RSS, JSON Feed and generic JSON.<\/td><td>Atom, RSS, JSON Feed and iCalendar.<\/td><\/tr><tr><td>Meaning of <code>json<\/code><\/td><td>Generic JSON array (<code>application\/json<\/code>); use <code>jsonfeed<\/code> for JSON Feed.<\/td><td>Alias for <code>jsonfeed<\/code> (<code>application\/feed+json<\/code>).<\/td><\/tr><tr><td>Omitted type<\/td><td>HTTP content negotiation.<\/td><td>Bare <code>\/feed<\/code> is Atom. Typed routes use their path type.<\/td><\/tr><tr><td>Conditional HTTP requests<\/td><td>No feed validator precomputation; results are streamed normally.<\/td><td>Supports <code>ETag<\/code>, <code>Last-Modified<\/code> and HTTP 304 on the bounded page.<\/td><\/tr><tr><td>Content disposition<\/td><td>Atom, RSS and JSON Feed are inline; other formats are attachments by default.<\/td><td>Always inline.<\/td><\/tr><\/tbody><\/table>\n<p>For example, <code>\/export\/query\/rss\/news<\/code> exports up to the configured limit (default 50,000) from the\nstored query <code>news<\/code>. <code>\/feed\/rss\/news?page=2&amp;pagelen=10<\/code> serves its second page\nof ten results, and <code>\/feed\/rss\/news?text=Erlang<\/code> adds a text filter.\n<code>\/feed\/rss<\/code> uses the default site selection; <code>\/feed\/rss?cat=event<\/code> instead\nsearches events. <code>\/feed\/rss?page=2<\/code> keeps the default selection and changes\nonly the page.<\/p>\n<p>Template links use the corresponding dispatch name:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-django\">{% url export_rsc_query type=&#39;rss&#39; id=&#39;news&#39; %}\n{% url feed type=&#39;rss&#39; id=&#39;news&#39; page=2 pagelen=10 %}\n{% url feed type=&#39;rss&#39; %}\n{% url feed %}\n<\/code><\/pre>\n<p>Both routes search with the visitor&#39;s ACL context. Explicit missing or invisible\nids do not trigger the default feed fallback. <code>export_rsc_query<\/code> also supports\nexport visibility overrides through <code>export_resource_visible<\/code>; <code>feed<\/code> checks\nresource visibility directly. A stored query can itself contain <code>qargs=true<\/code>\nto read request arguments; this is behavior of the stored query, separate from\nthe automatic filter handling described above.<\/p>\n<p>The export routes also have <code>\/export\/query\/&lt;id&gt;<\/code> and <code>\/export\/query?id=&lt;id&gt;<\/code>\nvariants. Their <code>type<\/code> argument is an export extension such as <code>csv<\/code>, <code>xlsx<\/code>,\nor <code>json<\/code>. Mod_export also registers the encoders as fallbacks for the normal\nresource <code>id<\/code> controller.<\/p>\n<p>Use <code>controller_export_resource<\/code> when the export belongs to a resource. It checks\nthat the resource exists and, by default, that it is visible. Use\n<code>controller_export<\/code> for an export that has no resource id.<\/p>\n<p>A custom export can bind all callbacks to one module:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">{my_export, [&quot;my&quot;, &quot;export&quot;, type, id],\n    controller_export_resource,\n    [{export_module, my_export}]}\n<\/code><\/pre>\n<p>A fixed-format export does not need a <code>type<\/code> path argument:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">{my_export_xlsx, [&quot;my&quot;, &quot;export&quot;, id],\n    controller_export_resource,\n    [{export_module, my_export}, {content_type, xlsx}]}\n<\/code><\/pre>\n<p>Useful dispatch options are:<\/p>\n<ul><li><code>export_module<\/code> - module implementing the export callbacks described below.<\/li><li><code>content_type<\/code> - a fixed extension atom, for example <code>xlsx<\/code>, or a MIME type.<\/li><li><code>is_query<\/code> - treat <code>id<\/code> as a query resource and export its result ids.<\/li><li><code>rsc_props<\/code> - resource properties or expressions used as columns by the\ntabular encoders.<\/li><li><code>header_template<\/code> and <code>row_template<\/code> - templates for the iCalendar header and\nevent rows. Their defaults are <code>ical\/feed_start.tpl<\/code> and <code>ical\/entry.tpl<\/code>.<\/li><\/ul>\n<p>CSV and XLSX requests also accept <code>?raw=1<\/code>. Without it, textual values are\nconverted from HTML to plain text. Raw mode retains the original text while\nstill applying the escaping required by the selected format.<\/p>\n<h2>Export module callbacks<\/h2>\n<p>An <code>export_module<\/code> can export any subset of the following two-argument\nfunctions. The function name is the notification record name, without an\n<code>observe_<\/code> prefix. Every callback receives the record and the request context.\nMissing callbacks return <code>undefined<\/code> and use the controller or encoder default;\nthey do not fall back to notification observers.<\/p>\n<p>The callbacks are called in these phases:<\/p>\n<ol><li><code>export_resource_visible\/2<\/code> authorizes the request. Return <code>true<\/code>, <code>false<\/code>,\nor <code>undefined<\/code>. The resource controller defaults to resource visibility; the\ncontroller without a resource defaults to allowed.<\/li><li><code>export_resource_content_type\/2<\/code> can return <code>{ok, MimeType}<\/code>. A dispatch\n<code>content_type<\/code> takes precedence, followed by this callback and then the\n<code>type<\/code> request argument.<\/li><li><code>export_resource_content_disposition\/2<\/code> returns <code>{ok, &lt;&lt;&quot;attachment&quot;&gt;&gt;}<\/code> or\n<code>{ok, &lt;&lt;&quot;inline&quot;&gt;&gt;}<\/code>. The default is attachment.<\/li><li><code>export_resource_filename\/2<\/code> returns <code>{ok, Filename}<\/code>. Mod_export adds or\ncorrects the extension for the selected encoder.<\/li><li><code>export_resource_header\/2<\/code> returns <code>{ok, Header}<\/code> or\n<code>{ok, Header, ExporterState}<\/code>. For tabular formats, <code>Header<\/code> is normally a\nlist of column names.<\/li><li><code>export_resource_data\/2<\/code> returns <code>{ok, Rows}<\/code> or\n<code>{ok, Rows, NextExporterState}<\/code>. <code>Rows<\/code> is a list. Return the next state to\nfetch another batch; use <code>undefined<\/code> as the next state for the final batch.\nIf this callback is absent, a resource export emits its resource id as the\nonly row and an export without a resource emits no rows.<\/li><li><code>export_resource_encode\/2<\/code> is called once for each item returned by the data\ncallback. Return <code>{ok, Row}<\/code> or <code>{ok, Row, NextExporterState}<\/code> to transform\nit into an encoder row. If absent, the item is sent directly to the encoder.<\/li><li><code>export_resource_footer\/2<\/code> is called after the last row and can return\n<code>{ok, Footer}<\/code>. It is also the place to clean up resources held in the\nexporter state.<\/li><\/ol>\n<p>The <code>dispatch<\/code>, <code>id<\/code>, and <code>content_type<\/code> fields identify the request. The data,\nencode, and footer records also contain <code>state<\/code>. A small tabular exporter can\ntherefore look like this:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">-include_lib(&quot;zotonic_core\/include\/zotonic.hrl&quot;).\n\nexport_resource_visible(#export_resource_visible{}, Context) -&gt;\n    z_acl:is_admin(Context).\n\nexport_resource_header(#export_resource_header{}, _Context) -&gt;\n    {ok, [&lt;&lt;&quot;Name&quot;&gt;&gt;, &lt;&lt;&quot;Count&quot;&gt;&gt;], first_page}.\n\nexport_resource_data(#export_resource_data{state = first_page}, _Context) -&gt;\n    {ok, [[&lt;&lt;&quot;Example&quot;&gt;&gt;, 10]], undefined}.\n<\/code><\/pre>\n<p>Do not put authorization solely in templates or links. In particular,\n<code>controller_export<\/code> has no resource whose visibility can be used as a default,\nso a private export must implement <code>export_resource_visible\/2<\/code>.<\/p>\n<h2>Notification-based data providers<\/h2>\n<p>As an alternative to <code>export_module<\/code>, omit that dispatch option and listen for\nthe first-notification variants of the same phases. Observer functions have the\nusual <code>observe_<\/code> prefix, for example:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">observe_export_resource_header(\n        #export_resource_header{dispatch = my_export},\n        _Context) -&gt;\n    {ok, [&lt;&lt;&quot;Name&quot;&gt;&gt;, &lt;&lt;&quot;Count&quot;&gt;&gt;], first_page};\nobserve_export_resource_header(#export_resource_header{}, _Context) -&gt;\n    undefined.\n\nobserve_export_resource_data(\n        #export_resource_data{dispatch = my_export, state = first_page},\n        _Context) -&gt;\n    {ok, [[&lt;&lt;&quot;Example&quot;&gt;&gt;, 10]], undefined};\nobserve_export_resource_data(#export_resource_data{}, _Context) -&gt;\n    undefined.\n<\/code><\/pre>\n<p>The available notifications are <code>#export_resource_visible{}<\/code>,\n<code>#export_resource_content_type{}<\/code>, <code>#export_resource_content_disposition{}<\/code>,\n<code>#export_resource_filename{}<\/code>, <code>#export_resource_header{}<\/code>,\n<code>#export_resource_data{}<\/code>, <code>#export_resource_encode{}<\/code>, and\n<code>#export_resource_footer{}<\/code>. They are first notifications, so observers must\nmatch their own dispatch and return <code>undefined<\/code> for exports they do not handle.\nThis style is useful when an existing module wants to augment exports without a\ndedicated callback module.<\/p>\n<h2>Export formats<\/h2>\n<table class=\"table\"><thead><tr><th>Extension<\/th><th>MIME type<\/th><th>Row representation and notes<\/th><\/tr><\/thead><tbody><tr><td><code>csv<\/code><\/td><td><code>text\/csv<\/code><\/td><td>Tabular data. A header list defines the columns.<\/td><\/tr><tr><td><code>xlsx<\/code><\/td><td><code>application\/vnd.openxmlformats-officedocument.spreadsheetml.sheet<\/code><\/td><td>Tabular workbook preserving numbers and dates. Cells can have a background color.<\/td><\/tr><tr><td><code>json<\/code><\/td><td><code>application\/json<\/code><\/td><td>JSON array. List rows are combined with header names; map rows remain objects. Duplicate or empty header names are made unique. Dates are rendered in ISO form.<\/td><\/tr><tr><td><code>atom<\/code><\/td><td><code>application\/atom+xml<\/code><\/td><td>Atom feed. Integer rows are resource ids rendered with category-specific <code>atom\/entry.tpl<\/code> templates.<\/td><\/tr><tr><td><code>rss<\/code><\/td><td><code>application\/rss+xml<\/code><\/td><td>RSS 2.0, using category-specific <code>rss\/entry.tpl<\/code> templates.<\/td><\/tr><tr><td><code>jsonfeed<\/code><\/td><td><code>application\/feed+json<\/code><\/td><td>JSON Feed 1.1, with visible resource ids as rows. Downloads use <code>.json<\/code>.<\/td><\/tr><tr><td><code>ics<\/code><\/td><td><code>text\/calendar<\/code><\/td><td>iCalendar stream rendered with <code>ical\/feed_start.tpl<\/code> and <code>ical\/entry.tpl<\/code>.<\/td><\/tr><tr><td><code>bert<\/code><\/td><td><code>application\/x-bert<\/code><\/td><td>Erlang external term containing full resource maps for integer resource-id rows.<\/td><\/tr><tr><td><code>ubf<\/code><\/td><td><code>text\/x-ubf<\/code><\/td><td>UBF stream containing resource maps for integer resource-id rows.<\/td><\/tr><\/tbody><\/table>\n<p>For XLSX presentation metadata, wrap a value with\n<code>export_encoder:cell(Value, #{background_color =&gt; &lt;&lt;&quot;#RRGGBB&quot;&gt;&gt;})<\/code>. Encoders\nwithout cell styling, including CSV and JSON, export the unwrapped value.<\/p>\n<p>Adding a wire format requires an encoder module with <code>extension\/0<\/code>, <code>mime\/0<\/code>,\n<code>init\/2<\/code>, <code>header\/3<\/code>, <code>row\/3<\/code>, and <code>footer\/3<\/code>, and adding the module to\n<code>export_encoder:encoders\/0<\/code>. Application export modules normally implement only\nthe data-provider callbacks above.<\/p>\n<h2>Accepted events<\/h2>\n<p>This module handles the following notifier callbacks:<\/p>\n<ul><li><code>observe_content_types_dispatch\/3<\/code> adds encoder content types to the <code>id<\/code>\ncontroller as fallbacks.<\/li><li><code>observe_export_resource_content_disposition\/2<\/code> returns <code>inline<\/code> for Atom, RSS and JSON Feed\nquery feeds and <code>attachment<\/code> for other generated exports.<\/li><\/ul>","slug":"mod_export","doc_module_config":[{"default":"50000","description":"Maximum rows per query export page for streaming formats. Must be positive; missing or invalid values use 50000.","has_default":true,"key":"query_max_rows_streaming","module":"mod_export","type":"integer"},{"default":"1000","description":"Maximum rows per query export page for XLSX and BERT, which buffer the complete export. Must be positive; missing or invalid values use 1000 for anonymous users and 20000 for logged-in users. An explicit value applies to both. Does not affect the feed page cap of 100.","has_default":true,"key":"query_max_rows_non_streaming","module":"mod_export","type":"integer"}],"is_protected":false,"visible_for":0,"tz":"UTC","language":["en"],"doc_source_hash":"f96161d9fdd49ae309f1247d7b784312062d7f877fb10093d538954a0dce0e44","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":320,"is_a":["meta","category"],"name":"module","title":"Modules","uri":"https:\/\/test.zotonic.com\/id\/320"},"doc_source_path":"apps\/zotonic_mod_export\/src\/mod_export.erl","publication_start":"2023-01-30T19:23:55Z","github_url":"https:\/\/github.com\/zotonic\/zotonic\/blob\/master\/apps\/zotonic_mod_export\/src\/mod_export.erl","pivot_location_lng":null,"doc_source_kind":"module","name":"doc_module_mod_export","is_unfindable":false,"is_published":true,"pivot_geocode":null,"created":"2020-05-30T05:47:31Z","uri":null,"doc_status":"current","is_dependent":false,"erlang_app":"zotonic_mod_export","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":"cd3f14df65adca114211fb2f2a57dd4edd1d62bb\n","creator_id":{"id":336,"is_a":["person","robot"],"name":"gitbot","title":"Git","uri":"https:\/\/zotonic.com\/id\/336"},"modified":"2026-10-26T14:37:25Z","title_slug":"mod_export"},"uri":"https:\/\/zotonic.com\/id\/1720","uri_template":"https:\/\/zotonic.com\/id\/:id","websub":{"hub":"https:\/\/zotonic.com\/.zotonic\/websub","topic":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/1720"}},"status":"ok"}