{"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":"Modules"}},"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":3328,"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, 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>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. A query export contains at most 50,000 matching resource ids.<\/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>The shorter variants without <code>type<\/code> and\/or <code>id<\/code> are also present. The <code>type<\/code>\npath argument is a filename extension such as <code>csv<\/code>, <code>xlsx<\/code>, or <code>json<\/code>. Without\na type, <code>controller_export_resource<\/code> uses HTTP content negotiation. Mod_export\nalso registers the encoders as fallbacks for the normal resource <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>_vcalendar_header.tpl<\/code> and <code>_vevent.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>ics<\/code><\/td><td><code>text\/calendar<\/code><\/td><td>iCalendar stream rendered with <code>_vcalendar_header.tpl<\/code> and <code>_vevent.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\nquery feeds and <code>attachment<\/code> for other generated exports.<\/li><\/ul>","slug":"mod_export","is_protected":false,"visible_for":0,"tz":"UTC","language":["en"],"doc_source_hash":"861fb291995ad31623deb5f4bf0b12f823bbd2ca9ab5d7e6e36e3379d0499019","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":"66cb145eae7002b184e7d8c4c7bba5e53e8248e5\n","creator_id":{"id":336,"is_a":["person","robot"],"name":"gitbot","title":"Git","uri":"https:\/\/zotonic.com\/id\/336"},"modified":"2026-10-02T12:40:19Z","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"}