{"database": "fieldnotes", "table": "content", "is_view": false, "human_description_en": "", "rows": [["links/sqlite-as-an-application-file-format.md", "sqlite-as-an-application-file-format", "links", "link", "SQLite as an application file format", "https://www.sqlite.org/appfileformat.html", null, null, "https://simonwillison.net/2022/Oct/23/datasette-gunicorn/", "[\"sqlite\",\"architecture\",\"preservation\"]", "SQLite's case for using a database as a document format is also a useful test for publishing systems: can the generated artifact be copied, inspected, and read without keeping an application server alive? A single database makes the search index disposable without making it opaque.", "<p>SQLite&rsquo;s case for using a database as a document format is also a useful test for publishing systems: can the generated artifact be copied, inspected, and read without keeping an application server alive? A single database makes the search index disposable without making it opaque.</p>", "SQLite\u2019s case for using a database as a document format is also a useful test for publishing systems: can the generated artifact be copied, inspected, and read without keeping an application server alive? A single database makes the search index disposable without making it opaque.", "2026-07-29T00:00:00+00:00", "2026-07-29T00:00:00+00:00", "2026-08-03T16:37:51+10:00", "2026-08-03T06:37:51+00:00"], ["practice/leave-a-trail-for-future-you.md", "leave-a-trail-for-future-you", "practice", "note", null, null, null, null, null, "[\"practice\",\"maintenance\"]", "Write the recovery command while the system is still working. Future you needs a trail of dry stones across the river, not a perfect map drawn from memory.", "<p>Write the recovery command while the system is still working. Future you needs a trail of dry stones across the river, not a perfect map drawn from memory.</p>", "Write the recovery command while the system is still working. Future you needs a trail of dry stones across the river, not a perfect map drawn from memory.", "2026-07-24T00:00:00+00:00", "2026-07-24T00:00:00+00:00", "2026-08-03T16:37:51+10:00", "2026-08-03T06:37:51+00:00"], ["reading/building-for-good.md", "building-for-good", "reading", "quote", null, null, "John Ruskin, The Seven Lamps of Architecture", "https://www.gutenberg.org/ebooks/35898", null, "[\"craft\",\"durability\",\"reading\"]", "> When we build, let us think that we build for ever.", "<blockquote>\n<p>When we build, let us think that we build for ever.</p>\n</blockquote>", "When we build, let us think that we build for ever.", "2026-07-17T00:00:00+00:00", "2026-07-17T00:00:00+00:00", "2026-08-03T16:37:51+10:00", "2026-08-03T06:37:51+00:00"], ["systems/a-small-publishing-system-that-can-last.md", "a-small-publishing-system-that-can-last", "systems", "post", "A small publishing system that can last", null, null, null, null, "[\"datasette\",\"sqlite\",\"publishing\",\"durability\"]", "A personal archive should be easier to leave than to maintain. This one starts with Markdown, Git, and a database we are always willing to throw away.\n\n## Begin with the thing worth keeping\n\nPublishing software invites a familiar reversal: the durable writing becomes trapped inside the most temporary part of the system. An editor changes direction, a database reaches the end of its supported life, or a hosting company disappears. The pages are still visible, perhaps, but the source has become an export job.\n\nFieldnotes makes a smaller bet. Each entry is an ordinary text file with a short YAML header. Directories supply topics; filenames supply stable slugs; Git remembers the edits. The web database is useful, but it is not precious. If it vanishes, one command reconstructs it from the files.\n\n![A diagram showing Markdown flowing through a local build into SQLite and then a readable website.](/assets/images/publishing-field-map.svg){: .mat}\n\nThe distinction is architectural, but it also changes the writing habit. A note can begin in any text editor. It can be searched with familiar command-line tools, copied without an export format, and understood by someone who has never seen the site.\n\n## Build offline, on purpose\n\nThe renderer has no reason to call a remote service. Python-Markdown turns the body into HTML, Pygments colours code locally, and SQLite stores the result beside the original source. The build begins with an empty temporary database and atomically replaces the old artifact only after every entry succeeds.\n\nThat rule makes failure pleasantly boring. A malformed date cannot leave half an archive behind, and two builds from the same tree produce the same database dump. A simplified version of the loop looks like this:\n\n~~~python\nfrom pathlib import Path\n\ndef discover_entries(root: Path) -> list[Path]:\n    \"\"\"Return content in a stable order before rendering.\"\"\"\n    return sorted(root.glob(\"**/*.md\"), key=lambda path: path.as_posix())\n\nfor source in discover_entries(Path(\"content\")):\n    entry = parse_entry(source)\n    if not entry.draft:\n        write_row(render(entry))\n~~~\n\n![Black-on-white line art showing sorted source files entering a fresh database build before an atomic replacement.](/assets/images/durable-pipeline-line-art.svg){: .invert .knockout}\n\nThe sort is modest but important. Reproducibility is usually the accumulated result of such modest decisions: explicit timestamps, canonical JSON, a known schema, and no dependence on whatever a network service happens to return today.\n\n## Keep each layer replaceable\n\nThere are only three layers in the reading path, and each has a different expected lifetime.\n\n| Layer | What it owns | Replacement test |\n| --- | --- | --- |\n| Markdown in Git | Words, metadata, history | Opens in a plain text editor |\n| Build script | Rendering and the search index | Rebuilds from a clean checkout |\n| Datasette site | Routes and reading experience | Can be replaced without editing content |\n\nSQLite is especially well suited to the middle of this arrangement. It is a single, inspectable file with excellent full-text search, yet it does not ask to become the source of truth. Datasette then turns that file into both a website and a useful JSON interface. Neither component needs write access in public.\n\nThis separation also gives the design room to have a point of view. The stored HTML can be set like a literary journal today and rendered by some future tool tomorrow. Presentation is taken seriously without being confused for preservation.\n\n## Let Git carry time\n\nDates sound simple until an archive outlives its first migration. Filesystem modification times are fragile: copying a directory can rewrite them all. Git history is a better record, so the first commit supplies the creation date and the latest commit supplies the update date. Frontmatter remains an explicit override for imported or deliberately backdated work.\n\nThere is one hard edge worth keeping: if neither frontmatter nor Git can supply a creation date, the build stops loudly. Publishing an entry with invented time would make the archive look complete while quietly corrupting its chronology.\n\n## Make the public surface smaller\n\nThe website is read-only. It exposes clean entry pages, topic indexes, feeds, full-text search, and Datasette's JSON views, but arbitrary SQL is disabled. Authoring belongs on the other side of the Git boundary, where a person\u2014or later, a tightly scoped publishing tool\u2014creates a Markdown file and commits it.\n\nThat is the whole system: text files as the heirloom, a deterministic database as the catalogue, and a deliberately quiet website as the reading room. Small enough to understand is not a concession here. It is the feature most likely to keep the work readable in twenty years.", "<p>A personal archive should be easier to leave than to maintain. This one starts with Markdown, Git, and a database we are always willing to throw away.</p>\n<h2 id=\"begin-with-the-thing-worth-keeping\"><a class=\"heading-anchor\" href=\"#begin-with-the-thing-worth-keeping\" title=\"Link to this section\">\u00a7</a>Begin with the thing worth keeping</h2>\n<p>Publishing software invites a familiar reversal: the durable writing becomes trapped inside the most temporary part of the system. An editor changes direction, a database reaches the end of its supported life, or a hosting company disappears. The pages are still visible, perhaps, but the source has become an export job.</p>\n<p>Fieldnotes makes a smaller bet. Each entry is an ordinary text file with a short YAML header. Directories supply topics; filenames supply stable slugs; Git remembers the edits. The web database is useful, but it is not precious. If it vanishes, one command reconstructs it from the files.</p>\n<p><img alt=\"A diagram showing Markdown flowing through a local build into SQLite and then a readable website.\" class=\"mat\" src=\"/assets/images/publishing-field-map.svg\"></p>\n<p>The distinction is architectural, but it also changes the writing habit. A note can begin in any text editor. It can be searched with familiar command-line tools, copied without an export format, and understood by someone who has never seen the site.</p>\n<h2 id=\"build-offline-on-purpose\"><a class=\"heading-anchor\" href=\"#build-offline-on-purpose\" title=\"Link to this section\">\u00a7</a>Build offline, on purpose</h2>\n<p>The renderer has no reason to call a remote service. Python-Markdown turns the body into HTML, Pygments colours code locally, and SQLite stores the result beside the original source. The build begins with an empty temporary database and atomically replaces the old artifact only after every entry succeeds.</p>\n<p>That rule makes failure pleasantly boring. A malformed date cannot leave half an archive behind, and two builds from the same tree produce the same database dump. A simplified version of the loop looks like this:</p>\n<div class=\"highlight\"><pre><span></span><code><span class=\"kn\">from</span><span class=\"w\"> </span><span class=\"nn\">pathlib</span><span class=\"w\"> </span><span class=\"kn\">import</span> <span class=\"n\">Path</span>\n\n<span class=\"k\">def</span><span class=\"w\"> </span><span class=\"nf\">discover_entries</span><span class=\"p\">(</span><span class=\"n\">root</span><span class=\"p\">:</span> <span class=\"n\">Path</span><span class=\"p\">)</span> <span class=\"o\">-&gt;</span> <span class=\"nb\">list</span><span class=\"p\">[</span><span class=\"n\">Path</span><span class=\"p\">]:</span>\n<span class=\"w\">    </span><span class=\"sd\">&quot;&quot;&quot;Return content in a stable order before rendering.&quot;&quot;&quot;</span>\n    <span class=\"k\">return</span> <span class=\"nb\">sorted</span><span class=\"p\">(</span><span class=\"n\">root</span><span class=\"o\">.</span><span class=\"n\">glob</span><span class=\"p\">(</span><span class=\"s2\">&quot;**/*.md&quot;</span><span class=\"p\">),</span> <span class=\"n\">key</span><span class=\"o\">=</span><span class=\"k\">lambda</span> <span class=\"n\">path</span><span class=\"p\">:</span> <span class=\"n\">path</span><span class=\"o\">.</span><span class=\"n\">as_posix</span><span class=\"p\">())</span>\n\n<span class=\"k\">for</span> <span class=\"n\">source</span> <span class=\"ow\">in</span> <span class=\"n\">discover_entries</span><span class=\"p\">(</span><span class=\"n\">Path</span><span class=\"p\">(</span><span class=\"s2\">&quot;content&quot;</span><span class=\"p\">)):</span>\n    <span class=\"n\">entry</span> <span class=\"o\">=</span> <span class=\"n\">parse_entry</span><span class=\"p\">(</span><span class=\"n\">source</span><span class=\"p\">)</span>\n    <span class=\"k\">if</span> <span class=\"ow\">not</span> <span class=\"n\">entry</span><span class=\"o\">.</span><span class=\"n\">draft</span><span class=\"p\">:</span>\n        <span class=\"n\">write_row</span><span class=\"p\">(</span><span class=\"n\">render</span><span class=\"p\">(</span><span class=\"n\">entry</span><span class=\"p\">))</span>\n</code></pre></div>\n\n<p><img alt=\"Black-on-white line art showing sorted source files entering a fresh database build before an atomic replacement.\" class=\"invert knockout\" src=\"/assets/images/durable-pipeline-line-art.svg\"></p>\n<p>The sort is modest but important. Reproducibility is usually the accumulated result of such modest decisions: explicit timestamps, canonical JSON, a known schema, and no dependence on whatever a network service happens to return today.</p>\n<h2 id=\"keep-each-layer-replaceable\"><a class=\"heading-anchor\" href=\"#keep-each-layer-replaceable\" title=\"Link to this section\">\u00a7</a>Keep each layer replaceable</h2>\n<p>There are only three layers in the reading path, and each has a different expected lifetime.</p>\n<table>\n<thead>\n<tr>\n<th>Layer</th>\n<th>What it owns</th>\n<th>Replacement test</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Markdown in Git</td>\n<td>Words, metadata, history</td>\n<td>Opens in a plain text editor</td>\n</tr>\n<tr>\n<td>Build script</td>\n<td>Rendering and the search index</td>\n<td>Rebuilds from a clean checkout</td>\n</tr>\n<tr>\n<td>Datasette site</td>\n<td>Routes and reading experience</td>\n<td>Can be replaced without editing content</td>\n</tr>\n</tbody>\n</table>\n<p>SQLite is especially well suited to the middle of this arrangement. It is a single, inspectable file with excellent full-text search, yet it does not ask to become the source of truth. Datasette then turns that file into both a website and a useful JSON interface. Neither component needs write access in public.</p>\n<p>This separation also gives the design room to have a point of view. The stored HTML can be set like a literary journal today and rendered by some future tool tomorrow. Presentation is taken seriously without being confused for preservation.</p>\n<h2 id=\"let-git-carry-time\"><a class=\"heading-anchor\" href=\"#let-git-carry-time\" title=\"Link to this section\">\u00a7</a>Let Git carry time</h2>\n<p>Dates sound simple until an archive outlives its first migration. Filesystem modification times are fragile: copying a directory can rewrite them all. Git history is a better record, so the first commit supplies the creation date and the latest commit supplies the update date. Frontmatter remains an explicit override for imported or deliberately backdated work.</p>\n<p>There is one hard edge worth keeping: if neither frontmatter nor Git can supply a creation date, the build stops loudly. Publishing an entry with invented time would make the archive look complete while quietly corrupting its chronology.</p>\n<h2 id=\"make-the-public-surface-smaller\"><a class=\"heading-anchor\" href=\"#make-the-public-surface-smaller\" title=\"Link to this section\">\u00a7</a>Make the public surface smaller</h2>\n<p>The website is read-only. It exposes clean entry pages, topic indexes, feeds, full-text search, and Datasette&rsquo;s JSON views, but arbitrary SQL is disabled. Authoring belongs on the other side of the Git boundary, where a person\u2014or later, a tightly scoped publishing tool\u2014creates a Markdown file and commits it.</p>\n<p>That is the whole system: text files as the heirloom, a deterministic database as the catalogue, and a deliberately quiet website as the reading room. Small enough to understand is not a concession here. It is the feature most likely to keep the work readable in twenty years.</p>", "A personal archive should be easier to leave than to maintain. This one starts with Markdown, Git, and a database we are always willing to throw away.", "2026-08-03T00:00:00+00:00", "2026-08-03T00:00:00+00:00", "2026-08-04T13:39:31+10:00", "2026-08-04T03:39:31+00:00"]], "truncated": false, "filtered_table_rows_count": 4, "expanded_columns": [], "expandable_columns": [], "columns": ["path", "slug", "topic", "type", "title", "url", "source", "source_url", "via", "tags", "body", "html", "summary", "created", "created_utc", "updated", "updated_utc"], "primary_keys": ["path"], "units": {}, "query": {"sql": "select path, slug, topic, type, title, url, source, source_url, via, tags, body, html, summary, created, created_utc, updated, updated_utc from content order by path limit 101", "params": {}}, "facet_results": {}, "suggested_facets": [], "next": null, "next_url": null, "private": false, "allow_execute_sql": false, "query_ms": 5.683503812178969}