<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Posts on Well-Shaped Words</title><link>https://wellshapedwords.com/posts/</link><description>Recent content in Posts on Well-Shaped Words</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><atom:link href="https://wellshapedwords.com/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>Learn how to build a DuckLake, snapshot by snapshot</title><link>https://wellshapedwords.com/posts/tutorial-ducklake/</link><pubDate>Thu, 27 Nov 2025 00:00:19 -0300</pubDate><guid>https://wellshapedwords.com/posts/tutorial-ducklake/</guid><description>&lt;p&gt;DuckDB is a democratizing influence on data analysis and engineering.
Because its creators obviously care about both design and performance,
the tool gives more people a chance to do complex data-processing tasks without the mental, financial, and computational overhead that would come with doing such jobs on a cloud platform.&lt;/p&gt;
&lt;p&gt;As I get better at using DuckDB, I also become more attentive to the nuances of how data is represented in various formats and schemas.
With better conceptual understanding, I get more ideas of how far I can go by using DuckDB as the main (if not only) tool in my stack.
So, when the DuckDB team &lt;a href="https://ducklake.select/media/ducklake/"&gt;announced its new DuckLake format&lt;/a&gt;, I looked forward to learning a new mode of working with datasets.&lt;/p&gt;
&lt;p&gt;This tutorial distills what I learned.
I&amp;rsquo;m starting from a fresh slate, so I proceed methodically, taking note of how each component changes with each data operation.&lt;/p&gt;
&lt;aside aria-label="note" class="note"&gt;
&lt;div&gt;
&lt;svg class="sign" aria-hidden="true" viewBox="0 0 41.667306 41.66729" focusable="false"&gt;
&lt;use xlink:href="#info"&gt;&lt;/use&gt;
&lt;/svg&gt;
&lt;p&gt;&lt;strong&gt;Why use DuckLake instead of DuckDB?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;After getting hands-on experience, I suggest DuckLake if one or more of these apply to you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It&amp;rsquo;s impractical to put your dataset in one file&lt;/li&gt;
&lt;li&gt;You need to control and audit data changes&lt;/li&gt;
&lt;li&gt;You need multiple connections to the database&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/aside&gt;
&lt;h2 id="short-conceptual-background"&gt;Short conceptual background&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s a little bit about Ducklake and the source data that I&amp;rsquo;m using.
Skip to &lt;a href="#procedure"&gt;procedure&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="the-components"&gt;The components&lt;/h3&gt;
&lt;figure&gt;&lt;img src="https://wellshapedwords.com/images/ducklake-architecture.png"
alt="I assume that the client and catalog coordinate to ensure the user reads and writes to the correct files."&gt;&lt;figcaption&gt;
&lt;p&gt;I assume that the client and catalog coordinate to ensure the user reads and writes to the correct files.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Ducklake has three components to think about: the client, the catalog, and the data files.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The client.&lt;/strong&gt; This is the SQL interface to model and query data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The catalog.&lt;/strong&gt; A transactional database that tracks metadata. Catalog data includes information about schema versions, data changes, and the correspondence between data files and their tables.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage.&lt;/strong&gt; The data files that store all the rows of the DuckLake tables. In practice these are parquet files on a file system or in an object store.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When the client makes a statement that updates the schema, things change in the catalog. Any time the client adds or modifies data, things change in both the catalog and the data files.
In this tutorial I try to highlight the corresponding changes that happen in each component with each modification.&lt;/p&gt;
&lt;h3 id="the-model"&gt;The model&lt;/h3&gt;
&lt;p&gt;To be somewhat realistic, I chose a dataset of SEC filings.
The SEC compiles all &lt;a href="https://www.sec.gov/data-research/sec-markets-data/financial-statement-data-sets"&gt;the statements they receive in a quarter&lt;/a&gt; in 4 related tables.&lt;/p&gt;
&lt;p&gt;For this demo, I use only the &lt;code&gt;num&lt;/code&gt; table, which records the value of transactions, and the &lt;code&gt;sub&lt;/code&gt; table, which records information about submission and the company that made it.&lt;/p&gt;
&lt;h2 id="procedure"&gt;Procedure&lt;/h2&gt;
&lt;aside aria-label="note" class="note"&gt;
&lt;div&gt;
&lt;svg class="sign" aria-hidden="true" viewBox="0 0 41.667306 41.66729" focusable="false"&gt;
&lt;use xlink:href="#info"&gt;&lt;/use&gt;
&lt;/svg&gt;
To follow along, you need a few gigabytes of free disk space.
&lt;/div&gt;
&lt;/aside&gt;
&lt;p&gt;My configuration is as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Client: DuckDB&lt;/li&gt;
&lt;li&gt;Catalog: Sqlite&lt;/li&gt;
&lt;li&gt;Storage: Files on local computer.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are many &lt;a href="https://ducklake.select/docs/stable/duckdb/usage/choosing_a_catalog_database"&gt;catalog DBs to choose from&lt;/a&gt;.
I originally used a DuckDB database for the catalog, but it can handle only one client connection.
That became annoying surprisingly quickly, so I chose SQLite for a local, multi-client experience.&lt;/p&gt;
&lt;p&gt;For a production DuckLake, &lt;a href="https://ducklake.select/docs/stable/duckdb/usage/choosing_storage"&gt;Object storage&lt;/a&gt; is a more likely choice than my local disk.&lt;/p&gt;
&lt;h3 id="prerequisites"&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;First, set up the structure for the source files.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;In a fresh directory, download one of the &lt;a href="https://www.sec.gov/data-research/sec-markets-data/financial-statement-data-sets"&gt;quarterly zip files&lt;/a&gt;—I use 2025q1. This zip is over 100MB, so downloading may take a moment.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create a directory called &lt;code&gt;sources&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Unzip the zip to the &lt;code&gt;sources&lt;/code&gt; directory.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;unzip 2025q1.zip -d sources/
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The file set up looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;.
├── 2025q1.zip
└── sources
├── num.txt
├── pre.txt
├── readme.htm
├── sub.txt
└── tag.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then install DuckDB and the relevant extensions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Install DuckDB if you haven&amp;rsquo;t already.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open a DuckDB client and install the DuckLake and SQLite extensions.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;INSTALL ducklake;
INSTALL sqlite;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="attach-the-database"&gt;Attach the database&lt;/h3&gt;
&lt;p&gt;Attach the client to a new catalog database and specify the path to the data files:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;ATTACH IF NOT EXISTS 'ducklake:sqlite:metadata.sqlite' AS ducklake_edgar
(DATA_PATH 'lake_files')
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a new catalog database file at &lt;code&gt;metadata.sqlite&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-."&gt;├── 2025q1.zip
├── metadata.sqlite &amp;lt;-- catalog
└── sources
├── num.txt
├── pre.txt
├── readme.htm
├── sub.txt
└── tag.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The catalog initializes with all the DuckLake &lt;a href="https://ducklake.select/docs/stable/specification/tables/overview"&gt;metadata tables&lt;/a&gt;.
Some tables, like &lt;code&gt;ducklake_snapshot&lt;/code&gt; and &lt;code&gt;ducklake_schema&lt;/code&gt;, already have rows.&lt;/p&gt;
&lt;p&gt;To query metadata tables, use the identifier &lt;code&gt;__ducklake_metadata_&amp;lt;DUCKLAKE_ALIAS&amp;gt;.&amp;lt;TABLE&amp;gt;&lt;/code&gt;.
For example, to query the &lt;code&gt;ducklake_snapshot&lt;/code&gt; table:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;
FROM __ducklake_metadata_ducklake_edgar.ducklake_snapshot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Appropriately, this &amp;ldquo;empty&amp;rdquo; snapshot is &amp;ldquo;Snapshot 0&amp;rdquo;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────┬───────────────────────────────┬────────────────┬─────────────────┬──────────────┐
│ snapshot_id │ snapshot_time │ schema_version │ next_catalog_id │ next_file_id │
│ int64 │ timestamp with time zone │ int64 │ int64 │ int64 │
├─────────────┼───────────────────────────────┼────────────────┼─────────────────┼──────────────┤
│ 0 │ 2025-11-22 10:40:04.206912-03 │ 0 │ 1 │ 0 │
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="create-a-schema"&gt;Create a schema&lt;/h3&gt;
&lt;p&gt;To make a logical grouping of tables and relationships, create a schema.&lt;/p&gt;
&lt;aside aria-label="note" class="note"&gt;
&lt;div&gt;
&lt;svg class="sign" aria-hidden="true" viewBox="0 0 41.667306 41.66729" focusable="false"&gt;
&lt;use xlink:href="#info"&gt;&lt;/use&gt;
&lt;/svg&gt;
The &lt;a href="https://duckdb.org/docs/stable/sql/statements/use"&gt;USE&lt;/a&gt; statements select the default DuckLake and schema to query for this session.
&lt;/div&gt;
&lt;/aside&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;USE ducklake_edgar;
CREATE SCHEMA IF NOT EXISTS fin_statements;
USE fin_statements;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Query the &lt;code&gt;ducklake_snapshot_changes&lt;/code&gt; see if anything changed:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT snapshot_id, changes_made
FROM __ducklake_metadata_ducklake_edgar.ducklake_snapshot_changes;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;┌─────────────┬─────────────────────────────────┐
│ snapshot_id │ changes_made │
│ int64 │ varchar │
├─────────────┼─────────────────────────────────┤
│ 0 │ created_schema:&amp;quot;main&amp;quot; │
│ 1 │ created_schema:&amp;quot;fin_statements&amp;quot; │
└─────────────┴─────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;More information about the schema is in the &lt;a href="https://ducklake.select/docs/stable/specification/tables/ducklake_schema"&gt;&lt;code&gt;ducklake_schema&lt;/code&gt;&lt;/a&gt; table.&lt;/p&gt;
&lt;h3 id="create-tables"&gt;Create tables&lt;/h3&gt;
&lt;p&gt;Now create the tables for the schema.
These tables correspond to the &lt;code&gt;num&lt;/code&gt; and &lt;code&gt;sub&lt;/code&gt; tables from the source dataset and follow their model.
To track data lineage, I also add &lt;code&gt;loaded_at&lt;/code&gt; and &lt;code&gt;source_file&lt;/code&gt; columns.&lt;/p&gt;
&lt;p&gt;Notice also that I wrap the &lt;code&gt;CREATE TABLE&lt;/code&gt; in &lt;code&gt;BEGIN&lt;/code&gt; and &lt;code&gt;COMMIT&lt;/code&gt; statements.
Each snapshot is tied to a &lt;a href="https://ducklake.select/docs/stable/duckdb/advanced_features/transactions"&gt;database transaction&lt;/a&gt;.
So it&amp;rsquo;s all or nothing: a new snapshot is created only if the new tables are created, and each new table is created only if all the others are created too.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;BEGIN;
CREATE TABLE IF NOT EXISTS submissions (
adsh VARCHAR,
cik INTEGER,
name VARCHAR,
sic SMALLINT,
countryba VARCHAR,
stprba VARCHAR,
cityba VARCHAR,
zipba VARCHAR,
bas1 VARCHAR,
bas2 VARCHAR,
baph VARCHAR,
countryma varchar,
stprma VARCHAR,
cityma VARCHAR,
zipma VARCHAR,
mas1 VARCHAR,
mas2 VARCHAR,
countryinc VARCHAR,
ein INTEGER,
former VARCHAR,
changed VARCHAR,
afs VARCHAR,
wksi BOOLEAN,
fye VARCHAR,
form VARCHAR,
period DATE,
fy SMALLINT,
fp VARCHAR,
filed DATE,
accepted DATETIME,
prevrpt BOOLEAN,
detail BOOLEAN,
instance VARCHAR,
nciks SMALLINT,
aciks VARCHAR,
source_file VARCHAR,
loaded_at TIMESTAMP
);
CREATE TABLE IF NOT EXISTS numeric_facts (
adsh VARCHAR,
tag VARCHAR,
version VARCHAR,
ddate DATE,
qtrs INTEGER,
uom VARCHAR,
segments VARCHAR,
coreg VARCHAR,
value DECIMAL(28,4),
footnote VARCHAR,
source_file VARCHAR,
loaded_at TIMESTAMP
);
CALL ducklake_edgar.set_commit_message('Matt Dodson', 'Creating the tables', extra_info =&amp;gt; 'https://www.sec.gov/files/financial-statement-data-sets.pdf');
COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also use a special DuckLake function to add a commit message.
To read this message and a summary of changes from one snapshot to the next,
query the &lt;code&gt;ducklake_snapshot_changes&lt;/code&gt; table:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM __ducklake_metadata_ducklake_edgar
.ducklake_snapshot_changes;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
┌─────────────┬──────────────────────┬─────────────┬─────────────────────┬────────────────────────────────────────┐
│ snapshot_id │ changes_made │ author │ commit_message │ commit_extra_info │
│ int64 │ varchar │ varchar │ varchar │ varchar │
├─────────────┼──────────────────────┼─────────────┼─────────────────────┼────────────────────────────────────────┤
│ 0 │ created_schema:&amp;quot;ma… │ NULL │ NULL │ NULL │
│ 1 │ created_schema:&amp;quot;fi… │ NULL │ NULL │ NULL │
│ 2 │ created_table:&amp;quot;fin… │ Matt Dodson │ Creating the tables │ https://www.sec.gov/files/financial-… │
└─────────────┴──────────────────────┴─────────────┴─────────────────────┴────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Besides the changes in the snapshot table, the catalog database now has data about the new tables.
Query the &lt;code&gt;ducklake_table&lt;/code&gt; table to see it:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT table_id, begin_snapshot, table_name, path
FROM __ducklake_metadata_ducklake_edgar
.ducklake_table;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;┌──────────┬────────────────┬───────────────┬────────────────┐
│ table_id │ begin_snapshot │ table_name │ path │
│ int64 │ int64 │ varchar │ varchar │
├──────────┼────────────────┼───────────────┼────────────────┤
│ 2 │ 2 │ numeric_facts │ numeric_facts/ │
│ 3 │ 2 │ submissions │ submissions/ │
└──────────┴────────────────┴───────────────┴────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice that these tables also have a path.
This path tracks where the data files for the table are in storage.
But no files exist yet, because no data has been loaded into the tables.&lt;/p&gt;
&lt;h3 id="load-data"&gt;Load data&lt;/h3&gt;
&lt;p&gt;Now load data from the source TXT files.&lt;/p&gt;
&lt;p&gt;At the time of writing, Ducklake does not support primary keys.
To avoid duplicate data, use the &lt;code&gt;MERGE INTO&lt;/code&gt; statement to &lt;a href="https://ducklake.select/docs/stable/duckdb/usage/upserting"&gt;upsert data&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To explain the logic, here&amp;rsquo;s an annotated snippet of how the transformation into &lt;code&gt;numeric_facts&lt;/code&gt; works:&lt;/p&gt;
&lt;div class="code-annotated numbered"&gt;&lt;code&gt;
&lt;span class='highlight'&gt;MERGE INTO numeric_facts&lt;/span&gt;
USING (
SELECT
adsh::VARCHAR AS adsh,
tag::VARCHAR AS tag,
version::VARCHAR AS version,
STRPTIME(ddate::VARCHAR, &amp;#39;%Y%m%d&amp;#39;)::DATE AS ddate,
qtrs::INTEGER AS qtrs,
uom::VARCHAR AS uom,
segments::VARCHAR AS segments,
coreg::VARCHAR AS coreg,
value::DECIMAL(28,4) AS value,
footnote::VARCHAR AS footnote,
&amp;#39;2025q1.zip&amp;#39;::VARCHAR AS source_file
FROM &lt;span class='highlight'&gt;read_csv(&amp;#39;sources/num.txt&amp;#39;, delim=&amp;#39;\t&amp;#39;, header=true)&lt;/span&gt;
) AS source
ON &lt;span class='highlight'&gt;numeric_facts.adsh = source.adsh&lt;/span&gt;
AND numeric_facts.tag = source.tag
AND numeric_facts.version = source.version
AND numeric_facts.ddate = source.ddate
AND numeric_facts.qtrs = source.qtrs
AND numeric_facts.uom = source.uom
&lt;span class='highlight'&gt;AND COALESCE(numeric_facts.segments, &amp;#39;&amp;#39;) = COALESCE(source.segments, &amp;#39;&amp;#39;)
&lt;/span&gt;AND COALESCE(numeric_facts.coreg, &amp;#39;&amp;#39;) = COALESCE(source.coreg, &amp;#39;&amp;#39;)
WHEN MATCHED THEN DO NOTHING
&lt;span class='highlight'&gt;WHEN NOT MATCHED THEN INSERT VALUES&lt;/span&gt; (
source.adsh, source.tag, source.version, source.ddate,
source.qtrs, source.uom, source.segments, source.coreg,
source.value, source.footnote,
source.source_file, CURRENT_TIMESTAMP
);
&lt;/code&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Merge the table numeric facts.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SELECT&lt;/code&gt; from the source file with some light transformation to validate data types.&lt;/li&gt;
&lt;li&gt;Use the table&amp;rsquo;s composite key to check that the row does not already exist.&lt;/li&gt;
&lt;li&gt;When checking, coalesce nulls to avoid inserting duplicates (I learned from experience).&lt;/li&gt;
&lt;li&gt;When the keys don&amp;rsquo;t match, insert a new record into the table.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here&amp;rsquo;s the full load statement.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;BEGIN;
MERGE INTO submissions
USING (
SELECT
adsh::VARCHAR AS adsh,
cik::INTEGER AS cik,
name::VARCHAR AS name,
sic::SMALLINT AS sic,
countryba::VARCHAR AS countryba,
stprba::VARCHAR AS stprba,
cityba::VARCHAR AS cityba,
zipba::VARCHAR AS zipba,
bas1::VARCHAR AS bas1,
bas2::VARCHAR AS bas2,
baph::VARCHAR AS baph,
countryma::VARCHAR AS countryma,
stprma::VARCHAR AS stprma,
cityma::VARCHAR AS cityma,
zipma::VARCHAR AS zipma,
mas1::VARCHAR AS mas1,
mas2::VARCHAR AS mas2,
countryinc::VARCHAR AS countryinc,
ein::INTEGER AS ein,
former::VARCHAR AS former,
changed::VARCHAR AS changed,
afs::VARCHAR AS afs,
wksi::BOOLEAN AS wksi,
fye::VARCHAR AS fye,
form::VARCHAR AS form,
STRPTIME(period::VARCHAR, '%Y%m%d')::DATE AS period,
fy::SMALLINT AS fy,
fp::VARCHAR AS fp,
STRPTIME(filed::VARCHAR, '%Y%m%d')::DATE AS filed,
accepted::DATETIME AS accepted,
prevrpt::BOOLEAN AS prevrpt,
detail::BOOLEAN AS detail,
instance::VARCHAR AS instance,
nciks::SMALLINT AS nciks,
aciks::VARCHAR AS aciks,
'2025q1.zip'::VARCHAR AS source_file
FROM read_csv('sources/sub.txt', delim='\t', header=true)
) AS source
ON submissions.adsh = source.adsh
WHEN MATCHED THEN DO NOTHING
WHEN NOT MATCHED THEN INSERT VALUES (
source.adsh, source.cik, source.name, source.sic,
source.countryba, source.stprba, source.cityba, source.zipba,
source.bas1, source.bas2, source.baph,
source.countryma, source.stprma, source.cityma, source.zipma,
source.mas1, source.mas2, source.countryinc, source.ein,
source.former, source.changed, source.afs, source.wksi,
source.fye, source.form, source.period, source.fy, source.fp,
source.filed, source.accepted, source.prevrpt, source.detail,
source.instance, source.nciks, source.aciks,
source.source_file, CURRENT_TIMESTAMP
);
-- Numeric facts
MERGE INTO numeric_facts
USING (
SELECT
adsh::VARCHAR AS adsh,
tag::VARCHAR AS tag,
version::VARCHAR AS version,
STRPTIME(ddate::VARCHAR, '%Y%m%d')::DATE AS ddate,
qtrs::INTEGER AS qtrs,
uom::VARCHAR AS uom,
segments::VARCHAR AS segments,
coreg::VARCHAR AS coreg,
value::DECIMAL(28,4) AS value,
footnote::VARCHAR AS footnote,
'2025q1.zip'::VARCHAR AS source_file
FROM read_csv('sources/num.txt', delim='\t', header=true)
) AS source
ON numeric_facts.adsh = source.adsh
AND numeric_facts.tag = source.tag
AND numeric_facts.version = source.version
AND numeric_facts.ddate = source.ddate
AND numeric_facts.qtrs = source.qtrs
AND numeric_facts.uom = source.uom
AND COALESCE(numeric_facts.segments, 'none') = COALESCE(source.segments, 'none')
AND COALESCE(numeric_facts.coreg, 'none') = COALESCE(source.coreg, 'none')
WHEN MATCHED THEN DO NOTHING
WHEN NOT MATCHED THEN INSERT VALUES (
source.adsh, source.tag, source.version, source.ddate,
source.qtrs, source.uom, source.segments, source.coreg,
source.value, source.footnote,
source.source_file, CURRENT_TIMESTAMP
);
CALL ducklake_edgar.set_commit_message('Matt Dodson', 'Loaded data from 2025q1.zip');
COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the tables have data. The &lt;code&gt;ducklake_table_stats&lt;/code&gt; table provides a summary:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;FROM __ducklake_metadata_ducklake_edgar
.ducklake_table_stats;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;┌──────────┬──────────────┬─────────────┬─────────────────┐
│ table_id │ record_count │ next_row_id │ file_size_bytes │
│ int64 │ int64 │ int64 │ int64 │
├──────────┼──────────────┼─────────────┼─────────────────┤
│ 2 │ 3658551 │ 3658551 │ 118205163 │
│ 3 │ 6231 │ 6231 │ 826336 │
└──────────┴──────────────┴─────────────┴─────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Loading data also created the data path, &lt;code&gt;lake_files&lt;/code&gt;, as a directory.
And this path has parquet files:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├── lake_files
│   └── fin_statements
│   ├── numeric_facts
│   │   └── ducklake-019abaa3-3089-7fd2-ad3d-fbe1e49c555a.parquet
│   └── submissions
│   └── ducklake-019abaa3-2feb-7b74-9ea0-7e7f3ed49182.parquet
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These parquet files are where the data exists.
Query the metadata about them through the &lt;code&gt;ducklake_data_file&lt;/code&gt; table:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT data_file_id, table_id, file_size_bytes, path, begin_snapshot, end_snapshot
FROM __ducklake_metadata_ducklake_edgar
.ducklake_data_file;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;┌──────────────┬──────────┬─────────────────┬───────────────────────────────────────────────────────┬────────────────┬──────────────┐
│ data_file_id │ table_id │ file_size_bytes │ path │ begin_snapshot │ end_snapshot │
│ int64 │ int64 │ int64 │ varchar │ int64 │ int64 │
├──────────────┼──────────┼─────────────────┼───────────────────────────────────────────────────────┼────────────────┼──────────────┤
│ 0 │ 2 │ 118205163 │ ducklake-019abaa3-3089-7fd2-ad3d-fbe1e49c555a.parquet │ 3 │ NULL │
│ 1 │ 3 │ 826336 │ ducklake-019abaa3-2feb-7b74-9ea0-7e7f3ed49182.parquet │ 3 │ NULL │
└──────────────┴──────────┴─────────────────┴───────────────────────────────────────────────────────┴────────────────┴──────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Time to query the actual data.&lt;/p&gt;
&lt;h3 id="query"&gt;Query&lt;/h3&gt;
&lt;p&gt;Query the top three firms that reported the most profit on their annual report for &lt;code&gt;2024-12-31&lt;/code&gt; (and who filed an annual report in this batch of SEC data).&lt;/p&gt;
&lt;p&gt;This query joins the two tables to relate the numeric entries with the information about the company that submitted the entry.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT name, value / 1000000 &amp;quot;net_income_millions&amp;quot;, ddate period_end, filed date_filed
FROM numeric_facts num
INNER JOIN submissions sub ON num.adsh = sub.adsh
WHERE tag = 'NetIncomeLoss'
AND form='10-K'
AND uom='USD' AND segments IS NULL
AND ddate='2024-12-31'
ORDER BY net_income_millions DESC
LIMIT 3;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
┌────────────────────────┬─────────────────────┬────────────┬────────────┐
│ name │ net_income_millions │ period_end │ date_filed │
│ varchar │ double │ date │ date │
├────────────────────────┼─────────────────────┼────────────┼────────────┤
│ ALPHABET INC. │ 100118.0 │ 2024-12-31 │ 2025-02-05 │
│ BERKSHIRE HATHAWAY INC │ 88995.0 │ 2024-12-31 │ 2025-02-24 │
│ META PLATFORMS, INC. │ 62360.0 │ 2024-12-31 │ 2025-01-30 │
└────────────────────────┴─────────────────────┴────────────┴────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The actual process of working with and cleaning this dataset is a topic of its own.
In short, firms have different fiscal years and apparently have full discretion over how they tag each financial statement.
Take care when comparing data across firms and submissions.&lt;/p&gt;
&lt;p&gt;Anyway, this article is about DuckLake, so let&amp;rsquo;s get back to the metadata.&lt;/p&gt;
&lt;h3 id="delete"&gt;Delete&lt;/h3&gt;
&lt;p&gt;Intel has performed quite poorly and frankly I&amp;rsquo;d prefer to think it never existed.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s delete all Intel entries from the &lt;code&gt;numeric_facts&lt;/code&gt; table:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;BEGIN;
DELETE FROM numeric_facts
USING submissions sub
WHERE numeric_facts.adsh = sub.adsh
AND sub.name ILIKE '%INTEL%';
CALL ducklake_edgar.set_commit_message('Matt Dodson', 'Getting rid of Intel data');
COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To confirm:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT count(*) FROM numeric_facts num
INNER JOIN submissions sub
ON sub.adsh = num.adsh
WHERE sub.name ILIKE '%INTEL%';
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;┌──────────────┐
│ count_star() │
│ int64 │
├──────────────┤
│ 0 │
└──────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This delete creates a new snapshot:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT snapshot_id, changes_made, commit_message
FROM __ducklake_metadata_ducklake_edgar
.ducklake_snapshot_changes
ORDER BY snapshot_id DESC
LIMIT 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────┬──────────────────────┬───────────────────────────┐
│ snapshot_id │ changes_made │ commit_message │
│ int64 │ varchar │ varchar │
├─────────────┼──────────────────────┼───────────────────────────┤
│ 4 │ deleted_from_table:2 │ Getting rid of Intel data │
└─────────────┴──────────────────────┴───────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Interesting. The delete also &lt;em&gt;creates&lt;/em&gt; a new file in storage.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lake_files/fin_statements/numeric_facts
├── ducklake-019abaa3-3089-7fd2-ad3d-fbe1e49c555a.parquet
└── ducklake-019abac9-2488-7301-be20-09fd7b27f0d6-delete.parquet
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a record of these &lt;em&gt;delete&lt;/em&gt; files, query the &lt;code&gt;ducklake_delete_file&lt;/code&gt; table:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT delete_file_id, table_id, delete_count
FROM __ducklake_metadata_ducklake_edgar
.ducklake_delete_file;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;┌────────────────┬──────────┬──────────────┐
│ delete_file_id │ table_id │ delete_count │
│ int64 │ int64 │ int64 │
├────────────────┼──────────┼──────────────┤
│ 2 │ 2 │ 4885 │
└────────────────┴──────────┴──────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In summary, all data changes are tracked at each snapshot.
This might be useful.&lt;/p&gt;
&lt;h3 id="time-travel"&gt;Time travel&lt;/h3&gt;
&lt;p&gt;As it turns out, my delete operation was overzealous.
With my sloppy &lt;code&gt;WHERE&lt;/code&gt; clause, I deleted not only the records for Intel but also all records for all companies with something about &amp;ldquo;intelligence&amp;rdquo; in their name.
Besides, I discovered that deleting unpleasant data does not necessarily delete unpleasant emotions.&lt;/p&gt;
&lt;p&gt;Fortunately, DuckLake snapshots preserve the data changes.
Using the &lt;a href="https://ducklake.select/docs/stable/duckdb/usage/time_travel"&gt;time travel&lt;/a&gt; feature, I can query old snapshots.&lt;/p&gt;
&lt;p&gt;From the &lt;code&gt;snapshot_changes&lt;/code&gt; table, I see that the delete happened in Snapshot 4.
To get the data before the delete, query with &lt;code&gt;AT (VERSION =&amp;gt; 3)&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT count(*) FROM numeric_facts num AT (VERSION =&amp;gt; 3)
INNER JOIN submissions sub
ON sub.adsh = num.adsh
WHERE sub.name ILIKE '%INTEL%';
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;┌──────────────┐
│ count_star() │
│ int64 │
├──────────────┤
│ 4885 │
└──────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can review all changes from one snapshot to the other with the &lt;a href="https://ducklake.select/docs/stable/duckdb/advanced_features/data_change_feed"&gt;Data change feed&lt;/a&gt;
function.&lt;/p&gt;
&lt;h3 id="recover"&gt;Recover&lt;/h3&gt;
&lt;p&gt;Here&amp;rsquo;s a short way to restore &lt;code&gt;numeric_facts&lt;/code&gt; to the table from an old snapshot (thanks to a user on the DuckDB discord for the tip).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;BEGIN;
CREATE OR REPLACE TABLE numeric_facts AS (FROM numeric_facts AT (version =&amp;gt; 3));
CALL ducklake_edgar.set_commit_message('Matt Dodson', 'Going back to the way things were');
COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="find-old-data-files"&gt;Find old data files&lt;/h3&gt;
&lt;p&gt;With all the changes, some of my data files are no longer needed.
You can expect this by checking the &lt;a href="https://ducklake.select/docs/stable/specification/tables/ducklake_data_file"&gt;&lt;code&gt;ducklake_data_file&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For example, the data in file 0 was relevant between snapshots 3 and 5:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;FROM __ducklake_metadata_ducklake_edgar.ducklake_data_file
WHERE end_snapshot IS NOT NULL;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;┌──────────────┬──────────┬────────────────┬──────────────┬
│ data_file_id │ table_id │ begin_snapshot │ end_snapshot │
│ int64 │ int64 │ int64 │ int64 │
├──────────────┼──────────┼────────────────┼──────────────┤
│ 0 │ 2 │ 3 │ 5 │
└──────────────┴──────────┴────────────────┴──────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="maintain"&gt;Maintain&lt;/h3&gt;
&lt;p&gt;One thing that surprised me at first was that until the user takes action, the number of total data files grows with &lt;a href="https://ducklake.select/docs/stable/duckdb/advanced_features/data_inlining"&gt;almost every&lt;/a&gt; write operation.
Besides delete files, inserting more data into a table adds a data file (and the catalog tracks how the rows start and end across files)&lt;/p&gt;
&lt;p&gt;Now I understand that this write-only approach is the normal way of working with parquet and data lakes.
Besides preserving history and read and write performance,
object storage is cheap.&lt;/p&gt;
&lt;p&gt;Nevertheless, the structure of the data files can weigh on performance and costs.
The DuckDB extension has a number of &lt;a href="https://ducklake.select/docs/stable/duckdb/maintenance/recommended_maintenance"&gt;maintenance functions&lt;/a&gt; to manage this.&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;I enjoyed learning about DuckLake.
Even if I just continue to use it locally, the catalog and multi-client experience make it a nice alternative to a &amp;ldquo;classic&amp;rdquo; DuckDB database file.&lt;/p&gt;
&lt;p&gt;With no implementation experience and only a vague idea of how data lakes and catalogs work in general, it took me some time to wrap my head around how DuckLake works as an application and data architecture.
I feel I have a solid intuition of how things work, and I hope this tutorial has helped you feel the same way.&lt;/p&gt;
&lt;h2 id="ideas-to-go-further"&gt;Ideas to go further&lt;/h2&gt;
&lt;p&gt;To learn more about how DuckLake works, try these operations and explore how the changes manifest in the catalog and storage.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Load more data&lt;/li&gt;
&lt;li&gt;Document your tables with comments&lt;/li&gt;
&lt;li&gt;Do an &lt;code&gt;UPDATE&lt;/code&gt; (check for snapshot expirations)&lt;/li&gt;
&lt;li&gt;Alter a table (what metadata tables will change?)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ducklake.select/docs/stable/duckdb/usage/configuration"&gt;Change the config&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For ideas about using Ducklake in the wider data engineering ecosystem, you can try these:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Change the storage to S3 or something similar&lt;/li&gt;
&lt;li&gt;Go further with the downstream transformations, building marts and views that can actually serve for financial screening and analysis&lt;/li&gt;
&lt;li&gt;Incorporate transformation tools like dbt or orchestration tools like Dagster to build full data pipelines&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Good luck!&lt;/p&gt;</description></item><item><title>Make software tutorials that stick</title><link>https://wellshapedwords.com/posts/make-tutorial-stick/</link><pubDate>Sat, 09 Aug 2025 12:51:21 -0300</pubDate><guid>https://wellshapedwords.com/posts/make-tutorial-stick/</guid><description>&lt;blockquote class="blockquote"&gt; &lt;p&gt; &lt;em&gt;Learning is deeper and more durable when it’s effortful.&lt;/em&gt; &lt;br&gt; &lt;span class="author"&gt;&amp;mdash;Make It Stick&lt;/span&gt; &lt;/p&gt; &lt;/blockquote&gt;
&lt;p&gt;This year I developed a course on an abstruse standard about manufacturing systems. To prepare for the work, I read &lt;a href="https://www.goodreads.com/book/show/18770267-make-it-stick?from_search=true&amp;amp;from_srp=true&amp;amp;qid=NjGYJpbAqp&amp;amp;rank=1"&gt;Make It Stick: The Science of Successful Learning&lt;/a&gt;, an accessible and practical book by cognitive scientists that synthesizes various studies in memory and learning.&lt;/p&gt;
&lt;p&gt;While designing a course isn&amp;rsquo;t the same as writing documentation, the principles overlap.
Tutorials, in particular, help people learn through practice.&lt;/p&gt;
&lt;p&gt;Many of the principles from &lt;em&gt;Make It Stick&lt;/em&gt;, then, also apply to making more effective tutorials.
Here are the tips I gleaned to design a better learning experience.
All quoted text is from the book.&lt;/p&gt;
&lt;div class="expandable-section"&gt;
&lt;button aria-expanded="false" data-expands="js-expandable-6808ff811db82253431b4bdd0c04b4dc"&gt;
&lt;span class="expandable-label"&gt;What is a tutorial, anyway?&lt;/span&gt;
&lt;svg aria-hidden="true" focusable="false" viewBox="0 0 70.866142 70.866141"&gt;
&lt;g transform="translate(0 -981.5)"&gt;
&lt;rect style="stroke-width:0;fill:currentColor" ry="5" height="60" width="9.8985" y="987.36" x="30.051" class="up-strut" /&gt;
&lt;rect style="stroke-width:0;fill:currentColor" ry="5" height="10" width="60" y="1012.4" x="5"/&gt;
&lt;/g&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div id="js-expandable-6808ff811db82253431b4bdd0c04b4dc" hidden&gt;
&lt;p&gt;A conventional tutorial typically offers the reader a reproducible procedure to follow, usually by copying code or commands from their docs into their environment.
This is an effective set up—it&amp;rsquo;s efficient, and following examples is one of the best ways for beginners to learn.&lt;/p&gt;
&lt;p&gt;However, some of the following ideas assume something more interactive, requiring more effort of the learner and designer. My view of what a tutorial could be is wider than the conventional &lt;code&gt;Hello World&lt;/code&gt; found in a &amp;ldquo;Get Started&amp;rdquo; section. I&amp;rsquo;m also imagining multistage documents and training that is embedded in browser-based IDEs.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="start-with-context"&gt;Start with context&lt;/h2&gt;
&lt;p&gt;Start every tutorial with a small narrative that frames a plausible problem that can be solved by applying a new skill.
As the tutorial progresses, your learner should actively participate in the construction of a solution.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Putting new knowledge into a larger context helps learning.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Skills and information are easier to recall if the learner can position what they&amp;rsquo;ve learned into a wider body of knowledge or memorable narrative.
Plus, if the example is plausible and common, the narrative serves as a training simulation for the problems that the learner will meet in real-world contexts.&lt;/p&gt;
&lt;h2 id="have-them-try-first"&gt;Have them try first&lt;/h2&gt;
&lt;p&gt;Before demonstrating solutions, give learners a chance to guess.
In practice, you could do something as simple as starting a section with a true-or-false or multiple-choice question. Of course, with a little JavaScript, the possibilities get much more interactive.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Trying to solve a problem before being taught the solution leads to better learning, even when errors are made in the attempt.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Guessing makes learners think about the problem and primes them to pay attention to the answer. And as long as you have some mechanism to give feedback, an incorrect guess isn&amp;rsquo;t a bad thing: it&amp;rsquo;s an opportunity.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Making mistakes and correcting them builds the bridges to advanced learning.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="weave-different-tasks-into-the-overall-story"&gt;Weave different tasks into the overall story&lt;/h2&gt;
&lt;p&gt;A long tutorial likely incorporates more than one task.
Rather than order the tasks in sequence, &lt;em&gt;interleave&lt;/em&gt; them.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;When you’re adept at extracting the underlying principles or “rules” that differentiate types of problems, you’re more successful at picking the right solutions in unfamiliar situations.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;figure&gt;&lt;img src="https://wellshapedwords.com/images/wellshapedwords-sequential-vs-braided-sequencing.png"
alt="Sequential vs. braided task sequencing. With interleaved tasks, spaced repetition comes for free." width="80%"&gt;&lt;figcaption&gt;
&lt;p&gt;Sequential vs. braided task sequencing. With interleaved tasks, spaced repetition comes for free.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;For example, an introduction to SQL likely covers not only the &lt;code&gt;SELECT&lt;/code&gt; statement, but also &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;CREATE&lt;/code&gt;, and so on.
The conventional approach proceeds sequentially, exhausting all there is to say about one topic before turning to another.
The more effective approach weaves these topics together. For example, the tutorial might teach learners first how to select a table, then update it, and then create a new table before finally rotating back to &lt;code&gt;SELECT&lt;/code&gt; to query the new data.&lt;/p&gt;
&lt;p&gt;Learning is more effective when the learner has a chance practice working on the problem domain from various angles.
This strategy also has the added effect of providing spaced recall.&lt;/p&gt;
&lt;h2 id="reincorporate-old-information"&gt;Reincorporate old information&lt;/h2&gt;
&lt;p&gt;As the tutorial progresses, revisit topics that you introduced earlier.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Retrieval practice—recalling facts or concepts or events from memory—is a more effective learning strategy than review by rereading.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Even if the tutorial doesn&amp;rsquo;t last long, a little bit of &lt;em&gt;spaced repetition&lt;/em&gt; can help the learners retain important aspects of it their long-term memory.
Recall is a critical part of applying skills, but you have to stop thinking about something in the first place to recall it later.&lt;/p&gt;
&lt;p&gt;Together with the interleaving technique, spacing information out helps the learner adapt the tutorial to their problem.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;if you space out practice at a task and get a little rusty between sessions, or you interleave the practice of two or more subjects,retrieval is harder and feels less productive, but the effort produces longer lasting learning and enables more versatile application of it in later settings.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="build-a-mock-test-at-the-end"&gt;Build a mock test at the end&lt;/h2&gt;
&lt;p&gt;A small assessment at the end can help reinforce the concepts.
This is the second layer of retrieval practice.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;In virtually all areas of learning, you build better mastery when you use testing as a tool to identify weaknesses and bring up your areas of weakness.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Testing in this case is not about trying to assess whether the learner knows something; it&amp;rsquo;s about helping learners remember and avoid a false sense of mastery.&lt;/p&gt;
&lt;h2 id="ask-the-user-to-elaborate"&gt;Ask the user to elaborate&lt;/h2&gt;
&lt;p&gt;After giving them a chance to practice, ask the learners some questions to get them thinking about how the information applies to their wider body of information.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;If you practice elaboration, there&amp;rsquo;s no known limit to how much you can learn. Elaboration is the process of giving new material meaning by expressing it in your own words and connecting it with what you already know.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Presumably your learners are taking the tutorial because they think the skill that it imparts might apply to some broader problem in their lives.
Ask them to think about what that application is.&lt;/p&gt;
&lt;p&gt;You could even add an input text field for answers. Even if no one reads their answers, the very act of writing to elaborate topic helps them assimilate it into their wider body of knowledge.&lt;/p&gt;
&lt;h2 id="for-learners-and-designers-more-effort-brings-better-results-and-more-effort"&gt;For learners and designers, more effort brings better results (and more effort)&lt;/h2&gt;
&lt;p&gt;A skill takes time to learn, and time is our most precious resource.
Things that are easy-to-learn are appealing objects of our attention.
Ironically, though, what&amp;rsquo;s easiest to learn is hardest to remember.
Durable skills are hard-won.&lt;/p&gt;
&lt;p&gt;However, designers of learning experiences can use these techniques to diminish the tradeoff between ease and memorability.
Efficiently designed tutorials can be both inviting to the learner and challenging enough to help them retain the information.
Of course, I don&amp;rsquo;t deny that designing tutorials in this way requires more effort on the part of the writer as well.
But all things excellent are as difficult as they are rare.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The need to understand that when learning is hard, you&amp;rsquo;re doing important work. To understand that striving and setbacks, as in any action video game or new BMX bike stunt, are essential if you are to surpass your current level of performance toward true expertise.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description></item><item><title>Shell tools for writers</title><link>https://wellshapedwords.com/posts/shell-tools-for-writers/</link><pubDate>Sun, 02 Mar 2025 16:11:17 -0300</pubDate><guid>https://wellshapedwords.com/posts/shell-tools-for-writers/</guid><description>&lt;p&gt;The command line is a text-based interface,
and writing is a text-based activity.
Shell tools, therefore, are writing tools.
Here are some examples that have served me.&lt;/p&gt;
&lt;aside aria-label="note" class="note"&gt;
&lt;div&gt;
&lt;svg class="sign" aria-hidden="true" viewBox="0 0 41.667306 41.66729" focusable="false"&gt;
&lt;use xlink:href="#info"&gt;&lt;/use&gt;
&lt;/svg&gt;
All scripts assume GNU versions and the Bash shell.
&lt;/div&gt;
&lt;/aside&gt;
&lt;h2 id="find-and-replace"&gt;Find and replace&lt;/h2&gt;
&lt;p&gt;Replace all instances of an old phrase within a set of MD files.
This works best on a Git repo so that you can review the diffs before committing any changes.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;old_term=&amp;quot;relref \&amp;quot;/&amp;quot;
new_term=&amp;quot;ref \&amp;quot;/&amp;quot;
find . -name &amp;quot;*.md&amp;quot; -exec sed -i &amp;quot;s/$old_term/$new_term/g&amp;quot; {} +
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="count-and-sort-frequency-across-files"&gt;Count and sort frequency across files&lt;/h2&gt;
&lt;p&gt;Some changes are too contextual for global search and replace.
If the fixes are made per page and incrementally, you can maximize early impact by starting on the pages that require the most changes.&lt;/p&gt;
&lt;p&gt;This &lt;code&gt;grep | sort | awk&lt;/code&gt; pipeline:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Searches for a term.&lt;/li&gt;
&lt;li&gt;Sorts the list by frequency.&lt;/li&gt;
&lt;li&gt;Filters out files with 0 occurrences.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;grep &amp;quot;relref \&amp;quot;/&amp;quot; -irc content/ \
| sort -t : -k 2 --reverse \
| awk -F : '$2 != 0'
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="sort-all-errors-by-frequency"&gt;Sort all errors by frequency&lt;/h2&gt;
&lt;p&gt;Very close in form to the preceding, this sorts all errors in a log file.
With over 200 errors to fix, this ensures time isn&amp;rsquo;t wasted having to edit the same page multiple times to fix errors.&lt;/p&gt;
&lt;p&gt;First, direct &lt;code&gt;stderr&lt;/code&gt; to a file:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;hugo server 2&amp;gt; relref-errors.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then sort the file by the third field.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;sort -t : --key=3 relref-errors.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However it was pretty messy to read.&lt;/p&gt;
&lt;p&gt;So rather than filtering I just used awk to make a CSV and then used duckdb to sort.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;echo &amp;quot;file,location,ref&amp;quot; &amp;gt; tmp.csv \
&amp;amp;&amp;amp; awk -F: '{print $3&amp;quot;\&amp;quot;&amp;quot;&amp;quot;,&amp;quot;$4&amp;quot;:&amp;quot;$5&amp;quot;,&amp;quot;$2}' relref-errors.log &amp;gt;&amp;gt; tmp.csv
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="filter-json-array-of-links-checked"&gt;Filter JSON array of links checked&lt;/h2&gt;
&lt;p&gt;The &lt;a href="https://github.com/JustinBeckwith/linkinator"&gt;Linkinator&lt;/a&gt; JSON format outputs a status report of every link on a site.&lt;/p&gt;
&lt;p&gt;Most links work and some of the broken ones stem from 500 or 429 errors, which don&amp;rsquo;t reflect link quality.
So I can use &lt;code&gt;jq&lt;/code&gt; to filter only the links that have a status of &lt;code&gt;404&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;linkinator --recurse --format JSON \
&amp;quot;https://docs.rhize.com&amp;quot; \
| jq '.links[] | select(.status == 404)'
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>Round a float to decimal places with Hugo Shortcodes</title><link>https://wellshapedwords.com/posts/hugo-shortcode-to-round-to-decimals/</link><pubDate>Sat, 28 Dec 2024 17:39:24 -0300</pubDate><guid>https://wellshapedwords.com/posts/hugo-shortcode-to-round-to-decimals/</guid><description>&lt;aside aria-label="note" class="note"&gt;
&lt;div&gt;
&lt;svg class="sign" aria-hidden="true" viewBox="0 0 41.667306 41.66729" focusable="false"&gt;
&lt;use xlink:href="#info"&gt;&lt;/use&gt;
&lt;/svg&gt;
The idea for this post came from &lt;a href="https://shaunagordon.com/"&gt;Shauna Gordon&lt;/a&gt;, who gave me some great tips on how to better handle math functions in Hugo.
Thanks, Shauna!
&lt;/div&gt;
&lt;/aside&gt;
&lt;p&gt;The built-in Go &lt;a href="https://gohugo.io/functions/math/round/"&gt;math.Round&lt;/a&gt; function rounds to integers.
So, to round a number to decimals using Hugo, you need to write a small shortcode.&lt;/p&gt;
&lt;p&gt;The following examples all use the technique described in &lt;a href="https://gosamples.dev/round-float/"&gt;Round float to any precision in Go&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &amp;ldquo;conventional&amp;rdquo; solution uses a shortcode that is available to all pages.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://gohugo.io/templates/shortcode/#inline-shortcodes"&gt;inline&lt;/a&gt; examples create a rounding function with &lt;code&gt;define&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="shortcode-to-round-floats"&gt;Shortcode to round floats&lt;/h2&gt;
&lt;p&gt;This shortcode is defined in &lt;code&gt;layouts/shortcodes/roundFlt.html&lt;/code&gt;.
It takes two arguments:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The number to round&lt;/li&gt;
&lt;li&gt;The decimal precision (default &lt;code&gt;2&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="language-go"&gt;{{ $num := (.Get 0 | float) }}
{{ $precision := default 2 (.Get 1 ) }}
{{ $ratio := math.Pow 10 $precision }}
{{ $product := math.Product $ratio $num }}
{{ div (math.Round $product) $ratio }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example call:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-markdown"&gt;
- 2 decimal places: {{&amp;lt; roundFlt 3.141592653589793238 &amp;gt;}}
- 5 decimal places: {{&amp;lt; roundFlt 3.141592653589793238 5 &amp;gt;}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Renders as:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;2 decimal places (default):
3.14
&lt;/li&gt;
&lt;li&gt;5 decimal places:
3.14159
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id="inline-shortcodes-scoped-to-one-block"&gt;Inline shortcodes scoped to one block&lt;/h2&gt;
&lt;p&gt;If you want to scope the shortcode to a page, you can use inline shortcodes with the same logic from the preceding section.
However, inline shortcodes cannot have nested shortcodes.
If you want to use the rounding function within more shortcode logic, you can use the template and define functions.&lt;/p&gt;
&lt;p&gt;Besides showing another technique, I include these because inline shortcodes have interesting use cases for pages that involve numerical analysis and presentation—for examples, read &lt;a href="https://wellshapedwords.com/posts/hugo-inline-spreadsheet/"&gt;Hugo inline spreadsheets&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="fixed-precision"&gt;Fixed precision&lt;/h3&gt;
&lt;p&gt;The least complex method keeps precision fixed to a certain number—in this case, 4.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{{ $precision := 4 }}
{{ define &amp;quot;rnd&amp;quot; }}
{{ $ratio := math.Pow 10 4 }}
{{ $product := math.Product $ratio . }}
{{ div (math.Round $product) $ratio }}
{{ end }}
Pi rounded to {{ $precision }} digits:
{{ template &amp;quot;rnd&amp;quot; math.Pi }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Renders as:&lt;/p&gt;
&lt;blockquote class="blockquote"&gt; &lt;p&gt; &lt;p&gt;Pi rounded to 4 digits:&lt;/p&gt;
&lt;p&gt;3.1416&lt;/p&gt;
&lt;/p&gt; &lt;/blockquote&gt;
&lt;h3 id="variable-precision"&gt;Variable precision&lt;/h3&gt;
&lt;p&gt;If you want variable precision, you can put the value and precision in a dict.
This is more verbose than the other examples.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-go"&gt;
{{ define &amp;quot;rnd&amp;quot; }}
{{ $num := .val }}
{{ $pow := default 2 .precision }}
{{ $ratio := math.Pow 10 $pow }}
{{ $product := math.Product $ratio $num }}
{{- div (math.Round $product) $ratio }}
{{ end }}
Pi to 5 digits:
{{- template &amp;quot;rnd&amp;quot; (dict &amp;quot;val&amp;quot; math.Pi &amp;quot;precision&amp;quot; 5 ) }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Renders as&lt;/p&gt;
&lt;blockquote class="blockquote"&gt; &lt;p&gt; &lt;p&gt;Pi to 5 digits:&lt;/p&gt;
&lt;p&gt;3.14159&lt;/p&gt;
&lt;/p&gt; &lt;/blockquote&gt;</description></item><item><title>Hugo inline spreadsheet</title><link>https://wellshapedwords.com/posts/hugo-inline-spreadsheet/</link><pubDate>Fri, 20 Dec 2024 16:00:42 -0300</pubDate><guid>https://wellshapedwords.com/posts/hugo-inline-spreadsheet/</guid><description>&lt;p&gt;With Hugo inline shortcodes, you calculate values and build complex, dynamic tables without ever leaving the Markdown page.
I discovered this technique when I wrote my post to &lt;a href="https://wellshapedwords.com/posts/chatgpt-as-book-research-assistant/"&gt;test the accuracy of a sample of ChatGPT–generated values&lt;/a&gt;. Rather than copy my calculations into the page, I realized I could use variables and Hugo built-in &lt;a href="https://gohugo.io/functions/math/"&gt;Math functions&lt;/a&gt; to keep the writing and computation in a single place.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve &lt;a href="https://wellshapedwords.com/posts/hugo-inline-shortcodes/"&gt;written before&lt;/a&gt; about inline shortcodes, but this use case might be the most interesting one I&amp;rsquo;ve found.
As an authoring experience, I find it intuitive to calculate values directly beside the accompanying prose.
And if these calculations or data presentations are unique to one page, separation of concerns is no concern of mine.&lt;/p&gt;
&lt;p&gt;This is something akin to &lt;a href="https://en.wikipedia.org/wiki/Literate_programming"&gt;literate programming&lt;/a&gt;.
Surely this technique won&amp;rsquo;t replace Jupyter notebooks, but it&amp;rsquo;s an interesting use case for Hugo pages that involve numerical research or demonstration.&lt;/p&gt;
&lt;p&gt;Here are some examples.&lt;/p&gt;
&lt;h2 id="examples-of-inline-calculation"&gt;Examples of Inline calculation&lt;/h2&gt;
&lt;p&gt;All the numbers in the following sections were calculated from an inline shortcode in the page. Open the accordion after the table to read the code.&lt;/p&gt;
&lt;h3 id="calculate-summary-statistics"&gt;Calculate summary statistics&lt;/h3&gt;
&lt;p&gt;Consider the following array of numbers:
&lt;code&gt;[100.12 140.2 134 150 102]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I can use an inline shortcode to calculate summary statistics and populate a Markdown table.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;statistic&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sum&lt;/td&gt;
&lt;td&gt;749.53&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mean&lt;/td&gt;
&lt;td&gt;124.92&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;variance&lt;/td&gt;
&lt;td&gt;348.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;std&lt;/td&gt;
&lt;td&gt;18.66&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class="expandable-section"&gt;
&lt;button aria-expanded="false" data-expands="js-expandable-b67188b393beef6786d7b84db2f33e3f"&gt;
&lt;span class="expandable-label"&gt;Inline summary statistics&lt;/span&gt;
&lt;svg aria-hidden="true" focusable="false" viewBox="0 0 70.866142 70.866141"&gt;
&lt;g transform="translate(0 -981.5)"&gt;
&lt;rect style="stroke-width:0;fill:currentColor" ry="5" height="60" width="9.8985" y="987.36" x="30.051" class="up-strut" /&gt;
&lt;rect style="stroke-width:0;fill:currentColor" ry="5" height="10" width="60" y="1012.4" x="5"/&gt;
&lt;/g&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div id="js-expandable-b67188b393beef6786d7b84db2f33e3f" hidden&gt;
&lt;pre&gt;&lt;code&gt;{{ $cost := slice 100.12 140.2 134.0 150.0 102.0 }}
{{ $cost := slice 100.12 140.2 134.0 150.0 102.0 123.21 }}
{{ $sum := math.Sum $cost }}
{{ $mean := div $sum (len $cost) }}
{{ $sumdiff := 0.0 }}
{{ range $cost }}
{{ $sumdiff = add $sumdiff (pow (sub . $mean) 2) }}
{{ end }}
{{ $variance := div $sumdiff (len $cost) }}
{{ $std := math.Sqrt $variance }}
| statistic | value |
|-----------|------------------------------------|
| sum | {{ printf &amp;quot;%.2f&amp;quot; $sum }} |
| mean | {{ printf &amp;quot;%.2f&amp;quot; $mean }} |
| variance | {{ printf &amp;quot;%.2f&amp;quot; $variance }} |
| std | {{ printf &amp;quot;%.2f&amp;quot; $std }} |
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="use-slices-to-compute-a-table-of-dynamic-values"&gt;Use slices to compute a table of dynamic values&lt;/h3&gt;
&lt;p&gt;This example compares two slices to create a &amp;ldquo;spreadsheet&amp;rdquo;.
Some conditional formatting changes the CSS of the derived cells whenever the value is less than 0.
The table footer gives a summary.&lt;/p&gt;
&lt;p&gt;In this case, I use an HTML table rather than a Markdown one. This is to avoid figuring out how to trim whitespace correctly to keep a single continuous Markdown table across a range of generated values.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Sale Price&lt;/th&gt;
&lt;th&gt;Difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100.12&lt;/td&gt;
&lt;td&gt;135.00&lt;/td&gt;
&lt;td&gt;
34.88
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;140.20&lt;/td&gt;
&lt;td&gt;135.00&lt;/td&gt;
&lt;td&gt;
&lt;span style="color:red"&gt; -5.20&lt;/span&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;134.00&lt;/td&gt;
&lt;td&gt;135.00&lt;/td&gt;
&lt;td&gt;
1.00
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;150.00&lt;/td&gt;
&lt;td&gt;200.00&lt;/td&gt;
&lt;td&gt;
50.00
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;102.00&lt;/td&gt;
&lt;td&gt;129.99&lt;/td&gt;
&lt;td&gt;
27.99
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;123.21&lt;/td&gt;
&lt;td&gt;115.15&lt;/td&gt;
&lt;td&gt;
&lt;span style="color:red"&gt; -8.06&lt;/span&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tfoot&gt;
&lt;tr&gt;
&lt;td colspan="2" style="text-align: right; font-weight: bold;"&gt;Total Profit&lt;/td&gt;
&lt;td style="font-weight: bold;"&gt;100.61&lt;/td&gt;
&lt;/tr&gt;
&lt;/tfoot&gt;
&lt;/table&gt;
&lt;div class="expandable-section"&gt;
&lt;button aria-expanded="false" data-expands="js-expandable-f1d09442cc7c6cbd92af034b982e29de"&gt;
&lt;span class="expandable-label"&gt;Inline spreadsheet&lt;/span&gt;
&lt;svg aria-hidden="true" focusable="false" viewBox="0 0 70.866142 70.866141"&gt;
&lt;g transform="translate(0 -981.5)"&gt;
&lt;rect style="stroke-width:0;fill:currentColor" ry="5" height="60" width="9.8985" y="987.36" x="30.051" class="up-strut" /&gt;
&lt;rect style="stroke-width:0;fill:currentColor" ry="5" height="10" width="60" y="1012.4" x="5"/&gt;
&lt;/g&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div id="js-expandable-f1d09442cc7c6cbd92af034b982e29de" hidden&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;
{{ $cost := slice 100.12 140.2 134.0 150.0 102.0 123.21 }}
{{ $price := slice 135.0 135.0 135.0 200.0 129.99 115.15 }}
{{ $differences := slice }}
&amp;lt;table&amp;gt;
&amp;lt;thead&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;th&amp;gt;Cost&amp;lt;/th&amp;gt;
&amp;lt;th&amp;gt;Sale Price&amp;lt;/th&amp;gt;
&amp;lt;th&amp;gt;Difference&amp;lt;/th&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;/thead&amp;gt;
&amp;lt;tbody&amp;gt;
{{ range $index, $costItem := $cost }}
{{ $priceItem := index $price $index }}
{{ $difference := sub $priceItem $costItem }}
{{ $differences = $differences | append $difference }}
&amp;lt;tr&amp;gt;
&amp;lt;td&amp;gt;{{ printf &amp;quot;%.2f&amp;quot; $costItem }}&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;{{ printf &amp;quot;%.2f&amp;quot; $priceItem }}&amp;lt;/td&amp;gt;
&amp;lt;td&amp;gt;{{ if lt $difference 0}}
&amp;lt;span style=&amp;quot;color:red&amp;quot;&amp;gt; {{ printf &amp;quot;%.2f&amp;quot; $difference }}&amp;lt;/span&amp;gt;
{{else}}
{{ printf &amp;quot;%.2f&amp;quot; $difference }}
&amp;lt;/td&amp;gt;
{{end}}
&amp;lt;/tr&amp;gt;
{{ end }}
&amp;lt;/tbody&amp;gt;
&amp;lt;tfoot&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td colspan=&amp;quot;2&amp;quot; style=&amp;quot;text-align: right; font-weight: bold;&amp;quot;&amp;gt;Total Profit&amp;lt;/td&amp;gt;
&amp;lt;td style=&amp;quot;font-weight: bold;&amp;quot;&amp;gt;{{ printf &amp;quot;%.2f&amp;quot; (math.Sum $differences) }}&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;/tfoot&amp;gt;
&amp;lt;/table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The HTML table could probably be more semantic, but this seems like a good proof of concept.&lt;/p&gt;
&lt;h3 id="embed-calculated-values-in-latex-formulas"&gt;Embed calculated values in LaTeX formulas&lt;/h3&gt;
&lt;p&gt;If you use &lt;a href="https://gohugo.io/content-management/mathematics/"&gt;Latex in Hugo&lt;/a&gt;, you can substitute the variables in your formulas with variables from an inline shortcode.&lt;/p&gt;
&lt;p&gt;The formula for margin of error is as follows:
$$ z_{\frac{\alpha}{2}}\cdot\sqrt{\dfrac{ {\hat p}(1-{\hat p})}{n}} $$&lt;/p&gt;
&lt;p&gt;In this case, our variables have the following values:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Statistic&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sample size, $$ n $$&lt;/td&gt;
&lt;td&gt;91&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sample proportion, $$ {\hat p} $$&lt;/td&gt;
&lt;td&gt;0.78&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.criticalvaluecalculator.com/blog/understanding-zscore-and-zcritical-value-in-statistics-a-comprehensive-guide"&gt;Z critical value&lt;/a&gt;, $$z_{\frac{\alpha}{2}}$$&lt;/td&gt;
&lt;td&gt;1.96&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;So our margin of error is calculated as follows:&lt;/p&gt;
&lt;p&gt;$$ 1.96 \cdot \sqrt{\dfrac{ 0.78(1-0.78)}{ 91 }} = 0.0851 $$&lt;/p&gt;
&lt;p&gt;In this case, variables in the inline shortcode store or calculate all values.
So if I change the value of a variable, the values in the table, formula, and derived margin of error all change, too.&lt;/p&gt;
&lt;div class="expandable-section"&gt;
&lt;button aria-expanded="false" data-expands="js-expandable-830e2b15ad7bba7ad269d448901161e6"&gt;
&lt;span class="expandable-label"&gt;latex inline shortcode&lt;/span&gt;
&lt;svg aria-hidden="true" focusable="false" viewBox="0 0 70.866142 70.866141"&gt;
&lt;g transform="translate(0 -981.5)"&gt;
&lt;rect style="stroke-width:0;fill:currentColor" ry="5" height="60" width="9.8985" y="987.36" x="30.051" class="up-strut" /&gt;
&lt;rect style="stroke-width:0;fill:currentColor" ry="5" height="10" width="60" y="1012.4" x="5"/&gt;
&lt;/g&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div id="js-expandable-830e2b15ad7bba7ad269d448901161e6" hidden&gt;
&lt;pre&gt;&lt;code class="language-markdown"&gt;
The formula for margin of error is as follows:
$$ z_{\frac{\alpha}{2}}\cdot\sqrt{\dfrac{ {\hat p}(1-{\hat p})}{n}} $$
{{ $total := 91.0 }}
{{ $errors := 20.0 }}
{{ $zcrit := 1.96 }}
{{ $correct := (sub $total $errors) }}
{{ $error_rate := (div $errors $total) }}
{{ $correct_rate := (div $correct $total ) }}
{{ $std_err :=
(math.Sqrt
(div (math.Product $error_rate $correct_rate)
($total))
)
}}
{{ $margin_err := math.Product $zcrit $std_err }}
In this case, our variables have the following values:
| Statistic | Value |
|----------------|--------------|
| Sample size, $$ n $$ | {{ $total }} |
| Sample proportion, $$ {\hat p} $$ | {{printf &amp;quot;%.2f&amp;quot; $correct_rate }} |
| [Z critical value](https://www.criticalvaluecalculator.com/blog/understanding-zscore-and-zcritical-value-in-statistics-a-comprehensive-guide), $$z_{\frac{\alpha}{2}}$$ | {{ printf &amp;quot;%.2f&amp;quot; $zcrit }} |
So our calculation is
$$ {{ $zcrit }} \cdot \sqrt{\dfrac{ {{printf &amp;quot;%.2f&amp;quot; $correct_rate}}(1-{{printf &amp;quot;%.2f&amp;quot; $correct_rate}})}{ {{$total}} }} $$
Which gives us a **margin of error of {{printf &amp;quot;%.4f&amp;quot; $margin_err}}**.
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="limitations-and-ideas-for-development"&gt;Limitations and ideas for development&lt;/h2&gt;
&lt;p&gt;This is a fun and flexible way to centralize concerns for work that involves numbers, prose, and Hugo.&lt;/p&gt;
&lt;p&gt;The main limitation I found was that I had to be careful to make sure all source values were floats, not integers.
Otherwise, I got unexpected values. This also means that I used a lot of &lt;a href="https://gohugo.io/functions/fmt/printf/"&gt;&lt;code&gt;printf&lt;/code&gt;&lt;/a&gt; statements to round values to a few decimals.
A more robust method to handle these issues would be to pipe the numbers into the &lt;a href="https://gohugo.io/functions/cast/tofloat/"&gt;&lt;code&gt;cast.ToFloat&lt;/code&gt;&lt;/a&gt; function and write a &lt;a href="https://wellshapedwords.com/posts/hugo-shortcode-to-round-to-decimals/"&gt;shortcode to round to decimals&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you are doing high-precision calculation, this is probably not viable.
Please do not plan works of civil engineering using Hugo inline shortcodes.&lt;/p&gt;
&lt;p&gt;For future work, one might experiment with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using &lt;a href="https://gohugo.io/functions/resources/getremote/"&gt;remote data&lt;/a&gt; to work with source data in a CSV or JSON file&lt;/li&gt;
&lt;li&gt;Using the &lt;a href="https://gohugo.io/methods/shortcode/store/"&gt;Store pad&lt;/a&gt; to persist values across different shortcodes in the page—or, alternatively, putting the entire page inside an inline shortcode.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There&amp;rsquo;s also much room to extend the presentation of the data, perhaps with custom inline JS or CSS.&lt;/p&gt;</description></item><item><title>Is ChatGPT a reliable research assistant? Building a books database</title><link>https://wellshapedwords.com/posts/chatgpt-as-book-research-assistant/</link><pubDate>Tue, 17 Dec 2024 17:58:07 -0300</pubDate><guid>https://wellshapedwords.com/posts/chatgpt-as-book-research-assistant/</guid><description>&lt;p&gt;LLMs are good at processing unstructured data, like internet pages and research papers, and at generating structured output, like code and JSON files. The devil is in the generated details. If they aren&amp;rsquo;t correct, what&amp;rsquo;s the use?&lt;/p&gt;
&lt;p&gt;Testing the correctness of generated data is the goal of my informal experiment, which uses ChatGPT to build a database of information about classic books.
The input is &lt;a href="https://web.archive.org/web/20190921184722/http://ebooks.adelaide.edu.au/l/literature/bloom/complete.html"&gt;long list of books that Harold Bloom considered to comprise the Western Canon&lt;/a&gt;.
The goal output is a table of these books with additional columns for information that wasn&amp;rsquo;t present in the initial list.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s ignore the validity of the list itself and consider its worth as source material to test an LLM&amp;rsquo;s capabilities as a research assistant:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It would be boring to manually add contextual information for every single one of these 1300+ works&lt;/li&gt;
&lt;li&gt;Almost all the books in the list have extensive discussion in the wider body of written literature, meaning the model&amp;rsquo;s training corpus likely includes some basic contextual information about each one.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So I used ChatGPT to build me a books database.&lt;/p&gt;
&lt;h2 id="results-summary"&gt;Results summary&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="#results"&gt;Jump to full results&lt;/a&gt; | &lt;a href="https://gitlab.com/MattDodson/site/-/tree/main/content/posts/chatgpt-as-book-research-assistant/source-files?ref_type=heads"&gt;Source files&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Though validation errors arose,
ChatGPT did reasonably well at following my prompt to create a CSV file using the input values and specified schema.
The categorical values, for the enum &lt;code&gt;Form&lt;/code&gt; field and freeform &lt;code&gt;Genre&lt;/code&gt; field, seem reasonable at a glance, but all I did was glance at them.&lt;/p&gt;
&lt;p&gt;To evaluate the quantity of the reported &lt;code&gt;year&lt;/code&gt;, I tested a sample of 100 randomly selected rows.
The sample accuracy rate was 78%, and I estimate the population accuracy-rate parameter to be between 70% and 87%.&lt;/p&gt;
&lt;p&gt;For this specific case, I&amp;rsquo;d say the LLM worked well enough as a research assistant for my unserious work.
With so many potential errors, however, it is unreliable for work with meaningful stakes.
Nevertheless, I see ways to improve the setup.
And, as an assistant, the bot may be a better fit for helping researchers categorize and filter, rather than delivering high-precision values and facts.&lt;/p&gt;
&lt;h2 id="method"&gt;Method&lt;/h2&gt;
&lt;p&gt;To generate the data,
I used the default version of ChatGPT 4o Mini that existed on November 24th, 2024.
I did not log in.&lt;/p&gt;
&lt;p&gt;To inspect and clean up the data, I used &lt;a href="https://duckdb.org/docs/"&gt;DuckDB&lt;/a&gt; to create a table from the generated CSV files.&lt;/p&gt;
&lt;h3 id="goal"&gt;Goal&lt;/h3&gt;
&lt;p&gt;The main goal was to test how well ChatGPT could date a list of literary works for me.
I also wanted to see how it does in classifying them by Genre and Medium.&lt;/p&gt;
&lt;h3 id="prompt"&gt;Prompt&lt;/h3&gt;
&lt;p&gt;I refined the prompt in iterations.
The added instructions tell the story of all the issues that arose in the prior attempts.&lt;/p&gt;
&lt;p&gt;First prompt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Turn this into a single CSV file in the format
Author, Title, Form (enum: novel, poetry, drama, philosophy/essay), Genre, Year, Country
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These first prompts returned many poor-quality and invalid fields.
After a few iterations, my prompt was something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Turn this into a single CSV file.
Each book needs its own row
Only enclose fields in &amp;quot; if they contain a comma.
The schema is as follows. Let year be an integer.
If year is indeterminate, leave empty.
All other fields must have values.
Author, Title, Form (enum: novel, poetry, drama, philosophy/essay, story collection), Genre, Year, Country
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This worked pretty well, enough that I generated all rows and started checking the accuracy of the &lt;code&gt;Year&lt;/code&gt; field from a sample.
This exposed a fatal flaw in my experimental design:
many of these works, in particular the collections of stories and poetry and the works from antiquity,
are impossible to precisely date.&lt;/p&gt;
&lt;p&gt;So I came up with my &lt;strong&gt;final attempt&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
Turn this into a single CSV file.
Each book needs its own row
Only enclose fields in &amp;quot; if they contain a comma.
The schema is as follows:
Author, Title, Form, Genre, Year, Country
Form is an enum with values ['Drama','Novel','Story collection','Philosophy/essay','Poetry','Memoire/Autobiography']
Let year be an integer.
If form is drama, use year it was first performed.
Otherwise, use year first published.
If multiple volumes, use year the first volume was published.
if you cannot determine year, leave blank.
I expect many collections and ancient works to have empty year fields.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At the end of the each prompt, I appended an unedited snippet of Bloom&amp;rsquo;s list.
In the first attempts, I pasted too many items and ChatGPT occasionally hallucinated additional rows.
In the final attempt, I prompted about 100 rows at a time.&lt;/p&gt;
&lt;p&gt;I made four CSV files, one for each &amp;ldquo;Age&amp;rdquo; as classified by Bloom.&lt;/p&gt;
&lt;h3 id="exploration-and-clean-up"&gt;Exploration and clean up&lt;/h3&gt;
&lt;p&gt;As I went through attempts, I used DuckDB to explore the data.
Sometimes obvious structural issues surfaced: all titles for a particular author were in one semi-colon-delimited row; a cluster of rows had large swaths of empty fields.
When stuff like this happened, I rewrote my prompt and tried again.&lt;/p&gt;
&lt;p&gt;For the final table, I added a column to record the literary &amp;ldquo;age&amp;rdquo; before joining the CSV files in one table.
I updated the final fields to fix minor validation issues and remove whitespace.
The most common nuisances were CSV validation errors for columns with commas in their values, such as &lt;em&gt;The Mandrake, A Comedy&lt;/em&gt;.
I also converted all BC years to negative integers.&lt;/p&gt;
&lt;h2 id="results"&gt;Results&lt;/h2&gt;
&lt;aside aria-label="note" class="note"&gt;
&lt;div&gt;
&lt;svg class="sign" aria-hidden="true" viewBox="0 0 41.667306 41.66729" focusable="false"&gt;
&lt;use xlink:href="#info"&gt;&lt;/use&gt;
&lt;/svg&gt;
&lt;a href="https://gitlab.com/MattDodson/site/-/tree/main/content/posts/chatgpt-as-book-research-assistant/source-files?ref_type=heads"&gt;Source CSVs and database&lt;/a&gt;
&lt;/div&gt;
&lt;/aside&gt;
&lt;p&gt;I assess how well ChatGPT did by a few criteria:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Did it structure the information as asked?&lt;/li&gt;
&lt;li&gt;Did it add information that was accurate?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the &amp;ldquo;added information&amp;rdquo; part, ChatGPT added three new data points for each book&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Year&lt;/li&gt;
&lt;li&gt;Form&lt;/li&gt;
&lt;li&gt;Genre&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the following sections, I&amp;rsquo;ll discuss how well ChatGPT did at each task.&lt;/p&gt;
&lt;h3 id="semi-structured-prose-to-csv"&gt;Semi-structured prose to CSV&lt;/h3&gt;
&lt;p&gt;ChatGPT ingested a large semi-structured prose list and turned it into a CSV file.
This definitely saved some labor.
However, as the list already had a regular structure, this task also might have been done with some old-fashioned regex.&lt;/p&gt;
&lt;p&gt;The structure of the data mostly conformed to my expectations, though it took some trial and error to validate the CSV output.&lt;/p&gt;
&lt;p&gt;A future experiment might try to extract data from completely unstructured prose into a structured table of facts.&lt;/p&gt;
&lt;h3 id="quality-of-output-for-form-field"&gt;Quality of output for &lt;code&gt;form&lt;/code&gt; field&lt;/h3&gt;
&lt;p&gt;Most of the time, ChatGPT reported the &lt;code&gt;Form&lt;/code&gt; according to one of the values specified in the prompt.&lt;/p&gt;
&lt;p&gt;However, it also repeatedly added new values to some works, which was against instructions and created invalid table inserts.
In some cases, I might have preferred ChatGPT&amp;rsquo;s suggestion—I especially wish I included the value &lt;code&gt;history&lt;/code&gt;—but this editorialization went outside of the prompt.&lt;/p&gt;
&lt;h3 id="from-1300-books-228-genres"&gt;From 1300 books, 228 genres&lt;/h3&gt;
&lt;p&gt;Unlike the &lt;code&gt;Form&lt;/code&gt; field, which was explicitly an enum, the &lt;code&gt;Genre&lt;/code&gt; field could take any value.&lt;/p&gt;
&lt;p&gt;ChatGPT created 228 genres.
Only 118 genres were used for more than one title, and only 26 genres were used for more than 10 titles.
This distribution makes some thin categories, but I do like the specificity of some of the singleton genres I saw, such as &amp;ldquo;Andalusian folk poetry.&amp;rdquo;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select genre, count(genre)
from books
GROUP BY all
order by count(genre) desc;
&lt;/code&gt;&lt;/pre&gt;
&lt;div class="expandable-section"&gt;
&lt;button aria-expanded="false" data-expands="js-expandable-afe7ef1bdca060e1f38a83ca9a380412"&gt;
&lt;span class="expandable-label"&gt;All 228 genres&lt;/span&gt;
&lt;svg aria-hidden="true" focusable="false" viewBox="0 0 70.866142 70.866141"&gt;
&lt;g transform="translate(0 -981.5)"&gt;
&lt;rect style="stroke-width:0;fill:currentColor" ry="5" height="60" width="9.8985" y="987.36" x="30.051" class="up-strut" /&gt;
&lt;rect style="stroke-width:0;fill:currentColor" ry="5" height="10" width="60" y="1012.4" x="5"/&gt;
&lt;/g&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div id="js-expandable-afe7ef1bdca060e1f38a83ca9a380412" hidden&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Genre&lt;/th&gt;
&lt;th style="text-align: right"&gt;count(genre)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Modernist fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;213&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modernist poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;191&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tragedy&lt;/td&gt;
&lt;td style="text-align: right"&gt;78&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comedy&lt;/td&gt;
&lt;td style="text-align: right"&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Realist novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contemporary poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Historical fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;41&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lyric poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Romantic poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Literary criticism&lt;/td&gt;
&lt;td style="text-align: right"&gt;31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postmodern fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Short stories&lt;/td&gt;
&lt;td style="text-align: right"&gt;24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modernist drama&lt;/td&gt;
&lt;td style="text-align: right"&gt;24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Epic&lt;/td&gt;
&lt;td style="text-align: right"&gt;24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Realist fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Philosophy&lt;/td&gt;
&lt;td style="text-align: right"&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Romantic novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Satire&lt;/td&gt;
&lt;td style="text-align: right"&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autobiography&lt;/td&gt;
&lt;td style="text-align: right"&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Symbolist poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drama&lt;/td&gt;
&lt;td style="text-align: right"&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Historical novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fantasy&lt;/td&gt;
&lt;td style="text-align: right"&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modernist novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Social novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Magical realism&lt;/td&gt;
&lt;td style="text-align: right"&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adventure novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Victorian poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Victorian novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Surrealist poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gothic novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Essays&lt;/td&gt;
&lt;td style="text-align: right"&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Political fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Science fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fantasy fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Romance&lt;/td&gt;
&lt;td style="text-align: right"&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Epic poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Satirical fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Philosophical novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contemporary drama&lt;/td&gt;
&lt;td style="text-align: right"&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Absurdist play&lt;/td&gt;
&lt;td style="text-align: right"&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Satirical novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Philosophical fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Historical drama&lt;/td&gt;
&lt;td style="text-align: right"&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Epistolary novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Political philosophy&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pre-Raphaelite poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Folk tales&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adventure fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yiddish poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Religious text&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Philosophical essay&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elegy&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Historical writing&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Absurdist theatre&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Biography&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Psychological fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yiddish literature&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Picaresque&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Baroque poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jewish fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;American poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Religious poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comic novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Experimental fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Art criticism&lt;/td&gt;
&lt;td style="text-align: right"&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autobiographical novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autobiographical fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mystical poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;War poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memoir&lt;/td&gt;
&lt;td style="text-align: right"&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ethics&lt;/td&gt;
&lt;td style="text-align: right"&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Neoclassical poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existentialist fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Romantic&lt;/td&gt;
&lt;td style="text-align: right"&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;History&lt;/td&gt;
&lt;td style="text-align: right"&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Religious philosophy&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bildungsroman&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Art history&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Devotional poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aesthetics&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Education&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Satirical poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Philosophical text&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Symbolism&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Surrealist fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Futurist poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;True crime&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pastoral poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Philosophical drama&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feminist literature&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crime fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feminist fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Socialist realism&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentimental novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Psychological novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expressionist poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Symbolist fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Novella&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Philosophical treatise&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aestheticism&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Naturalist fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diary&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modernist theory&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Philosophical essays&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Naturalist drama&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Industrial novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dystopian fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poetics&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Theology&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mythology&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lyrical poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autobiographical&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Political drama&lt;/td&gt;
&lt;td style="text-align: right"&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Science and art notes&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Social philosophy&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arthurian legend&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Epigram&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ballad opera&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ballads&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prose poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comic verse&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decadent poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Satirical tales&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Travel literature&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Historical study&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existentialist poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Philosophical prose&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dadaism&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Humor&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Political thriller&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allegory&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Religious&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Philosophical tale&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Political commentary&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Travel&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Philosophical poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Christmas fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tragicomedy&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gothic tales&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Political novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;War novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Experimental poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feminist philosophy&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Religious fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Political poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gothic fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Western&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fantasy drama&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Translation&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detective novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modernist essays&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metaphysical poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nature Writing&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Masque&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Utopian literature&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dialogue&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dramatic monologue&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aesthetic novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operetta&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mystery novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Surrealist drama&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Utopian philosophy&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Travel poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet sequence&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spiritual&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autobiographical essay&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mystery&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nonsense verse&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Letters&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Travel writing&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Holocaust poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adventure&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Periodical essays&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maxims&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Epigrams&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Philosophical fragments&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scottish poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Children&amp;rsquo;s literature&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Religious novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coming-of-age novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Neorealism&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Philosophical dialogue&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Surrealist play&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existentialist novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropological theory&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Futurist drama/poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Romantic fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-colonial fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modernist philosophy&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Surreal fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poetry/Drama&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memoire/Autobiography&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Political theory&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tragedy/Comedy&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Political&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fables&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aesthetic theory&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Folklore&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Utopian novel&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Romantic opera&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Historical account&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Surrealism&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Art theory&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Theater theory&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Literary fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Psychological thriller&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Andalusian folk poetry&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Philosophical memoir&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modernist prose&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yiddish fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jewish memoir&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fable&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Social criticism&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fairy tales&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modernism&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Literary memoir&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existential fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Holocaust fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-apartheid fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Negritude&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existentialist play&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;War fiction&lt;/td&gt;
&lt;td style="text-align: right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Evaluating the quality of the generated genres is a topic for another post.
I didn&amp;rsquo;t see anything obviously absurd or wrong but I didn&amp;rsquo;t look hard.&lt;/p&gt;
&lt;h3 id="accuracy-of-reported-year"&gt;Accuracy of reported &lt;code&gt;Year&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;When I started this experiment, the &lt;code&gt;Year&lt;/code&gt; field seemed the most objective and easiest to fact check.
So &lt;code&gt;Year&lt;/code&gt; is the only field I validated for this first experiment.&lt;/p&gt;
&lt;p&gt;As checking all 1300 fields for errors would defeat the purpose of having a research assistant,
I used DuckDB to give me a 100-field sample:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE sample
as SELECT Author, Title, &amp;quot;Year&amp;quot;
FROM BOOKS
USING SAMPLE 100;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My fact-checking technique was irresponsibly informal: first I looked for the work on English Wikipedia.
If the work and its date didn&amp;rsquo;t appear in a page or in the &amp;ldquo;Works&amp;rdquo; section of the author, I checked Goodreads, Google Books, and the Internet Archive.&lt;/p&gt;
&lt;p&gt;Of the 100-work sample, 91 had dates that I could verify.
If you&amp;rsquo;re interested in the 9 I couldn&amp;rsquo;t verify, you can read the comments:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;select GPT_correct, count(GPT_correct) from &amp;quot;sample&amp;quot;
group by all;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;─────────────┬────────────────────┐
│ GPT_correct │ count(GPT_correct) │
│ boolean │ int64 │
├─────────────┼────────────────────┤
│ false │ 20 │
│ true │ 71 │
└─
&lt;/code&gt;&lt;/pre&gt;
&lt;div class="expandable-section"&gt;
&lt;button aria-expanded="false" data-expands="js-expandable-f506f473c0c4753c2b944e127ebc9074"&gt;
&lt;span class="expandable-label"&gt;unverifiable rows&lt;/span&gt;
&lt;svg aria-hidden="true" focusable="false" viewBox="0 0 70.866142 70.866141"&gt;
&lt;g transform="translate(0 -981.5)"&gt;
&lt;rect style="stroke-width:0;fill:currentColor" ry="5" height="60" width="9.8985" y="987.36" x="30.051" class="up-strut" /&gt;
&lt;rect style="stroke-width:0;fill:currentColor" ry="5" height="10" width="60" y="1012.4" x="5"/&gt;
&lt;/g&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div id="js-expandable-f506f473c0c4753c2b944e127ebc9074" hidden&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;D select * from &amp;quot;sample&amp;quot;
where GPT_correct IS NULL;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-markdown"&gt;| Author | Title | Year | GPT_correct | correction_comments |
|-------------------------------|---------------------------|-----:|-------------|-------------------------------------|
| Yeats, W. B. (William Butler) | Mythologies | 1938 | | not sure how to date |
| Angelos Sikelianos | Selected Poems | 1960 | | not sure about this |
| Fernando Pessoa | Always | | | not sure about title |
| Kevin Hart | Peniel and Other Poems | 1986 | | not sure about title |
| Robert Penn Warren | Selected Poems | 1997 | | Likely indeterminate, but ambiguous |
| Ronald Firbank | Five Novels | 1924 | | ambiguous |
| Németh Laszlo | Guilt | 1942 | | not sure about title |
| Francis Ponge | Things: Selected Writings | | | ambiguous |
| Lucretius | The Way Things Are | -55 | | not sure how to date |
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;This left a sample of 91 works.
Of these, ChatGPT incorrectly reported the year of 20 rows.
&amp;ldquo;Incorrect&amp;rdquo; in this case means either factually wrong (e.g. reported 2003 when the first publication was 2001) or against the prompt&amp;rsquo;s instructions to leave ambiguous fields blank.&lt;/p&gt;
&lt;p&gt;Here are the results:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT * FROM sample
WHERE GPT_correct IS False;
&lt;/code&gt;&lt;/pre&gt;
&lt;div class="expandable-section"&gt;
&lt;button aria-expanded="false" data-expands="js-expandable-86a28fb65e439c2f79a8d40b7c5fbd00"&gt;
&lt;span class="expandable-label"&gt;false years&lt;/span&gt;
&lt;svg aria-hidden="true" focusable="false" viewBox="0 0 70.866142 70.866141"&gt;
&lt;g transform="translate(0 -981.5)"&gt;
&lt;rect style="stroke-width:0;fill:currentColor" ry="5" height="60" width="9.8985" y="987.36" x="30.051" class="up-strut" /&gt;
&lt;rect style="stroke-width:0;fill:currentColor" ry="5" height="10" width="60" y="1012.4" x="5"/&gt;
&lt;/g&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div id="js-expandable-86a28fb65e439c2f79a8d40b7c5fbd00" hidden&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Author&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th style="text-align: right"&gt;Year&lt;/th&gt;
&lt;th style="text-align: right"&gt;GPT_correct&lt;/th&gt;
&lt;th&gt;correction_comments&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Thomas Hardy&lt;/td&gt;
&lt;td&gt;Collected Poems&lt;/td&gt;
&lt;td style="text-align: right"&gt;1928&lt;/td&gt;
&lt;td style="text-align: right"&gt;false&lt;/td&gt;
&lt;td&gt;1919&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Philippe de Commynes&lt;/td&gt;
&lt;td&gt;Memoirs&lt;/td&gt;
&lt;td style="text-align: right"&gt;1489&lt;/td&gt;
&lt;td style="text-align: right"&gt;false&lt;/td&gt;
&lt;td&gt;1498 or indeterminate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Miroslav Holub&lt;/td&gt;
&lt;td&gt;The Fly&lt;/td&gt;
&lt;td style="text-align: right"&gt;1972&lt;/td&gt;
&lt;td style="text-align: right"&gt;false&lt;/td&gt;
&lt;td&gt;indeterminate or 1987 (year of translation)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Léonie Adams&lt;/td&gt;
&lt;td&gt;Poems: A Selection&lt;/td&gt;
&lt;td style="text-align: right"&gt;1953&lt;/td&gt;
&lt;td style="text-align: right"&gt;false&lt;/td&gt;
&lt;td&gt;1954&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Günter Eich&lt;/td&gt;
&lt;td&gt;Moles&lt;/td&gt;
&lt;td style="text-align: right"&gt;1964&lt;/td&gt;
&lt;td style="text-align: right"&gt;false&lt;/td&gt;
&lt;td&gt;indeterminate: english edition dates from 66 to 72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alfred Cord&lt;/td&gt;
&lt;td&gt;A Call in the Midst of the Crowd&lt;/td&gt;
&lt;td style="text-align: right"&gt;2005&lt;/td&gt;
&lt;td style="text-align: right"&gt;false&lt;/td&gt;
&lt;td&gt;1978&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Georges Bataille&lt;/td&gt;
&lt;td&gt;Blue of Noon&lt;/td&gt;
&lt;td style="text-align: right"&gt;1955&lt;/td&gt;
&lt;td style="text-align: right"&gt;false&lt;/td&gt;
&lt;td&gt;1957&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sholem Asch&lt;/td&gt;
&lt;td&gt;East River&lt;/td&gt;
&lt;td style="text-align: right"&gt;1922&lt;/td&gt;
&lt;td style="text-align: right"&gt;false&lt;/td&gt;
&lt;td&gt;1946&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heinrich von Kleist&lt;/td&gt;
&lt;td&gt;Stories&lt;/td&gt;
&lt;td style="text-align: right"&gt;1810&lt;/td&gt;
&lt;td style="text-align: right"&gt;false&lt;/td&gt;
&lt;td&gt;indeterminate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paul Celan&lt;/td&gt;
&lt;td&gt;Poems&lt;/td&gt;
&lt;td style="text-align: right"&gt;1991&lt;/td&gt;
&lt;td style="text-align: right"&gt;false&lt;/td&gt;
&lt;td&gt;indeterminate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vachel Lindsay&lt;/td&gt;
&lt;td&gt;Collected Poems&lt;/td&gt;
&lt;td style="text-align: right"&gt;1931&lt;/td&gt;
&lt;td style="text-align: right"&gt;false&lt;/td&gt;
&lt;td&gt;indeterminate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;François Villon&lt;/td&gt;
&lt;td&gt;Poems&lt;/td&gt;
&lt;td style="text-align: right"&gt;1400&lt;/td&gt;
&lt;td style="text-align: right"&gt;false&lt;/td&gt;
&lt;td&gt;indeterminate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Denis Johnson&lt;/td&gt;
&lt;td&gt;Fiskadoro&lt;/td&gt;
&lt;td style="text-align: right"&gt;1999&lt;/td&gt;
&lt;td style="text-align: right"&gt;false&lt;/td&gt;
&lt;td&gt;1985&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthony Burgess&lt;/td&gt;
&lt;td&gt;Nothing Like the Sun&lt;/td&gt;
&lt;td style="text-align: right"&gt;1961&lt;/td&gt;
&lt;td style="text-align: right"&gt;false&lt;/td&gt;
&lt;td&gt;1964&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jacob and Wilhelm Grimm&lt;/td&gt;
&lt;td&gt;Fairy Tales&lt;/td&gt;
&lt;td style="text-align: right"&gt;&lt;/td&gt;
&lt;td style="text-align: right"&gt;false&lt;/td&gt;
&lt;td&gt;First published 1812&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Witold Gombrowicz&lt;/td&gt;
&lt;td&gt;Three Novels&lt;/td&gt;
&lt;td style="text-align: right"&gt;1967&lt;/td&gt;
&lt;td style="text-align: right"&gt;false&lt;/td&gt;
&lt;td&gt;indeterminate or 1978 (year of translation and compiliation)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hugo von Hofmannsthal&lt;/td&gt;
&lt;td&gt;Poems and Verse Plays&lt;/td&gt;
&lt;td style="text-align: right"&gt;1990&lt;/td&gt;
&lt;td style="text-align: right"&gt;false&lt;/td&gt;
&lt;td&gt;indeterminate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seneca, Lucius Annaeus&lt;/td&gt;
&lt;td&gt;Tragedies, particularly Medea and Hercules Furens&lt;/td&gt;
&lt;td style="text-align: right"&gt;-65&lt;/td&gt;
&lt;td style="text-align: right"&gt;false&lt;/td&gt;
&lt;td&gt;indeterminate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ernest Hemingway&lt;/td&gt;
&lt;td&gt;Complete Short Stories&lt;/td&gt;
&lt;td style="text-align: right"&gt;1938&lt;/td&gt;
&lt;td style="text-align: right"&gt;false&lt;/td&gt;
&lt;td&gt;indeterminate or 1987&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Robert Garnier&lt;/td&gt;
&lt;td&gt;Mark Antony&lt;/td&gt;
&lt;td style="text-align: right"&gt;1582&lt;/td&gt;
&lt;td style="text-align: right"&gt;false&lt;/td&gt;
&lt;td&gt;1578&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="estimate"&gt;Estimating accuracy rate&lt;/h3&gt;
&lt;p&gt;The sample year rate has the following statistics:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Statistic&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total fields&lt;/td&gt;
&lt;td&gt;91&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Errors&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sample accuracy rate&lt;/td&gt;
&lt;td&gt;0.78%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard error&lt;/td&gt;
&lt;td&gt;0.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sample distribution interval&lt;/td&gt;
&lt;td&gt;[0.70,0.87]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In other words, using this method to &lt;a href="https://stats.libretexts.org/Courses/Rio_Hondo_College/Math_130%3A_Statistics/07%3A_Confidence_Intervals/7.02%3A_Confidence_Interval_for_a_Proportion"&gt;build a confidence interval for a proportion&lt;/a&gt;, we can say with 95% confidence that the range
&lt;code&gt;[0.70,0.87]&lt;/code&gt; covers the true accuracy rate.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://gitlab.com/MattDodson/site/-/blob/main/content/posts/chatgpt-as-book-research-assistant/index.md?ref_type=heads#estimating-accuracy-rate-estimate"&gt;Check my work&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="discussion"&gt;Discussion&lt;/h2&gt;
&lt;p&gt;Even with all the time spent rewriting prompts and fixing validation issues,
ChatGPT built the books CSV database in far less time than it would have taken manually.&lt;/p&gt;
&lt;p&gt;The results for the added fields are mixed.
From my cursory checks, the &lt;code&gt;Genre&lt;/code&gt; and &lt;code&gt;Form&lt;/code&gt; fields are at least reasonable.
The error rate of the &lt;code&gt;Year&lt;/code&gt; field, though, is too high to be reliable.&lt;/p&gt;
&lt;p&gt;In this case, I judge my ChatGPT-built database to good enough for unserious work.
If you were using this database just to discover some books that a specific critic recommended and wanted to filter by some crude categorizations and fuzzy dates, the data quality is acceptable.
If, however, you needed to do precise research, all the generated values are too suspicious to be acceptable.&lt;/p&gt;
&lt;h3 id="ideas-for-extended-research"&gt;Ideas for extended research&lt;/h3&gt;
&lt;p&gt;I believe there is potential in using LLMs to create bespoke databases;
I&amp;rsquo;m not sure how easy it is to generate a very good one.
Part of the quality issues in my DB arose from my haphazard design, and but another part stems from the inherent instability of ChatGPT&amp;rsquo;s answers.
For this reason, the build-a-research-database use case might work better for exploration and rough filtering—say filtering by &lt;code&gt;Genre&lt;/code&gt;— than for retrieving precise values.&lt;/p&gt;
&lt;p&gt;If anything, this generated table can serve as a baseline to try to improve.
Here are some ideas to improve this experiment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Have a clear hypothesis and criteria for how to test quality&lt;/li&gt;
&lt;li&gt;Think carefully about the specific values to generate and whether they really are objectively verifiable&lt;/li&gt;
&lt;li&gt;Test this quality on a small sample first to discover issues with the prompt&lt;/li&gt;
&lt;li&gt;If creating a large database, build some automation tools to check for basic issues and reprompt (things like adding all works for an author on one row happened multiple times)&lt;/li&gt;
&lt;li&gt;Refine the interaction with the LLM: better prompting, better use and understanding of context windows, changing temperature, and so on.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also have some ideas to extend this use case:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Normalize the facts in different tables. For the input, create a table with a primary key. Then make that a foreign key in the tables where information needs to be generated. Pass the LLM the schema and have it build the database.&lt;/li&gt;
&lt;li&gt;Require a source for each reported fact&lt;/li&gt;
&lt;li&gt;If the database is public, offer a way for users to fact check and edit values, similar to Wikipedia.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="the-journey-was-more-fun-than-the-result"&gt;The journey was more fun than the result&lt;/h3&gt;
&lt;p&gt;My books DB may be a dud, but the process of making it was fun:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I got to use DuckDB for the first time..&lt;/li&gt;
&lt;li&gt;I researched a ton of books I never heard of:&lt;/li&gt;
&lt;li&gt;I now have a database for future learning projects, e.g. to build a simple web app or practice normalizing a data model.&lt;/li&gt;
&lt;li&gt;I discovered tons of issues in how I thought about creating an LLM test for a certain use case&lt;/li&gt;
&lt;li&gt;I gained a new appreciation for how difficult it is to manage information precisely. All of my table&amp;rsquo;s values are subject for interpretation.&lt;/li&gt;
&lt;/ul&gt;
&lt;!---
This is the story of all non-experimental research:
we set out in the belief that an interesting story or discovery already exists, we only need to assimilate the wide body of sources.
Being able to describe, a
Any program that can reliably retrieve information from disparate, unstructured sources is useful to researches. --&gt;
&lt;!--
The entire discipline of knowledge organization exists to help humans find information and further research.
Its practices are many, but all converge around the goals of classification, indexing, and schematizing information.
--&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;it also added &amp;ldquo;Country&amp;rdquo; for each author. This information was usually in the prompt, but sometimes this was regional or politically ambigious. In these cases ChatGPT picked a country. I saw no errors there.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>Auto-generate glossaries With Hugo</title><link>https://wellshapedwords.com/posts/hugo-manage-terminologies/</link><pubDate>Wed, 20 Mar 2024 21:53:58 -0500</pubDate><guid>https://wellshapedwords.com/posts/hugo-manage-terminologies/</guid><description>&lt;aside aria-label="note" class="note"&gt;
&lt;div&gt;
&lt;svg class="sign" aria-hidden="true" viewBox="0 0 41.667306 41.66729" focusable="false"&gt;
&lt;use xlink:href="#info"&gt;&lt;/use&gt;
&lt;/svg&gt;
I was inspired by Ian Cowley&amp;rsquo;s talk at WriteTheDocs, &lt;a href="https://www.youtube.com/watch?v=3YPisLxVy2I"&gt;One glossary to rule them all&lt;/a&gt;.
For the filename, I&amp;rsquo;ve adopted the term he used, &lt;em&gt;termbase&lt;/em&gt;.
&lt;/div&gt;
&lt;/aside&gt;
&lt;p&gt;Hugo can use &lt;a href="https://gohugo.io/templates/data-templates/"&gt;data files&lt;/a&gt;, like JSON and YAML, to make templates.
This feature is often useful for technical writing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The data file can serve as a source of truth, and writers can use its properties wherever they please.&lt;/li&gt;
&lt;li&gt;For long repetitive structures, the format needs to be written only once in the shortcode, rather than once for every item.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This topic shows some shortcodes that use a YAML file as a single source of &lt;em&gt;terminology&lt;/em&gt; truth in a Hugo site.
These templates save some formatting tedium and ensure that terminology definitions stay consistent across different pages and presentations.&lt;/p&gt;
&lt;h2 id="steps-to-generate-a-glossary"&gt;Steps to generate a glossary&lt;/h2&gt;
&lt;aside aria-label="note" class="note"&gt;
&lt;div&gt;
&lt;svg
class="sign"
aria-hidden="true"
viewBox="0 0 41.667306 41.66729"
focusable="false"
&gt;
&lt;use xlink:href="#info"&gt;&lt;/use&gt;
&lt;/svg&gt;
This page uses the logic it demonstrates.
It was last built with Hugo v0.152.2.
&lt;/div&gt;
&lt;/aside&gt;
&lt;p&gt;The procedure is as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Write the glossary to something like &lt;code&gt;data/termbase.yaml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Write a template that reads the YAML file and makes a formatted list.&lt;/li&gt;
&lt;li&gt;Extend its usefulness by adding however many properties you want to the data file.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="write-the-yaml-file"&gt;Write the YAML file&lt;/h3&gt;
&lt;p&gt;First create the file to be your source of truth:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In the &lt;code&gt;data&lt;/code&gt; directory, create a YAML file called something like &lt;code&gt;data/termbase.yaml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Create an array of term objects. For a glossary, you need at least &lt;code&gt;term&lt;/code&gt; and &lt;code&gt;definition&lt;/code&gt; properties (choose whatever key names you want).&lt;/li&gt;
&lt;li&gt;Optionally, add other properties to extend the templating possibilities (subsequent sections give examples).&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="expandable-section"&gt;
&lt;button aria-expanded="false" data-expands="js-expandable-7e879df84e70120c55f918b3cfab2cd6"&gt;
&lt;span class="expandable-label"&gt;a YAML list&lt;/span&gt;
&lt;svg aria-hidden="true" focusable="false" viewBox="0 0 70.866142 70.866141"&gt;
&lt;g transform="translate(0 -981.5)"&gt;
&lt;rect style="stroke-width:0;fill:currentColor" ry="5" height="60" width="9.8985" y="987.36" x="30.051" class="up-strut" /&gt;
&lt;rect style="stroke-width:0;fill:currentColor" ry="5" height="10" width="60" y="1012.4" x="5"/&gt;
&lt;/g&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div id="js-expandable-7e879df84e70120c55f918b3cfab2cd6" hidden&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;- term: Abies
definition: A genus of coniferous trees, properly called fir.
category: nature
- term: Write the Docs
definition: A great place to discuss docs
abbr: WtD
category: &amp;quot;docs&amp;quot;
- term: Mean time to repair
definition: The average time it takes to repair a failed component in a system
abbr: MTTR
category: &amp;quot;&amp;quot;
- term: Montiform
definition: Resembling a mountain in form
category: &amp;quot;nature&amp;quot;
- term: Mozilla Developer Network
definition: A great place to read docs
abbr: MDN
category: &amp;quot;docs&amp;quot;
- term: Oryctology
definition: An old name for mineralogy and geology.
category: &amp;quot;nature&amp;quot;
- term: Request for comment
definition: A publication from the standards-setting bodies of the internet
abbr: RFC
category: &amp;quot;protocols&amp;quot;
- term: Request for proposal
definition: A call for proposals
abbr: RFP
- term: Transmission Control Protocol
definition: A bidirectional protocol to deliver a reliable, ordered, and error-checked stream of octets between applications.
abbr: TCP
category: &amp;quot;protocols&amp;quot;
- term: Topaz
definition: A mineral occuring in rhombic prisms, generally yellowish and pellucid
category: &amp;quot;nature&amp;quot;
- term: User Datagram Protocol
definition: A connectionless communication protocol.
abbr: UDP
category: &amp;quot;protocols&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Alphabetizing the list is probably a good idea, but the &lt;a href="https://gohugo.io/functions/collections/sort/"&gt;&lt;code&gt;sort&lt;/code&gt;&lt;/a&gt; function can do that for you.&lt;/p&gt;
&lt;h3 id="write-the-glossary-template"&gt;Write the glossary template&lt;/h3&gt;
&lt;p&gt;Next, write a template to generate a glossary page.
This is a good time to use an HTML &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dl"&gt;description list&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;{{- $arg := (.Get 0) -}}
{{- range sort .Site.Data.termbase &amp;#34;term&amp;#34; -}}
&amp;lt;dl&amp;gt;
&amp;lt;dt&amp;gt;{{- .term }}{{ with .abbr }} ({{ . -}}){{ end -}}
&amp;lt;/dt&amp;gt;
&amp;lt;dd&amp;gt;{{- .definition -}}&amp;lt;/dd&amp;gt;
&amp;lt;/dl&amp;gt;
{{- end -}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this particular template, if the term has an &lt;code&gt;abbr&lt;/code&gt; property, it prints an abbreviation in parenthesis next to the definition.
But it works fine without abbreviations, too. Feel free to copy and paste.&lt;/p&gt;
&lt;h3 id="use-the-shortcode-in-a-page"&gt;Use the shortcode in a page&lt;/h3&gt;
&lt;p&gt;This shortcode renders the preceding YAML file as follows:&lt;/p&gt;
&lt;div class="expandable-section"&gt;
&lt;button aria-expanded="false" data-expands="js-expandable-e4db25d6c5db7ac4028f47f76259d3ae"&gt;
&lt;span class="expandable-label"&gt;HTML list, rendered from YAML&lt;/span&gt;
&lt;svg aria-hidden="true" focusable="false" viewBox="0 0 70.866142 70.866141"&gt;
&lt;g transform="translate(0 -981.5)"&gt;
&lt;rect style="stroke-width:0;fill:currentColor" ry="5" height="60" width="9.8985" y="987.36" x="30.051" class="up-strut" /&gt;
&lt;rect style="stroke-width:0;fill:currentColor" ry="5" height="10" width="60" y="1012.4" x="5"/&gt;
&lt;/g&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div id="js-expandable-e4db25d6c5db7ac4028f47f76259d3ae" hidden&gt;
&lt;dl&gt;
&lt;dt&gt;Abies&lt;/dt&gt;
&lt;dd&gt;A genus of coniferous trees, properly called fir.&lt;/dd&gt;
&lt;/dl&gt;&lt;dl&gt;
&lt;dt&gt;Mean time to repair (MTTR)&lt;/dt&gt;
&lt;dd&gt;The average time it takes to repair a failed component in a system&lt;/dd&gt;
&lt;/dl&gt;&lt;dl&gt;
&lt;dt&gt;Montiform&lt;/dt&gt;
&lt;dd&gt;Resembling a mountain in form&lt;/dd&gt;
&lt;/dl&gt;&lt;dl&gt;
&lt;dt&gt;Mozilla Developer Network (MDN)&lt;/dt&gt;
&lt;dd&gt;A great place to read docs&lt;/dd&gt;
&lt;/dl&gt;&lt;dl&gt;
&lt;dt&gt;Oryctology&lt;/dt&gt;
&lt;dd&gt;An old name for mineralogy and geology.&lt;/dd&gt;
&lt;/dl&gt;&lt;dl&gt;
&lt;dt&gt;Request for comment (RFC)&lt;/dt&gt;
&lt;dd&gt;A publication from the standards-setting bodies of the internet&lt;/dd&gt;
&lt;/dl&gt;&lt;dl&gt;
&lt;dt&gt;Request for proposal (RFP)&lt;/dt&gt;
&lt;dd&gt;A call for proposals&lt;/dd&gt;
&lt;/dl&gt;&lt;dl&gt;
&lt;dt&gt;Topaz&lt;/dt&gt;
&lt;dd&gt;A mineral occuring in rhombic prisms, generally yellowish and pellucid&lt;/dd&gt;
&lt;/dl&gt;&lt;dl&gt;
&lt;dt&gt;Transmission Control Protocol (TCP)&lt;/dt&gt;
&lt;dd&gt;A bidirectional protocol to deliver a reliable, ordered, and error-checked stream of octets between applications.&lt;/dd&gt;
&lt;/dl&gt;&lt;dl&gt;
&lt;dt&gt;User Datagram Protocol (UDP)&lt;/dt&gt;
&lt;dd&gt;A connectionless communication protocol.&lt;/dd&gt;
&lt;/dl&gt;&lt;dl&gt;
&lt;dt&gt;Write the Docs (WtD)&lt;/dt&gt;
&lt;dd&gt;A great place to discuss docs&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="more-uses-of-the-termbase"&gt;More uses of the termbase&lt;/h2&gt;
&lt;p&gt;With a single source of terminology truth, you can add more shortcodes to manage terminology across an entire site.
Here are some ways.&lt;/p&gt;
&lt;h3 id="reuse-the-definitions-in-tooltips"&gt;Reuse the definitions in tooltips&lt;/h3&gt;
&lt;p&gt;Each entry in the preceding YAML file has properties for &lt;code&gt;term&lt;/code&gt; and &lt;code&gt;definition&lt;/code&gt;—that&amp;rsquo;s all the data you need to create a tooltip that expands a definition on hover.
I started using this quite often in my work on &lt;a href="https://docs.rhize.com"&gt;docs.rhize.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This shortcode takes one argument: either the &lt;code&gt;term&lt;/code&gt; or, if you have one, the &lt;code&gt;abbreviation&lt;/code&gt;.
It creates an HTML &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/abbr"&gt;&lt;code&gt;abbr&lt;/code&gt;&lt;/a&gt;( or &amp;ldquo;tooltip&amp;rdquo;) on the page.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;!--- variables based on shortcode argument --&amp;gt;
{{- $entry := (.Get 0) -}}
&amp;lt;!-- range over data file --&amp;gt;
{{- range $.Site.Data.termbase -}}
&amp;lt;!-- Find matches --&amp;gt;
{{- if eq ( .abbr | lower) ( $entry | lower ) -}}
&amp;lt;abbr title=&amp;quot;{{- .term -}}. {{ .definition | plainify -}}&amp;quot;&amp;gt; {{- .abbr -}}&amp;lt;/abbr&amp;gt;
{{- else if eq ( .term | lower) ( $entry | lower ) -}}
&amp;lt;abbr title=&amp;quot;{{ .definition | plainify -}}&amp;quot;&amp;gt; {{- $entry -}}&amp;lt;/abbr&amp;gt;
{{- end -}}
{{- end -}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When called, it prints the term along with a tooltip that expands its definition.
For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;Some knowledge of {{&amp;lt; abbr &amp;quot;tcp&amp;quot; &amp;gt;}}
is useful to anyone who works with networks.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Renders as:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Some knowledge of &lt;abbr title="Transmission Control Protocol. A bidirectional protocol to deliver a reliable, ordered, and error-checked stream of octets between applications."&gt;TCP&lt;/abbr&gt;
is useful to anyone who works with networks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To get even fancier, you could make an &lt;a href="https://wellshapedwords.com/posts/hugo-abbreviation-shortcode/"&gt;abbreviation that expands dynamically&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="make-mini-glossaries"&gt;Make mini-glossaries&lt;/h3&gt;
&lt;p&gt;If you notice, most words in the termbase have a &lt;code&gt;category&lt;/code&gt; property.
You could use this to create mini glossaries.&lt;/p&gt;
&lt;p&gt;This requires only two more evaluations in the preceding glossary creator:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Accept one argument in the call&lt;/li&gt;
&lt;li&gt;Print the term and definition only if its &lt;code&gt;category&lt;/code&gt; property matches the argument.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;{{- $entry := (.Get 0) -}}
&lt;dl&gt;
{{- range sort .Site.Data.termbase "term" -}}
{{- if (eq .category $entry) -}}
&lt;dt&gt;{{- .term }} {{ with .abbreviation }} ({{ . -}}) {{- end -}} &lt;/dt&gt;
&lt;dd&gt;{{- .definition -}}&lt;/dd&gt;
{{- end -}}
{{- end -}}
&lt;/dl&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now I&amp;rsquo;ll make some mini-glosses:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-HTML"&gt;#### Nature gloss:
{{&amp;lt; mini-gloss &amp;quot;nature&amp;quot; &amp;gt;}}
#### Protocol gloss:
{{&amp;lt; mini-gloss &amp;quot;protocols&amp;quot; &amp;gt;}}
#### Docs gloss:
{{&amp;lt; mini-gloss &amp;quot;docs&amp;quot; &amp;gt;}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These render as follows:&lt;/p&gt;
&lt;div class="expandable-section"&gt;
&lt;button aria-expanded="false" data-expands="js-expandable-5a82154b0a24ebb4000aa16fba7ed41b"&gt;
&lt;span class="expandable-label"&gt;mini glossaries&lt;/span&gt;
&lt;svg aria-hidden="true" focusable="false" viewBox="0 0 70.866142 70.866141"&gt;
&lt;g transform="translate(0 -981.5)"&gt;
&lt;rect style="stroke-width:0;fill:currentColor" ry="5" height="60" width="9.8985" y="987.36" x="30.051" class="up-strut" /&gt;
&lt;rect style="stroke-width:0;fill:currentColor" ry="5" height="10" width="60" y="1012.4" x="5"/&gt;
&lt;/g&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div id="js-expandable-5a82154b0a24ebb4000aa16fba7ed41b" hidden&gt;
&lt;blockquote&gt;
&lt;h4 id="nature-gloss"&gt;Nature gloss:&lt;/h4&gt;
&lt;dl&gt;&lt;dt&gt;Abies &lt;/dt&gt;
&lt;dd&gt;A genus of coniferous trees, properly called fir.&lt;/dd&gt;&lt;dt&gt;Montiform &lt;/dt&gt;
&lt;dd&gt;Resembling a mountain in form&lt;/dd&gt;&lt;dt&gt;Oryctology &lt;/dt&gt;
&lt;dd&gt;An old name for mineralogy and geology.&lt;/dd&gt;&lt;dt&gt;Topaz &lt;/dt&gt;
&lt;dd&gt;A mineral occuring in rhombic prisms, generally yellowish and pellucid&lt;/dd&gt;&lt;/dl&gt;
&lt;h4 id="protocol-gloss"&gt;Protocol gloss:&lt;/h4&gt;
&lt;dl&gt;&lt;dt&gt;Request for comment &lt;/dt&gt;
&lt;dd&gt;A publication from the standards-setting bodies of the internet&lt;/dd&gt;&lt;dt&gt;Transmission Control Protocol &lt;/dt&gt;
&lt;dd&gt;A bidirectional protocol to deliver a reliable, ordered, and error-checked stream of octets between applications.&lt;/dd&gt;&lt;dt&gt;User Datagram Protocol &lt;/dt&gt;
&lt;dd&gt;A connectionless communication protocol.&lt;/dd&gt;&lt;/dl&gt;
&lt;h4 id="docs-gloss"&gt;Docs gloss:&lt;/h4&gt;
&lt;dl&gt;&lt;dt&gt;Mozilla Developer Network &lt;/dt&gt;
&lt;dd&gt;A great place to read docs&lt;/dd&gt;&lt;dt&gt;Write the Docs &lt;/dt&gt;
&lt;dd&gt;A great place to discuss docs&lt;/dd&gt;&lt;/dl&gt;
&lt;/blockquote &gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="add-supportive-links-to-the-glossary"&gt;Add supportive links to the glossary&lt;/h3&gt;
&lt;p&gt;Perhaps you want to link some glossary entries to their most relevant documentation topic.
For this, you can just add an array of links in a &lt;code&gt;ReadMore&lt;/code&gt; property, then modify the glossary shortcode to print them at the end of the html &lt;code&gt;&amp;lt;dd&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="ways-to-improve"&gt;Ways to improve&lt;/h2&gt;
&lt;p&gt;These shortcodes are unsophisticated.
I wouldn&amp;rsquo;t be surprised to learn of ways to write the logic more efficiently or separate concerns more elegantly within the Hugo architecture.
Still, I have used or am actively using all these strategies in my professional work, and they&amp;rsquo;ve worked well enough.&lt;/p&gt;
&lt;p&gt;But I&amp;rsquo;d be happy to learn of better ways, and I&amp;rsquo;ll update this doc to acknowledge any help.
Some ideas to iterate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In the mini-gloss, make the &lt;code&gt;categories&lt;/code&gt; property an array so one term could display in multiple mini-glossaries.
I think this involves managing the state of an outer context from an inner one.&lt;/li&gt;
&lt;li&gt;Give the abbreviation smarter capitalization.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At some point, a huge YAML file would be a pain to maintain: I wouldn&amp;rsquo;t want to write a whole dictionary like this. Some ideas to improve the UX:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add validation of the data file.&lt;/li&gt;
&lt;li&gt;Combine multiple files into one gloss (the opposite of the mini-gloss example)&lt;/li&gt;
&lt;li&gt;Add some interface to manage entries without directly editing the data file&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hugo also has an open &lt;a href="https://github.com/gohugoio/hugo/issues/10957"&gt;issue to add glossaries&lt;/a&gt;, so maybe this doc will be outdated someday.
Either way, I like the flexibility of using a data file to manage terms.&lt;/p&gt;</description></item><item><title>Write up: WTD Atlantic, 2023</title><link>https://wellshapedwords.com/posts/writeup-wtd-atlantic-2023/</link><pubDate>Wed, 27 Sep 2023 09:03:38 -0300</pubDate><guid>https://wellshapedwords.com/posts/writeup-wtd-atlantic-2023/</guid><description>&lt;p&gt;Earlier this month, I presented a talk at Write the Docs called &amp;ldquo;Graphs, not trees: a ground-up approach to fixing a docs site.&amp;rdquo;&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://wellshapedwords.com/images/dawson-wsw-trees-graphs-sketch.jpg"
alt="A summary of my talk in sketch form" &gt;
&lt;figcaption&gt;Thanks to &lt;a href="https://dennissdawson.wixsite.com/mr--dawson/portfolio"&gt;Dennis Dawson&lt;/a&gt; for this great sketch&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Before I write up my thoughts, let me thank some people:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/yesica-t-uxui/"&gt;Yesica Torrico&lt;/a&gt; for being the design brain behind the visuals, style, and overall organization&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Perlence"&gt;Sviatoslav Abakumov&lt;/a&gt; for giving an incisive critique of a quite crappy first version.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also thank the conference organizers, who did a great job curating talks and running a tight ship; the speakers, who all gave interesting talks; and of course the participants, who made this feel as interactive as an in-person conference.
Through the chat, I really felt that the audience was an active participant in each talk.&lt;/p&gt;
&lt;h2 id="preparation"&gt;Preparation&lt;/h2&gt;
&lt;p&gt;It started with an
.
I realized how inchoate my idea was only when I wrote the
.
Then the
was accepted, and I thought &lt;em&gt;what the hell did I write&lt;/em&gt;?&lt;/p&gt;
&lt;p&gt;From here the process went through a few drafts, starting with a little storyboard and then an outlined script.
The content of the images changed the content of the speech;
for example, I originally used that man-donkey image only because it amused me, but after I referenced it a few more times, it became a through line of my whole narrative.&lt;/p&gt;
&lt;p&gt;One month out, I recorded a loose, semi-improvised talk and requested feedback from my friend Sviatoslav.
He pointed out a number of dead ends and faulty arguments.
So I went back and wrote a script (ironically, now that I knew what I wanted to say, I used a trunk-and-branch approach to re-design my argument). Responding to this feedback made the presentation much tighter.&lt;/p&gt;
&lt;aside aria-label="note" class="note"&gt;
&lt;div&gt;
&lt;svg class="sign" aria-hidden="true" viewBox="0 0 41.667306 41.66729" focusable="false"&gt;
&lt;use xlink:href="#info"&gt;&lt;/use&gt;
&lt;/svg&gt;
The more I read about architecture, the more I regretted my metaphor.
The example is worn-down (c.f. Jane Jacobs, Stewart Brand, James Scott, and so on).
And, for each place I showed, I found comments from former residents who had positive memories.
Now I think the subject is so complex that I should&amp;rsquo;ve chosen another angle.
&lt;/div&gt;
&lt;/aside&gt;
&lt;h2 id="tools"&gt;Tools&lt;/h2&gt;
&lt;p&gt;Effective communication does the most with its medium.
Since the presentation was pre-recorded, my medium involved not only voice and image but also moving tracks that can be layered, edited, and re-watched.
My original dream had animation and multi-track audio and video,
all disciplines I know nothing about—even just making suitable audio was above me—and soon I hauled that dream away.
&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;I do think the &lt;a href="https://revealjs.com"&gt;RevealJS&lt;/a&gt; medium gave me some flexibility to animate and make transitions that worked with the content.&lt;/p&gt;
&lt;p&gt;To control the slides, I used &lt;a href="https://kdeconnect.kde.org/"&gt;KDE connect&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="presentations"&gt;Presentations&lt;/h2&gt;
&lt;p&gt;The best part of the presentation was the live chat.
I was pumped up!&lt;/p&gt;
&lt;p&gt;All the talks were great. My only complaint is that there were too many good choices.&lt;/p&gt;
&lt;p&gt;I also enjoyed the format of the parallel unconference sessions.
I liked to speedily cycle through the rooms, a unique benefit of virtual conferences.
The equivalent in real life would be to constantly barge into new rooms, listen for five seconds, leave, then return five minutes later to pop my head in for another five seconds.&lt;/p&gt;
&lt;h2 id="lessons"&gt;Lessons&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;d never done a talk like this before.
If I could do it over, I&amp;rsquo;d try to get a good recording environment before I did anything else.
I underestimated how hard it is to get good lighting and sound, and by the time I recorded, it was too late to change environments.
I also realized I had a lot of &amp;ldquo;ums&amp;rdquo; and &amp;ldquo;kind of, like, you know, a sort of&amp;hellip;&amp;rdquo;, all noise that obscures the signal.&lt;/p&gt;
&lt;p&gt;More advice for my past self:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Don&amp;rsquo;t underestimate how hard it is to learn a new technology. RevealJS, audio, and video all presented new challenges (especially the latter two).&lt;/li&gt;
&lt;li&gt;Get feedback from multiple perspectives.&lt;/li&gt;
&lt;li&gt;Get feedback on an early, rough form. More polished versions might limit ideas.&lt;/li&gt;
&lt;li&gt;Rewrite and practice—but leave room for improvisation.&lt;/li&gt;
&lt;li&gt;Let the material dictate the story.&lt;/li&gt;
&lt;li&gt;Start with the story, then the images, then rework the story. Add all the little finishing details at the end. Words are way easier to change than images.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pretty bland and generic, to be honest. I think I knew all this before I did the presentation, but now I &lt;em&gt;really know&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="wrap-up"&gt;Wrap up&lt;/h2&gt;
&lt;p&gt;I had never attended a professional conference, and I wasn&amp;rsquo;t expecting much from the virtual format.
But the process ended up being wonderfully interactive, and there were so many smart ideas.&lt;/p&gt;
&lt;p&gt;This was probably the most fun moment of my technical-writing career.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;For the line &amp;ldquo;Google is not a tower, it&amp;rsquo;s a subway&amp;rdquo;, I spent hours &lt;a href="https://raw.githubusercontent.com/MattDodsonEnglish/WTD-Atlantic-presentation-2023/d645bcc14664c16063d542ea6f713292f98a2c03/static/google-train.svg"&gt;animating the Google logo to look like a train&lt;/a&gt;. After watching multiple times to understand what I was trying to represent, my designer colleague asked why I was showing a freight train when I&amp;rsquo;m talking about a subway.&lt;figure&gt; &lt;img src="https://wellshapedwords.com/images/google-subway.png" alt="horrible graphic design"&gt; &lt;figcaption&gt;Besides looking terrible, this &amp;ldquo;Google subway train&amp;rdquo; would give everyone in the station carbon-monoxide poisoning.&lt;/figcaption&gt; &lt;/figure&gt;&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>A shortcode for dynamic abbreviations</title><link>https://wellshapedwords.com/posts/hugo-abbreviation-shortcode/</link><pubDate>Mon, 03 Jul 2023 10:06:11 -0300</pubDate><guid>https://wellshapedwords.com/posts/hugo-abbreviation-shortcode/</guid><description>&lt;aside aria-label="note" class="note"&gt;
&lt;div&gt;
&lt;svg class="sign" aria-hidden="true" viewBox="0 0 41.667306 41.66729" focusable="false"&gt;
&lt;use xlink:href="#info"&gt;&lt;/use&gt;
&lt;/svg&gt;
&lt;p&gt;&lt;strong&gt;This shortcode is mainly for acronyms and initialisms.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That is, abbreviations made from the first letters of the words in a phrase (like &lt;em&gt;NASA&lt;/em&gt; or &lt;em&gt;UDP&lt;/em&gt;).
For details and usage advice, read the Google Style topic about &lt;a href="https://developers.google.com/style/abbreviations"&gt;abbreviations&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/aside&gt;
&lt;p&gt;This shortcode ensures that an abbreviation is always defined on a page and always has an HTML &lt;code&gt;abbr&lt;/code&gt; element.
It also saves writers from writing verbose, error-prone HTML and needing to track when a term first appears on a page.&lt;/p&gt;
&lt;p&gt;For example, this text uses a shortcode:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;_{{&amp;lt; abbr &amp;quot;mttr&amp;quot; &amp;gt;}}_ is a basic measure
of maintainability in repairable systems.
If two systems have equal types
of failure and equal failure rates,
the system with the lower {{&amp;lt; abbr &amp;quot;mttr&amp;quot; &amp;gt;}}
has higher availability.
Let's say it one more time for no reason:
{{&amp;lt; abbr &amp;quot;mttr&amp;quot; &amp;gt;}}.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On the page, it renders as follows. Notice the abbreviations have tooltips:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;
&lt;/em&gt; is a basic measure of maintainability in repairable systems.
If two systems have equal types of failure and equal failure rates, the system with the lower
has higher availability.
Let&amp;rsquo;s say it one more time for no reason:
.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Abbreviations should generally be avoided.
But when they are necessary, this shortcode improves the experience for both readers and writers.&lt;/p&gt;
&lt;h2 id="hugo-requirements"&gt;Hugo requirements&lt;/h2&gt;
&lt;p&gt;The following examples are ready to copy and paste.
To use them, change the terms and abbreviations for your lexis.&lt;/p&gt;
&lt;p&gt;But, to modify these or make your own versions, it helps to know the following Hugo concepts:&lt;/p&gt;
&lt;div class="ticks"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/templates/introduction/#variables"&gt;Variables&lt;/a&gt;. Required both to make custom variables and to understand how to use shortcode, page, and &lt;a href="https://gohugo.io/functions/scratch/"&gt;.Scratch&lt;/a&gt; variables.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/functions"&gt;Functions&lt;/a&gt;. Required to make templates to read data and conditionally write text.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.smashingmagazine.com/2021/02/context-variables-hugo-static-site-generator/"&gt;Context&lt;/a&gt;. Required to understand how Hugo interprets a variable reference.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="steps-to-implement-a-conditional-shortcode"&gt;Steps to implement a conditional shortcode&lt;/h2&gt;
&lt;p&gt;The general procedure is as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Define a term and its abbreviation. To facilitate reuse, store the data in variables or key-value pairs.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;{{ $term := &amp;quot;Mozilla Developer Network&amp;quot; }}
{{ $abbreviation := &amp;quot;MDN&amp;quot; }}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use another variable to give each term a unique counter.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;{{ $count_id := print ( $abbreviation | lower ) &amp;quot;_count&amp;quot;}}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In a shortcode, create a conditional statement based on the counter&amp;rsquo;s value.
If the count has started, write the HTML abbreviation.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;{{ if gt ( .Page.Scratch.Get $count_id) 0 }}
&amp;lt;abbr title=&amp;quot;{{- $term -}}&amp;quot;&amp;gt;{{- $abbreviation -}}&amp;lt;/abbr&amp;gt;
{{ .Scratch.Get $count_id }}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In the &lt;code&gt;else&lt;/code&gt; block, write the full phrase with its abbreviation. Then use the scratch pad to set the counter to &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;{{ else }}
{{ $term }} ({{ $abbreviation }})
{{ .Page.Scratch.Set ($count_id) 1 }}
{{ end }}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I combined these snippets in a shortcode located at &lt;code&gt;layouts/shortcodes/abbreviations/mdn.html&lt;/code&gt;.
I can call it as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;&amp;gt; Do you know the {{&amp;lt; abbreviations/mdn &amp;gt;}} docs?
&amp;gt;
&amp;gt; Yes! I love the {{&amp;lt; abbreviations/mdn &amp;gt;}} docs.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It renders as follows:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Do you know the
Mozilla Developer Network (MDN)
docs?&lt;/p&gt;
&lt;p&gt;Yes! I love the
&lt;abbr title="Mozilla Developer Network"&gt;MDN&lt;/abbr&gt;
docs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;However, if you have multiple terms, this shortcode has avoidable boilerplate.
&lt;strong&gt;The following sections describe some better, more general solutions.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="design-decisions-and-examples"&gt;Design decisions (and examples)&lt;/h2&gt;
&lt;p&gt;Hugo is flexible about how it handles variables and logic, so you have many ways to implement this shortcode.
The following sections outline some approaches to take.
When possible, &lt;strong&gt;I recommend keeping terms in a separate data file.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="keep-terms-in-a-data-file"&gt;Keep terms in a data file&lt;/h3&gt;
&lt;p&gt;One method is to use a data file to store your terms in an array.
I like this approach because it separates concerns and because the list can be reused as the source of other templated pages (like glossaries).&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Write your abbreviations in &lt;code&gt;data/terms.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;- term: Transmission Control Protocol
abbreviation: TCP
- term: User Datagram Protocol
abbreviation: UDP
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Range over the abbreviations in a shortcode.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;
&amp;lt;!--- variables based on shortcode argument --&amp;gt;
{{ $entry := (.Get 0 | lower) }}
{{ $count_id := print $entry &amp;quot;_count&amp;quot;}}
&amp;lt;!-- range over data file --&amp;gt;
{{ range $.Site.Data.terms }}
&amp;lt;!-- Find matches --&amp;gt;
{{ if eq ( .abbreviation | lower) $entry }}
{{ if gt ( $.Page.Scratch.Get ( $count_id )) 0 }}
&amp;lt;abbr title=&amp;quot;{{- .term -}}&amp;quot;&amp;gt;{{- .abbreviation -}}&amp;lt;/abbr&amp;gt;
{{ else }}
{{ .term }} ({{ .abbreviation }})
{{ $.Page.Scratch.Set ( $count_id ) 1 }}
{{ end }}
{{ end }}
{{ end }}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Call it using the abbreviation as an argument.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;The {{&amp;lt; abbreviations &amp;quot;tcp&amp;quot; &amp;gt;}} is not the
{{&amp;lt; abbreviations &amp;quot;udp&amp;quot; &amp;gt;}}.
Again,
{{&amp;lt; abbreviations &amp;quot;tcp&amp;quot; &amp;gt;}} is not
{{&amp;lt; abbreviations &amp;quot;udp&amp;quot; &amp;gt;}}.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This renders as:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The
Transmission Control Protocol (TCP)
is not the
User Datagram Protocol (UDP)
.
Again,
&lt;abbr title="Transmission Control Protocol"&gt;TCP&lt;/abbr&gt;
is not
&lt;abbr title="User Datagram Protocol"&gt;UDP&lt;/abbr&gt;
.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The drawback is that it requires a little more conditional coding, and the syntax to call it is a bit more complex.&lt;/p&gt;
&lt;h3 id="a-shortcode-for-each-file"&gt;A shortcode for each file&lt;/h3&gt;
&lt;p&gt;If you want each acronym to use its own file or shortcode call, try this approach:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;In a partial, write the reusable logic. This file is called &lt;code&gt;abbreviation.html&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt; {{ $count_id := print ( .acronym | lower ) &amp;quot;_count&amp;quot;}}
{{ if gt ( .context.Scratch.Get $count_id) 0 }}
&amp;lt;abbr title=&amp;quot;{{ .term }}&amp;quot;&amp;gt;{{- .acronym -}}&amp;lt;/abbr&amp;gt;
{{ .Scratch.Get $count_id }}
{{ else }}
{{ .term }} ({{ .acronym }})
{{ .context.Scratch.Set ($count_id) 1 }}
{{ end }}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In a shortcode, define the term and abbreviation, and then call the partial.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt; {{ $term := &amp;quot;transmission control protocol&amp;quot; }}
{{ $abbreviation := &amp;quot;tcp&amp;quot; }}
&amp;lt;!-- reusable boilerplate --&amp;gt;
{{ partial &amp;quot;abbreviation.html&amp;quot; (dict &amp;quot;term&amp;quot; $term &amp;quot;abbreviation&amp;quot; $abbreviation &amp;quot;context&amp;quot; .page ) }}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The call syntax is slightly simpler, but I don&amp;rsquo;t like much else about it.
Still, I&amp;rsquo;m happy I figured out how to do this because now I know how to pass variables and context to a partial (thanks to &lt;a href="https://discourse.gohugo.io/t/is-it-possible-to-reuse-logic-and-variables-between-shortcodes/45042"&gt;help from the Hugo forums&lt;/a&gt;).&lt;/p&gt;
&lt;h3 id="in-line"&gt;In-line&lt;/h3&gt;
&lt;p&gt;If you plan to use the shortcode exactly once, you can use an &lt;a href="https://wellshapedwords.com/posts/hugo-inline-shortcodes/"&gt;inline shortcode&lt;/a&gt; and define terms in the page parameters.
Setting terms in the page parameters probably has a number of reasonable uses,
but
I can imagine only two times where an inline shortcode makes sense for this use case:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you are writing a long blog with acronyms that you never plan to use again.&lt;/li&gt;
&lt;li&gt;If you are writing in a modularized set-up where it&amp;rsquo;s difficult to change the &lt;code&gt;layouts&lt;/code&gt; or &lt;code&gt;data&lt;/code&gt; directories.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nevertheless, if you want to do this, read the &lt;a href="https://wellshapedwords.com/posts/hugo-inline-shortcodes/#synthesis"&gt;inline example&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="discussion-and-links"&gt;Discussion and links&lt;/h2&gt;
&lt;p&gt;This shortcode is handy for docs where abbreviations for specialized terms are often used.
Perhaps you could combine it with a prose linter to enforce formatting.&lt;/p&gt;
&lt;p&gt;On broader note, this shortcode hints at something more powerful: a way to conditionally format text based on a mutable page state.
Now that I&amp;rsquo;ve figured out these small examples, maybe I&amp;rsquo;ll discover some greater use of the Hugo scratch pad.&lt;/p&gt;
&lt;p&gt;For more reading, these helped me make this post.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Blog posts about Hugo &lt;code&gt;.Scratch&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.regisphilibert.com/blog/2017/04/hugo-scratch-explained-variable/"&gt;Hugo Scratch explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.luasoftware.com/tutorials/hugo/hugo-scope-variable-in-template/"&gt;Hugo scope in a template&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The forum &lt;a href="https://discourse.gohugo.io/t/is-it-possible-to-reuse-logic-and-variables-between-shortcodes/45042"&gt;post I made&lt;/a&gt; about how to share logic and variables between shortcodes.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>"Allows You To" considered harmful</title><link>https://wellshapedwords.com/posts/antipattern-allows-you-to/</link><pubDate>Wed, 21 Jun 2023 22:11:19 -0300</pubDate><guid>https://wellshapedwords.com/posts/antipattern-allows-you-to/</guid><description>&lt;blockquote class="blockquote"&gt; &lt;p&gt; The Forward feature allows you to forward messages&amp;hellip; &lt;/p&gt; &lt;/blockquote&gt;
&lt;p&gt;This is one of the most common constructions in technical documents&amp;mdash;and one of the least useful.
In fact, you can always avoid the phrase &amp;ldquo;&lt;code&gt;feature&lt;/code&gt; allows you to &lt;code&gt;verb&lt;/code&gt;&amp;rdquo; and your docs will be better off:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The phrase often incorrectly implies an intermediary between a behavior and its result.&lt;/li&gt;
&lt;li&gt;Even when an intermediary is necessary, the semantic meaning of &lt;em&gt;allows&lt;/em&gt; is ambiguous.&lt;/li&gt;
&lt;li&gt;Even when the phrase is appropriate, it has a shorter alternative, &lt;em&gt;lets you&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;rsquo;s the gist. The following sections elaborate with examples, citations, and proposed alternatives.&lt;/p&gt;
&lt;h2 id="reasons-to-avoid-allows-you-to"&gt;Reasons to avoid &amp;ldquo;allows you to&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;In short, the phrase always creates inaccuracy, imprecision, or unnecessary text.&lt;/p&gt;
&lt;h3 id="its-often-inaccurate"&gt;It&amp;rsquo;s often inaccurate&lt;/h3&gt;
&lt;p&gt;Along with similar constructions like &lt;em&gt;lets you&lt;/em&gt; or &lt;em&gt;enables you to&lt;/em&gt;, &lt;em&gt;allows you to&lt;/em&gt; can create an unnecessary grammatical layer between a feature and its behavior.
In linguistic terms, such verbs are examples of &lt;em&gt;catenative verbs&lt;/em&gt;, meaning they subordinate other verbs in a chain.
More specifically, they are examples of &lt;em&gt;causatives&lt;/em&gt;, verbs that &amp;ldquo;cause&amp;rdquo; some other action. &lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;But these constructions often describe behavior that has no secondary effect.
Let&amp;rsquo;s return to this text, lifted from an old version of the WhatsApp documentation (now fixed): &lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;blockquote class="blockquote"&gt; &lt;p&gt; The Forward feature allows you to forward messages from a group or individual chat to another group or individual chat. &lt;/p&gt; &lt;/blockquote&gt;
&lt;p&gt;Is this true? Does the forward feature &lt;em&gt;allow&lt;/em&gt; me to forward messages, or &lt;em&gt;does&lt;/em&gt; it forward messages?
Why not describe behavior directly?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The Forward feature forwards messages&amp;hellip;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Maybe that example sounds awkwardly redundant, but unnecessary catenative verbs commonly intrude in all sorts of technical description.
For example, this is from the
&lt;abbr title="Mozilla Developer Network"&gt;MDN&lt;/abbr&gt; JavaScript reference.&lt;/p&gt;
&lt;blockquote&gt;
&lt;dl&gt;
&lt;dt&gt;&lt;code&gt;Window.cancelIdleCallback()&lt;/code&gt;&lt;/dt&gt;
&lt;dd&gt;Enables you to cancel a callback previously scheduled with &lt;code&gt;Window.requestIdleCallback&lt;/code&gt;.&lt;/dd&gt;
&lt;/dl&gt;
&lt;/blockquote&gt;
&lt;p&gt;What does the method do: cancel callbacks or enable cancelling a callback elsewhere?
Though this example uses &lt;em&gt;enables&lt;/em&gt; instead of &lt;em&gt;allows&lt;/em&gt;, the effect is the same: unnecessary words and increased ambiguity.&lt;/p&gt;
&lt;h3 id="its-meaning-is-often-ambiguous"&gt;Its meaning is often ambiguous&lt;/h3&gt;
&lt;p&gt;Sometimes a writer really does want to describe a feature as a means to some end.
In this case, a catenative verb might be appropriate.
For example, these are from the &lt;abbr title="Mozilla Developer Network"&gt;MDN&lt;/abbr&gt; docs:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Making a copy like this allows you to effectively use the request/response again while varying the init options&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;The Twitter API allows you to do things like displaying your latest tweets on your website.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Both of these sentences describe one action (making a copy, calling the Twitter API) that can help users accomplish some later action (reusing, displaying).
So causatives seem appropriate.
But even in these cases, are phrases like &lt;em&gt;allows you to&lt;/em&gt; or &lt;em&gt;lets you&lt;/em&gt; really the best constructions?&lt;/p&gt;
&lt;p&gt;If you look up &lt;em&gt;allow&lt;/em&gt; or &lt;em&gt;let&lt;/em&gt; in any dictionary, you&amp;rsquo;ll find the first definition is always about giving permission. &lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;
Isn&amp;rsquo;t it patronizing and inaccurate to think of a software object as giving me, a human, permission to display tweets on a website?
(I could just take a screenshot and bypass the API).
Of course, &lt;em&gt;allows you to&lt;/em&gt; also has a more general meaning (I discuss that in the last section).
But in software docs, where many readers are reading in a foreign language or using a machine translator, the least ambiguous option is best.&lt;/p&gt;
&lt;h3 id="lets-you-is-shorter"&gt;&amp;ldquo;Lets you&amp;rdquo; is shorter&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://wellshapedwords.com/images/reduce-noise-boost-signal-wellshapedwords.svg" alt="Comparing a red signal against two gray blocks of noise. The volume of the noise is proportional to the amount of filler text."&gt;&lt;/p&gt;
&lt;p&gt;Maybe you find my previous arguments unconvincing.
Besides, some software features really do configure permissions.
Either way, &lt;em&gt;lets you&lt;/em&gt; is still a better construction.
The reason is undeniable: it&amp;rsquo;s shorter.&lt;/p&gt;
&lt;p&gt;Good technical writing should be &lt;a href="https://wellshapedwords.com/essentials/principles/#short"&gt;as short as possible&lt;/a&gt;.
Semantically, the following sentences are essentially identical: &lt;sup id="fnref1:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-diff"&gt;- The `authenticate` endpoint allows you to view resources according to your credentials.
+ The `authenticate` endpoint lets you view resources according to your credentials.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But &lt;em&gt;lets you&lt;/em&gt; uses five fewer characters.
If you can transmit the same meaning with less text, you should: boost signal, reduce noise.&lt;/p&gt;
&lt;p&gt;In a single sentence, such an improvement is small and rather inconsequential.
But small changes add up,
and technical communicators should try to standardize quality at all levels.&lt;/p&gt;
&lt;p&gt;I am not alone in this idea: the Google Style Guide also recommends &lt;em&gt;lets&lt;/em&gt; over &lt;em&gt;allows&lt;/em&gt;. &lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2 id="alternatives"&gt;Alternatives&lt;/h2&gt;
&lt;aside aria-label="note" class="note"&gt;
&lt;div&gt;
&lt;svg class="sign" aria-hidden="true" viewBox="0 0 41.667306 41.66729" focusable="false"&gt;
&lt;use xlink:href="#info"&gt;&lt;/use&gt;
&lt;/svg&gt;
These ideas&amp;mdash;this whole post&amp;mdash;were generated from a thread on the WriteTheDocs Slack channel.
I thank everyone for the robust discussion.
&lt;/div&gt;
&lt;/aside&gt;
&lt;p&gt;Instead of &lt;em&gt;allows you to&lt;/em&gt; and other intermediaries, tailor your vocabulary and syntax &lt;a href="https://wellshapedwords.com/essentials/practices/#structure-content-according-to-different-reader-needs"&gt;to the goals of your text&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="in-task-based-docs-front-the-action"&gt;In task-based docs, front the action&lt;/h3&gt;
&lt;p&gt;In a doc that describes procedures, consider inverting your syntax.
That is, place the result before the command. For example, a doc called &amp;ldquo;How to use Nginx&amp;rdquo; could have a phrase like this.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;To stop the server, run &lt;code&gt;nginx -s quit&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Beginning with an &lt;a href="https://en.wiktionary.org/wiki/infinitive_of_purpose" title="The use of the infinitive form of a verb in answer to the implied question, why?"&gt;&lt;em&gt;infinitive of purpose&lt;/em&gt;&lt;/a&gt; helps readers scan for what they want to do (since readers scan in an &lt;a href="https://wellshapedwords.com/essentials/principles#no-reading"&gt;F-shaped pattern&lt;/a&gt;).&lt;/p&gt;
&lt;h3 id="in-references-put-behavior-before-context"&gt;In references, put behavior before context&lt;/h3&gt;
&lt;p&gt;Descriptions in references should be spartan.
For example, a doc called &amp;ldquo;Nginx CLI reference&amp;rdquo; might have something like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;dl&gt;
&lt;dt&gt;&lt;code&gt;nginx -s quit&lt;/code&gt;&lt;/dt&gt;
&lt;dd&gt;Stops the server&lt;/dd&gt;
&lt;/dl&gt;
&lt;/blockquote&gt;
&lt;p&gt;If it&amp;rsquo;s important to communicate goals, you could provide context in a second sentence. For example:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The OCR API processes a batch of images and detects common visual features.
These groupings can serve as search filters for a large image database.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="use-the-imperative"&gt;Use the imperative&lt;/h3&gt;
&lt;p&gt;This is often the simplest and most direct way to describe the purpose of a tool.
For example, the Whatsapp doc quoted at the beginning now reads as follows:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Use the Forward feature to forward messages from an individual or group chat to another individual or group chat.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Some writer at Facebook also didn&amp;rsquo;t like &lt;em&gt;allows you to&lt;/em&gt;!&lt;/p&gt;
&lt;h2 id="but-who-cares-about-such-a-common-and-tiny-phrase"&gt;But who cares about such a common and tiny phrase?&lt;/h2&gt;
&lt;p&gt;True, this issue is small in the grand scheme of things.
Good docs won&amp;rsquo;t be spoiled by an &lt;em&gt;allows you to&lt;/em&gt; (&lt;abbr title="Mozilla Developer Network"&gt;MDN&lt;/abbr&gt; is proof).
But these details do matter if you think systematically about creating quality documentation.
If &lt;em&gt;allows you to&lt;/em&gt; creates an unnecessary intermediary between an actor and action, it also creates inaccuracy, and technical communicators should always &lt;a href="https://wellshapedwords.com/essentials/principles#truth"&gt;Try to tell the truth&lt;/a&gt;. Even when it&amp;rsquo;s appropriate, shorter alternatives exist, and technical texts should be as simple as possible and no simpler.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s also true that the phrase is common.
When it comes to language in ordinary life, I&amp;rsquo;m a descriptivist.
And I must acknowledge that &lt;em&gt;allows you to&lt;/em&gt; is a perfectly common expression, &lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;
very often used without any sub-text about giving permission.
However, technical writing represents a highly constrained use of language, where brevity and clarity are more important than &amp;ldquo;naturalness.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Avoid &lt;em&gt;allows you to&lt;/em&gt;. This isn&amp;rsquo;t a big issue, but it&amp;rsquo;s an easy one to fix, and better is better.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Pullum et al. &lt;em&gt;The Cambridge Grammar of the English Language.&lt;/em&gt; Chapter 14, &amp;ldquo;Non-finite and verbless clauses.&amp;rdquo;&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;At least, from the quoted text in this &lt;a href="https://ell.stackexchange.com/questions/236923/is-it-better-to-say-this-feature-allows-you-to-or-this-feature-lets-you"&gt;Stack Exchange Post&lt;/a&gt;,
I infer that the WhatsApp page used to say &amp;ldquo;allows you to.&amp;rdquo;&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;For example, the &lt;a href="https://dictionary.cambridge.org/dictionary/english/allow"&gt;Cambridge dictionary entry&lt;/a&gt; for &lt;em&gt;allow&lt;/em&gt; says &amp;ldquo;To give permission.&amp;rdquo; The entry for &lt;em&gt;let&lt;/em&gt; says &amp;ldquo;To allow.&amp;rdquo;&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref1:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;&lt;a href="https://developers.google.com/style/word-list#allows-you-to"&gt;&amp;ldquo;Instead, use &lt;em&gt;lets you&lt;/em&gt;.&amp;rdquo;&lt;/a&gt;&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;For informal evidence, the expression has over 16 thousand occurrences in the &lt;a href="https://youglish.com/pronounce/allows%20you%20to/english?"&gt;Youglish corpus&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>Sentence repair: technical edits for two academic texts</title><link>https://wellshapedwords.com/posts/sentence-repair/</link><pubDate>Sat, 17 Jun 2023 23:15:24 -0300</pubDate><guid>https://wellshapedwords.com/posts/sentence-repair/</guid><description>&lt;p&gt;I approach editing with the same creed as a doctor: &lt;a href="https://en.wikipedia.org/wiki/Primum_non_nocere"&gt;&lt;em&gt;first do no harm&lt;/em&gt;&lt;/a&gt;.
My constant doubt is I&amp;rsquo;ll over-edit and change the meaning.
This is why I have researched set of &lt;a href="https://wellshapedwords.com/essentials/practices/"&gt;practices&lt;/a&gt;: to reduce guessing and have a standardized set of quality improvements.&lt;/p&gt;
&lt;p&gt;However, when reading for fun, I sometimes can&amp;rsquo;t stop myself from trying to fix sentences that confuse me.
Here are two examples of how a writer of software documentation would fix two texts that have nothing to do with software.&lt;/p&gt;
&lt;h2 id="wait-who-did-spinoza-write-for"&gt;Wait, who did Spinoza write for?&lt;/h2&gt;
&lt;p&gt;This little primer on Spinoza is clear and well-written, but this particular sentence is hard to understand.&lt;/p&gt;
&lt;blockquote class="blockquote"&gt; &lt;p&gt; &lt;p&gt;It may surprise you to hear that Spinoza’s &lt;em&gt;Ethics&lt;/em&gt; was written just as
much for a non-expert audience in the twenty-first century as for the
philosophical world of the seventeenth.&lt;/p&gt;
&lt;p&gt;&amp;mdash;Beth Lord, &lt;em&gt;Spinoza&amp;rsquo;s Ethics&lt;/em&gt;&lt;/p&gt;
&lt;/p&gt; &lt;/blockquote&gt;
&lt;p&gt;It&amp;rsquo;s hard to diagnose exactly why:
at 33 words, it&amp;rsquo;s a bit long, and the negation, &amp;ldquo;non-expert&amp;rdquo; is avoidable.
But no major flaw jumps out.
Yet it&amp;rsquo;s hard to understand the sentence&amp;rsquo;s relationships.&lt;/p&gt;
&lt;p&gt;Having read this sentence a few times, I take it to literally mean that Spinoza wrote this book for two audiences:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The philosophical world of the 17th century&lt;/li&gt;
&lt;li&gt;Non-experts of the 21st century.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The surrounding context, though, doesn&amp;rsquo;t give Spinoza so much foresight.
Rather, Lord wants to say that the audience of this book have always been experts, &amp;ldquo;steeped in the philosophical tradition.&amp;rdquo;
I think the confusion comes from the length and the long phrases.&lt;/p&gt;
&lt;p&gt;How about one of these? One focuses on the difficulty of reading it, and another focuses on the publication audience.&lt;/p&gt;
&lt;p&gt;To emphasize the difficulty of the text:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It may surprise you to hear that Spinoza&amp;rsquo;s &lt;em&gt;Ethics&lt;/em&gt; has always been for a specialist audience. A non-expert of the seventeenth century would find the book just as difficult to read as a non-expert of the twenty-first century would.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To emphasize who the work was published for:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It may surprise you to hear that Spinoza&amp;rsquo;s Ethics were written for an expert audience.
This was true when it was published for the philosophical world of the seventeenth century, and it&amp;rsquo;s just as true today.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is an example where expanding might make a sentence easier to parse.
In the next example, cutting text makes the sentence clearer.&lt;/p&gt;
&lt;h2 id="oracular-or-plain-verbose"&gt;Oracular or plain verbose?&lt;/h2&gt;
&lt;p&gt;This example is trickier.
Though shorter, it has many more phrases that I try to avoid.
Yet I think fans of this work would find my proposed edits disagreeable.&lt;/p&gt;
&lt;blockquote class="blockquote"&gt; &lt;p&gt; &lt;p&gt;A building or town will only be alive to the extent that it is governed by the timeless way.&lt;/p&gt;
&lt;p&gt;&amp;mdash;Christopher Alexander, &lt;em&gt;The Timeless Way of Building&lt;/em&gt;&lt;/p&gt;
&lt;/p&gt; &lt;/blockquote&gt;
&lt;p&gt;First, a note on style:
from the little I&amp;rsquo;ve read, Christopher Alexander seems to like these orbicular ways of writing, and perhaps some readers find this style imbues a &amp;ldquo;timelessness&amp;rdquo; of its own.
Furthermore, the passive construction arranges the sentence to end with &lt;em&gt;timeless way&lt;/em&gt;, which perhaps reinforces his central subject.&lt;/p&gt;
&lt;p&gt;In an essay, such rhetorical flair is the author&amp;rsquo;s prerogative.
But in a piece of technical writing, I&amp;rsquo;d chop this down.
Though the sentence is short, it could be shorter:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Will only be alive.&lt;/em&gt; Why use the future? Was this false at the time of writing?&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Be alive.&lt;/em&gt; Why use an adjective when a verb exists?&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Only be&lt;/em&gt;. What is &lt;em&gt;only&lt;/em&gt; modifying? The verb &lt;em&gt;be&lt;/em&gt; or the extent?&lt;/li&gt;
&lt;li&gt;&lt;em&gt;To the extent that.&lt;/em&gt; Doesn&amp;rsquo;t &lt;em&gt;only&lt;/em&gt; imply &lt;em&gt;to the extent&lt;/em&gt;?&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Is governed by.&lt;/em&gt; Is the passive functionally necessary?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&amp;rsquo;s chop it down progressively.
I&amp;rsquo;ll let you decide where I went too far.&lt;/p&gt;
&lt;p&gt;First, removing the passive and future doesn&amp;rsquo;t change meaning at all, but it brings the character count from 93 to 87.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A building or town is only alive to the extent that it is governed by the timeless way.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Removing the passive gets it down to 82.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A building or town is only alive to the extent that the timeless way governs it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Next, does the distinction between &amp;ldquo;it is alive&amp;rdquo; and &amp;ldquo;it lives&amp;rdquo; matter here?
If not, we can get the character count down to 78.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A building or town lives only to the extent that the timeless way governs it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Aren&amp;rsquo;t &amp;ldquo;only&amp;rdquo; and &amp;ldquo;to the extent&amp;rdquo; redundant?
Maybe only acts as necessary emphasis.
But if not, removing only gets it to 73.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A building or town lives to the extent that the timeless way governs it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Or maybe we could just compress it in a relative clause.
Even restoring the passive, this version brings the character count down to 64.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A building or town lives only when governed by the timeless way.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It seems like Alexander wants to make a boundary in time and space delimited by the &amp;ldquo;the timeless way&amp;rdquo;: life and timeless governance on one side, death and ephemeral governance on the other.
Perhaps his &amp;ldquo;to the extent that&amp;rdquo; conveys this boundary better than my truncated version does.&lt;/p&gt;
&lt;p&gt;Have I changed the essential meaning too much?&lt;/p&gt;
&lt;h2 id="a-note-on-style-and-audience"&gt;A note on style and audience&lt;/h2&gt;
&lt;p&gt;As an editor, I&amp;rsquo;d be much more comfortable proposing my change to Beth Lord&amp;rsquo;s text than I would to Christopher Alexander&amp;rsquo;s.
Lord&amp;rsquo;s book is about philosophy, but her goals are much closer to those of a technical writer: make a difficult topic easier to approach.
Alexander&amp;rsquo;s work is in some sense a philosophy of its own, which specifically mimics works like the &lt;em&gt;Tao Te Ching&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;In any case, I&amp;rsquo;m not being totally fair to either author, because they have different audiences and style considerations than I do.
People rarely settle in to enjoy a technical text as they would for a favorite book.&lt;/p&gt;
&lt;p&gt;The audience of my technical texts are usually reading to accomplish something else, and a large majority of them may be reading in a second language and under stressful work environments.
For this reason, I impose a minimalist, even brutalist, style on my technical texts, a style that would ruin the work of many great prose writers.
As a technical editor, I would not be able to ethically operate on the works of, for example, Annie Dillard or Sir Thomas Browne.&lt;/p&gt;</description></item><item><title>Uses for Hugo inline shortcodes</title><link>https://wellshapedwords.com/posts/hugo-inline-shortcodes/</link><pubDate>Tue, 09 May 2023 00:00:00 +0000</pubDate><guid>https://wellshapedwords.com/posts/hugo-inline-shortcodes/</guid><description>&lt;p&gt;A typical Hugo setup designates one directory for content (the text that people read) and another for presentation configuration (the logic that controls how the text is organized on the page). Such &lt;a href="https://en.wikipedia.org/wiki/Separation_of_concerns#HTML,_CSS,_JavaScript"&gt;separation of concerns&lt;/a&gt; is quite sensible, but it adds some overhead to the writing process.&lt;/p&gt;
&lt;p&gt;As it turns out, Hugo also supports &lt;em&gt;inline shortcodes&lt;/em&gt;, defined directly in the content file.
This means you can stay in your markdown file and still create shortcodes for templates, variables, and data mutations on the content within.&lt;/p&gt;
&lt;p&gt;Inline shortcodes might be handy in a few cases.&lt;/p&gt;
&lt;dl&gt;
&lt;dt&gt;When the shortcode is scoped to one page&lt;/dt&gt;
&lt;dd&gt;This is the essential circumstance. If the shortcode is to be reused across content files, it should be shared in the &lt;code&gt;layouts/shortcodes&lt;/code&gt; dir.&lt;/dd&gt;
&lt;dt&gt;When the build and content repos are separated&lt;/dt&gt;
&lt;dd&gt;If the Hugo build system is &lt;a href="https://gohugo.io/hugo-modules/"&gt;modularized&lt;/a&gt; across different repos, an inline shortcode might be more convenient.&lt;/dd&gt;
&lt;dt&gt;For quick prototyping&lt;/dt&gt;
&lt;dd&gt;Sometimes, opportunities to template or reuse text are revealed only after writers start creating.
With inline shortcodes, writers could prototype the shortcode in the markdown file, then abstract it out later.&lt;/dd&gt;
&lt;dt&gt;For fun&lt;/dt&gt;
&lt;dd&gt;It&amp;rsquo;s liberating to write shortcodes on the fly—no need to think about how things fit into the bigger picture.&lt;/dd&gt;
&lt;/dl&gt;
&lt;p&gt;This post documents ways to use inline shortcodes for common technical-writing problems.
All the shortcodes here also work as &amp;ldquo;normal&amp;rdquo; standalone functions.
So, again, if you want to reuse a shortcode here, remove the &lt;code&gt;inline&lt;/code&gt; suffix and add it to &lt;code&gt;layouts/shortcodes&lt;/code&gt; instead.&lt;/p&gt;
&lt;h2 id="requirements"&gt;Requirements&lt;/h2&gt;
&lt;p&gt;To use inline shortcodes, you must:&lt;/p&gt;
&lt;div class="ticks"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Use Hugo version 0.52.0 or later&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Enable inline shortcodes in your Hugo config:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;enableInlineShortcodes: true
&lt;/code&gt;&lt;/pre&gt;
&lt;aside aria-label="warning" class="note warning"&gt;
&lt;div&gt;
&lt;svg class="sign" aria-hidden="true" viewBox="0 0 48.430474 41.646302" focusable="false"&gt;
&lt;use xlink:href="#warning"&gt;&lt;/use&gt;
&lt;/svg&gt;
Inline shortcodes execute code. Enable them only if you trust your authors and reviewers.
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/aside&gt;
&lt;/div&gt;
&lt;h2 id="syntax"&gt;Syntax&lt;/h2&gt;
&lt;p&gt;To declare an inline shortcode:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Name your shortcode in opening and closing tags. Append &lt;code&gt;.inline&lt;/code&gt; to the tag name.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Within the shortcode tag, write the value that the shortcode will return.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;!--- opening tag --&amp;gt;
{{&amp;lt; hello.inline &amp;gt;}}
&amp;lt;!--- content --&amp;gt;
Hello, you!
&amp;lt;!--- closing tag --&amp;gt;
{{&amp;lt; /hello.inline &amp;gt;}}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The first time you use the shortcode, the value renders. For example, the preceding snippet renders as follows:&lt;/p&gt;
&lt;blockquote&gt;
Hello, you!
&lt;/blockquote&gt;
&lt;p&gt;On subsequent calls, use one tag with a self-closing &lt;code&gt;/&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;{{&amp;lt; hello.inline /&amp;gt;}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Like all shortcodes, inline shortcodes can use parameters, site or page data, and built-in Hugo functions.&lt;/p&gt;
&lt;h2 id="examples"&gt;Examples&lt;/h2&gt;
&lt;p&gt;These examples show inline shortcodes to solve some manual tasks that arise in my work as a technical writer.&lt;/p&gt;
&lt;h3 id="embed-dynamic-calculations"&gt;Embed dynamic calculations&lt;/h3&gt;
&lt;p&gt;This has its own post: &lt;a href="https://wellshapedwords.com/posts/hugo-inline-spreadsheet/"&gt;Hugo inline spreadsheet&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="expand-acronym-names"&gt;Expand acronym names&lt;/h3&gt;
&lt;aside aria-label="note" class="note"&gt;
&lt;div&gt;
&lt;svg class="sign" aria-hidden="true" viewBox="0 0 41.667306 41.66729" focusable="false"&gt;
&lt;use xlink:href="#info"&gt;&lt;/use&gt;
&lt;/svg&gt;
For more robust ways to implement this, read &lt;a href="https://wellshapedwords.com/posts/hugo-abbreviation-shortcode/"&gt;A shortcode for dynamic abbreviations&lt;/a&gt;.
&lt;/div&gt;
&lt;/aside&gt;
&lt;p&gt;This first example uses a variable to avoid typing verbose text.&lt;/p&gt;
&lt;p&gt;The HTML &lt;code&gt;abbr&lt;/code&gt; element has a &lt;code&gt;title&lt;/code&gt; element that expands when the cursor hovers over it.
Here&amp;rsquo;s the example from the Mozilla Developer Network docs:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;Ashok's joke made me
&amp;lt;abbr title=&amp;quot;Laugh Out Loud&amp;quot;&amp;gt;LOL&amp;lt;/abbr&amp;gt;
big time.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That HTML renders as follows:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Ashok&amp;rsquo;s joke made me
&lt;abbr title="Laugh Out Loud"&gt;LOL&lt;/abbr&gt; big time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It&amp;rsquo;s a chore to write that HTML each time, and the chance of a typo increases with each use.
If you need to expand this acronym on only one page, you could use an inline shortcode to minimize typing.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;The first time you use an acronym shortcode,
you type more,
{{&amp;lt; lol-abbr.inline &amp;gt;}}
&amp;lt;abbr title=&amp;quot;laugh out loud&amp;quot;&amp;gt;lol&amp;lt;/abbr&amp;gt;
{{&amp;lt; /lol-abbr.inline &amp;gt;}}
On subsequent uses, you type less,
{{&amp;lt; lol-abbr.inline /&amp;gt;}}.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The preceding snippet renders as follows:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The first time you use an acronym shortcode,
you type more,
&lt;abbr title="Laugh Out Loud"&gt;lol&lt;/abbr&gt;
&lt;/p&gt;
&lt;p&gt;On subsequent uses, you type less,
&lt;abbr title="Laugh Out Loud"&gt;lol&lt;/abbr&gt;
.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="frontmatter"&gt;Use page frontmatter&lt;/h3&gt;
&lt;p&gt;For another way to make variables, you can add a new property to your frontmatter.
The inline shortcode can access the value as a variable scoped to the page.&lt;/p&gt;
&lt;p&gt;For example, you could use frontmatter to keep track of the last application version tested, and then
use an inline shortcode to automatically update version references within the docs.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Add the version name in a frontmatter property:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;last_version: Arctic effulgence
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Reference the frontmatter value within your inline shortcode as &lt;code&gt;$.Page.Params.&amp;lt;key-name&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;{{% frontmatter.inline %}}
This procedure was last tested on version
`{{ $.Page.Params.last_version }}`
{{% /frontmatter.inline %}}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This shortcode renders as follows:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This procedure was last tested on version
&lt;code&gt;Arctic effulgence&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The text processing could get much more complex if you want to mutate the data, or if your frontmatter includes arrays or nested object.&lt;/p&gt;
&lt;aside aria-label="note" class="note"&gt;
&lt;div&gt;
&lt;svg class="sign" aria-hidden="true" viewBox="0 0 41.667306 41.66729" focusable="false"&gt;
&lt;use xlink:href="#info"&gt;&lt;/use&gt;
&lt;/svg&gt;
Inline shortcodes can access other site data, too.
But frontmatter is a good example, since this document is all about working in a single page.
&lt;/div&gt;
&lt;/aside&gt;
&lt;h3 id="make-a-page-specific-template"&gt;Make a page-specific template&lt;/h3&gt;
&lt;p&gt;You can also use inline shortcodes to template repetitive phrasing (or lists or tables) in a document.&lt;/p&gt;
&lt;p&gt;A template could apply to even single sentences.
For example, a page of conceptual content may end each section with links to learn more.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Write a snippet sentence with placeholder values.&lt;/p&gt;
&lt;p&gt;This template uses variables for author names and their &lt;code&gt;id&lt;/code&gt; within the &lt;a href="https://gutenberg.org"&gt;Project Gutenberg&lt;/a&gt; online library.
For clarity, I&amp;rsquo;m using named parameters like &lt;code&gt;author&lt;/code&gt; instead of positional arguments.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-markdown"&gt;For more by {{ .Get &amp;quot;author&amp;quot; }}, visit the author's
&amp;lt;a href=&amp;quot;({{ $.Page.Params.library_url }}/{{ .Get &amp;quot;id&amp;quot; }})&amp;quot;
Gutenberg page. &amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;aside aria-label="note" class="note"&gt;
&lt;div&gt;
&lt;svg class="sign" aria-hidden="true" viewBox="0 0 41.667306 41.66729" focusable="false"&gt;
&lt;use xlink:href="#info"&gt;&lt;/use&gt;
&lt;/svg&gt;
Continuing the previous &lt;a href="#frontmatter"&gt;Frontmatter example&lt;/a&gt;, the base Gutenberg URL is part of the page frontmatter. Unlike author or ID, this shouldn&amp;rsquo;t change between shortcode calls.
&lt;/div&gt;
&lt;/aside&gt;
&lt;/br&gt;
&lt;ol start="2"&gt;
&lt;li&gt;
&lt;p&gt;Wrap the template in an inline shortcode tag, with values for the parameters.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-markdown"&gt;{{&amp;lt; learn-more.inline author=&amp;quot;Emily Dickinson&amp;quot; id=&amp;quot;996&amp;quot; &amp;gt;}}
For more by {{ .Get &amp;quot;author&amp;quot; }},
visit the author's
&amp;lt;a href=
&amp;quot;{{ $.Page.Params.library_url }}/{{ .Get &amp;quot;id&amp;quot; }}&amp;quot;&amp;gt;
Gutenberg page. &amp;lt;/a&amp;gt;
{{&amp;lt; /learn-more.inline &amp;gt;}}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This shortcode generates the following HTML snippet.&lt;/p&gt;
&lt;blockquote class="blockquote"&gt; &lt;p&gt; For more by Emily Dickinson,
visit the author&amp;rsquo;s
&lt;a href=
"https://gutenberg.org/ebooks/author/996"&gt;
Gutenberg page. &lt;/a&gt; &lt;/p&gt; &lt;/blockquote&gt;
&lt;p&gt;After declaring it once, I can reuse with new properties:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;{{% learn-more.inline author=&amp;quot;Sappho&amp;quot; id=&amp;quot;32618&amp;quot; /%}}
{{% learn-more.inline author=&amp;quot;shakespeare&amp;quot; id=&amp;quot;65&amp;quot; /%}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the final rendering, these shortcodes appear as follows:&lt;/p&gt;
&lt;blockquote class="blockquote"&gt; &lt;p&gt; &lt;ul&gt;
&lt;li&gt;
For more by Sappho,
visit the author's
&lt;a href=
"https://gutenberg.org/ebooks/author/32618"&gt;
Gutenberg page. &lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
For more by Shakespeare,
visit the author's
&lt;a href=
"https://gutenberg.org/ebooks/author/65"&gt;
Gutenberg page. &lt;/a&gt;
&lt;/li&gt;
&lt;ul&gt;
&lt;/p&gt; &lt;/blockquote&gt;
&lt;h3 id="make-a-counter"&gt;Make a counter&lt;/h3&gt;
&lt;p&gt;Sometimes, a tutorial has multiple sections and a required order.
I generally avoid numbering these sections, because many things could go wrong:
I could miscount, more steps could be added later, and so on.&lt;/p&gt;
&lt;p&gt;An inline shortcode can serve as a counter to dynamically increment steps.
For this one, I create two shortcodes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;The &lt;code&gt;counter-reset&lt;/code&gt; starts or resets the counter, &lt;code&gt;step&lt;/code&gt;. It uses the &lt;a href="https://gohugo.io/functions/scratch/"&gt;&lt;code&gt;.Scratch&lt;/code&gt;&lt;/a&gt; function to store page data.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;{{% counter-reset.inline %}}
{{ .Page.Scratch.Set &amp;quot;step&amp;quot; 1 }}
{{% /counter-reset.inline %}}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;code&gt;increment&lt;/code&gt; shortcode prints the value of the counter and then increments it.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-markdown"&gt;{{% increment.inline %}}
{{ .Page.Scratch.Get &amp;quot;step&amp;quot; */}}
{{ .Page.Scratch.Add &amp;quot;step&amp;quot; 1 */}}
{{% /increment.inline %}}
&amp;lt;!--- call it again --&amp;gt;
{{% increment.inline %}}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For demonstration purposes, I&amp;rsquo;ll incorporate the counter with a template, wrapping the &lt;code&gt;increment&lt;/code&gt; function around text to describe a procedure.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;{{% counter-reset.inline %}}
{{- .Page.Scratch.Set &amp;quot;step&amp;quot; 1 -}}
{{% /counter-reset.inline %}}
# How to run a race
{{% increment.inline &amp;quot;start at the beginning&amp;quot; /*%}}
## Step {{ .Page.Scratch.Get &amp;quot;step&amp;quot; }}: {{ .Get 0 }}
{{- .Page.Scratch.Add &amp;quot;step&amp;quot; 1 -}}
{{%/* /increment.inline %}}
{{% increment.inline &amp;quot;run to the middle&amp;quot; /%}}
{{% increment.inline &amp;quot;finish strong&amp;quot; /%}}
# How to technically write
&amp;lt;!--- reset counter --&amp;gt;
{{% counter-reset.inline /%}}
{{% increment.inline &amp;quot;say what you're going to say&amp;quot; /%}}
{{% increment.inline &amp;quot;say it&amp;quot; /%}}
{{% increment.inline &amp;quot;say what you said&amp;quot; /%}}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;
# How to run a race
## Step 1: start at the beginning
## Step 2: run to the middle
## Step 3: finish strong
# How to technically write
&amp;lt;!--- reset counter --&amp;gt;
## Step 1: say what you're going to say
## Step 2: say it
## Step 3: say what you said
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="synthesis"&gt;Synthesis: First the full phrase, then abbreviate with tooltip&lt;/h2&gt;
&lt;p&gt;This example uses one shortcode to write the entire phrase the first time a term is used, then to write the abbreviation with a tooltip all other uses.
This way, you can move acronyms around and always be sure that they are defined the first time.&lt;/p&gt;
&lt;p&gt;This combines techniques from previous sections.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The page frontmatter defines a word and its acronym.&lt;/li&gt;
&lt;li&gt;The shortcode uses the &lt;code&gt;Scratch&lt;/code&gt; pad to determine whether the acronym has been used.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This page has the following frontmatter:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;lol:
term: Laugh out loud
acronym: LOL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The following shortcode uses &lt;code&gt;with&lt;/code&gt; to check whether the shortcode has already been used, then use that as a condition to print different strings.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;The first time, I'll write an inline shortcode
and expand the entire phrase.
And that phrase is...
&amp;lt;!-- define acronym --&amp;gt;
- {{&amp;lt; acro.inline &amp;gt;}}
&amp;lt;!-- use with to if check word exists --&amp;gt;
{{ with .Page.Scratch.Get &amp;quot;lol_count&amp;quot; }}
&amp;lt;!-- If it exists, write abbr --&amp;gt;
&amp;lt;abbr title=&amp;quot;{{ $.Page.Params.lol.term }}&amp;quot;&amp;gt;
{{ $.Page.Params.lol.acronym }}&amp;lt;/abbr&amp;gt;
&amp;lt;!-- If not, write whole term, then indicate it was used by setting counter --&amp;gt;
{{ else }}
{{ $.Page.Params.lol.term }} ({{ $.Page.Params.lol.acronym }})
&amp;lt;!-- change state of variable to indicate use --&amp;gt;
{{ .Page.Scratch.Set &amp;quot;lol_count&amp;quot; 1 }}
{{ end }}
{{&amp;lt; /acro.inline &amp;gt;}}
Now I'll use it a few more times:
- 2nd
{{&amp;lt; acro.inline /&amp;gt;}}
- 3rd
{{&amp;lt; acro.inline /&amp;gt;}}
- 4th
{{&amp;lt; acro.inline /&amp;gt;}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is how it renders:&lt;/p&gt;
&lt;p&gt;The first time, I&amp;rsquo;ll write an inline shortcode and expand the entire phrase.
And that phrase is&amp;hellip;&lt;/p&gt;
&lt;!-- define acronym --&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;!-- use with to check if word exists --&gt;
laugh out loud (LOL)
&lt;!-- change state of variable to indicate use --&gt;
&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now I&amp;rsquo;ll use it a few more times.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2nd
&lt;!-- use with to check if word exists --&gt;
&lt;!-- If it exists, write abbr --&gt;
&lt;abbr title="laugh out loud"&gt;
LOL&lt;/abbr&gt;
&lt;!-- If not, write whole term, then indicate it was used by setting counter --&gt;
&lt;/li&gt;
&lt;li&gt;3rd
&lt;!-- use with to check if word exists --&gt;
&lt;!-- If it exists, write abbr --&gt;
&lt;abbr title="laugh out loud"&gt;
LOL&lt;/abbr&gt;
&lt;!-- If not, write whole term, then indicate it was used by setting counter --&gt;
&lt;/li&gt;
&lt;li&gt;4th
&lt;!-- use with to check if word exists --&gt;
&lt;!-- If it exists, write abbr --&gt;
&lt;abbr title="laugh out loud"&gt;
LOL&lt;/abbr&gt;
&lt;!-- If not, write whole term, then indicate it was used by setting counter --&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="discussion"&gt;Discussion&lt;/h2&gt;
&lt;p&gt;As I wrote this post, I learned much more about shortcodes in general than I did about inline shortcodes.&lt;/p&gt;
&lt;p&gt;Inline shortcodes are fun to write, but they can turn a page into a soupy, tangled mess (for example, the &lt;a href="https://gitlab.com/MattDodson/site/-/blob/main/content/posts/hugo-inline-shortcodes.md?plain=1"&gt;source for this page&lt;/a&gt; has started to get confusing to navigate).
Let me reiterate one more time: if you want to reuse a shortcode on another page, add it to your &lt;code&gt;layouts&lt;/code&gt;.
Besides that, some other drawbacks exist:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;They make the document more complicated to understand.&lt;/li&gt;
&lt;li&gt;They have some technical limitations. For example, you can&amp;rsquo;t nest them.&lt;/li&gt;
&lt;li&gt;They return values when declared, so you can&amp;rsquo;t write your functions in one part of the page and use them in another.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That said, I think inline shortcodes can be pretty handy, and I&amp;rsquo;ll probably start using them more.
I especially see using them to template repetitive pages and to prototype.&lt;/p&gt;
&lt;h2 id="read-more"&gt;Read more&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/gohugoio/hugo/issues/4011"&gt;Hugo issue #4011&lt;/a&gt;. This issue is where the inline shortcodes were proposed. Aside from historical interest, it has a good discussion about naming and design decisions.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/templates/shortcode-templates/"&gt;Hugo Shortcode docs&lt;/a&gt;. Canonical docs about all shortcodes, including inline ones.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/functions/"&gt;Hugo functions&lt;/a&gt;. All these are available in your inline shortcodes.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>How to use shortcode headings in the Hugo TOC</title><link>https://wellshapedwords.com/posts/shortcode-headings-in-toc/</link><pubDate>Thu, 20 Apr 2023 00:00:00 +0000</pubDate><guid>https://wellshapedwords.com/posts/shortcode-headings-in-toc/</guid><description>&lt;p&gt;When I write headings, I always consider how they&amp;rsquo;ll look in the auto-generated table of contents (TOC).
If the headings are descriptive,
the TOC creates a powerful navigation tool, providing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A high-level summary of the overall document&lt;/li&gt;
&lt;li&gt;A set of entry points to help readers find the sections that most relate to them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I wrote a long post, &lt;a href="https://wellshapedwords.com/essentials/practices/"&gt;Most common edits in technical documentation&lt;/a&gt;.
Because it was so long and regularly structured, I wrote a shortcode to template the content from different data files.
When my shortcode content didn&amp;rsquo;t appear in my TOC on this Hugo site, I was sad.&lt;/p&gt;
&lt;p&gt;But I accidentally found a solution: write the shortcode in markdown.&lt;/p&gt;
&lt;h2 id="solution-markdown-headings"&gt;Solution: markdown headings&lt;/h2&gt;
&lt;p&gt;In short:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use markdown headings in the shortcode (that&amp;rsquo;s &lt;code&gt;##&lt;/code&gt; instead of &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Delimit the shortcodes with the percent sign (that&amp;rsquo;s &lt;code&gt;%&lt;/code&gt; instead of &lt;code&gt;&amp;lt;&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Shortcodes with the &lt;code&gt;%&lt;/code&gt; delimiter render before the page.
I don&amp;rsquo;t know why the headings wouldn&amp;rsquo;t work with a percent delimiter and HTML
headings, but this is what I&amp;rsquo;ve found.
For a demonstration, read the next section.&lt;/p&gt;
&lt;h2 id="live-demonstration"&gt;Live demonstration&lt;/h2&gt;
&lt;aside aria-label="note" class="note"&gt;
&lt;div&gt;
&lt;svg
class="sign"
aria-hidden="true"
viewBox="0 0 41.667306 41.66729"
focusable="false"
&gt;
&lt;use xlink:href="#info"&gt;&lt;/use&gt;
&lt;/svg&gt;
This page uses the logic it demonstrates.
It was last built with Hugo v0.152.2.
&lt;/div&gt;
&lt;/aside&gt;
&lt;p&gt;I&amp;rsquo;ve written &lt;a href="https://gitlab.com/MattDodson/site/-/tree/main/layouts/shortcodes/toc-demo" title="link to shortcode directory"&gt;two shortcodes&lt;/a&gt; with level-three headings.
The only difference is heading style:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The &lt;code&gt;html-headings&lt;/code&gt; shortcode uses HTML.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;h3&amp;gt;HTML heading, {{ (.Get 0) }}-delimited shortcode&amp;lt;/h3&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;code&gt;markdown-headings&lt;/code&gt; shortcode uses markdown.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-markdown"&gt;### Markdown heading, {{ (.Get 0) }}-delimited shortcode
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now, I&amp;rsquo;ll put the following snippet directly in the file that you&amp;rsquo;re reading.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-markdown"&gt;
{{&amp;lt; toc-demo/html-headings &amp;quot;bracket&amp;quot; &amp;gt;}}
This heading doesn't appear in the TOC.
{{&amp;lt; toc-demo/markdown-headings &amp;quot;bracket&amp;quot; &amp;gt;}}
No heading is created because the angle bracket
causes the shortcode to be processed as HTML.
{{% toc-demo/html-headings &amp;quot;percent&amp;quot; %}}
This heading doesn't appear in the TOC.
{{% toc-demo/markdown-headings &amp;quot;percent&amp;quot; %}}
This heading _appears_ in TOC.
Scroll back up and check yourself!
![Drawing of a confused Jackie Chan](/images/jackie-what.jpg)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here it comes:&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;HTML heading, bracket-delimited shortcode&lt;/h3&gt;
&lt;p&gt;This heading doesn&amp;rsquo;t appear in the TOC.&lt;/p&gt;
### Markdown heading, bracket-delimited shortcode
&lt;p&gt;No heading is created because the angle bracket causes the shortcode to be processed as HTML.&lt;/p&gt;
&lt;h3&gt;HTML heading, percent-delimited shortcode&lt;/h3&gt;
&lt;p&gt;This heading doesn&amp;rsquo;t appear in the TOC.&lt;/p&gt;
&lt;h3 id="markdown-heading-percent-delimited-shortcode"&gt;Markdown heading, percent-delimited shortcode&lt;/h3&gt;
&lt;p&gt;This heading &lt;em&gt;appears&lt;/em&gt; in TOC. Scroll back up and check yourself!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://wellshapedwords.com/images/jackie-what.jpg" alt="Drawing of a confused Jackie Chan"&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="discussion"&gt;Discussion&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m not sure why only Markdown headings work.
As far as I can gather, before v0.55, no shortcode content could appear in a TOC, because the markdown parser didn&amp;rsquo;t process shortcodes in any way.
Somehow, though, my HTML headings still get skipped by the part of the application that generates the TOC.&lt;/p&gt;
&lt;p&gt;EDIT: with a bit of looking, I believe some better solutions might be to use &lt;code&gt;unsafe = true&lt;/code&gt; in the config, or perhaps, in more complex shortcodes, using &lt;code&gt;RawContent&lt;/code&gt;. If I investigate more, I&amp;rsquo;ll make a new post.&lt;/p&gt;
&lt;h2 id="related-links"&gt;Related links&lt;/h2&gt;
&lt;p&gt;This seems to be a common problem in Hugo.
Here are some relevant links and issues that I could find.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Hugo 0.55 release notes: &lt;a href="https://github.com/gohugoio/hugo/releases/tag/v0.55.0"&gt;Shortcodes revisted&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Pulls:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/gohugoio/hugo/pull/2623"&gt;2323&lt;/a&gt;: Handle TOC before handling shortcodes&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gohugoio/hugo/pull/5702"&gt;5702&lt;/a&gt;: Make page an interface&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/gohugoio/hugo/issues/6690"&gt;6690&lt;/a&gt;: Shortcode way of including md cause headings missed in TOC&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Community posts. &lt;em&gt;I think better solutions are here!&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://discourse.gohugo.io/t/applying-headings-in-shortcodes-to-the-table-of-contents/32132"&gt;Applying headings in shortcodes to TOC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discourse.gohugo.io/t/url-toc-issues-when-inserting-one-page-into-another/43077"&gt;URL/TOC issues&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>SEO basics are all you need: too much "optimization" worsens docs</title><link>https://wellshapedwords.com/posts/seo-the-docs/</link><pubDate>Mon, 07 Mar 2022 07:59:32 -0300</pubDate><guid>https://wellshapedwords.com/posts/seo-the-docs/</guid><description>&lt;p&gt;For a long time, I knew nothing about search-engine optimization.
I avoided learning about it.
I thought that SEO was something for marketers and other mercantile types,
not for technical-writing &amp;ldquo;purists&amp;rdquo; like me.&lt;/p&gt;
&lt;p&gt;Of course, this was a silly line of thinking:
technical writers should care about SEO.
Why wouldn&amp;rsquo;t I want to make my docs findable?
Everybody searches!&lt;/p&gt;
&lt;p&gt;Besides, how could I dismiss SEO if I didn&amp;rsquo;t know how it worked?
Now that I&amp;rsquo;ve learned a bit, I can say some SEO is a good thing, but too much will harm your docs.&lt;/p&gt;
&lt;h2 id="standard-disclaimer-im-ignorant"&gt;Standard disclaimer: I&amp;rsquo;m ignorant&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m far from an SEO expert.
I&amp;rsquo;ve read two books&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; and the &lt;a href="https://developers.google.com/search/docs/essentials"&gt;Google search essentials&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Besides, I believe that in the long run, &lt;em&gt;making any page &amp;ldquo;perfect&amp;rdquo; for search engines is a doomed strategy.&lt;/em&gt;
What is optimal today may be suboptimal tomorrow.
I explain this a bit more at the end of this article.&lt;/p&gt;
&lt;h2 id="the-good-parts"&gt;The good parts&lt;/h2&gt;
&lt;p&gt;It&amp;rsquo;s very important to support searching.
Even if you have no commercial interest, don&amp;rsquo;t you want people to find what you write?
Fortunately, the official recommendations
of SEO are perfectly harmonious with the guidelines of good doc writing.&lt;/p&gt;
&lt;h3 id="seo-essentials-are-doc-essentials"&gt;SEO essentials are doc essentials&lt;/h3&gt;
&lt;p&gt;The following guidelines are just good practices for writing:&lt;/p&gt;
&lt;div class="ticks"&gt;
&lt;ul&gt;
&lt;li&gt;Have descriptive headers.&lt;/li&gt;
&lt;li&gt;Make accessible pages.&lt;/li&gt;
&lt;li&gt;Break up your content and make it scannable.&lt;/li&gt;
&lt;li&gt;Use descriptive link text.&lt;/li&gt;
&lt;li&gt;Promote docs in other channels (e.g. on Twitter)&lt;/li&gt;
&lt;li&gt;Write good content that people want to link to.&lt;/li&gt;
&lt;li&gt;Write good content that people want to link to (I know I repeated myself).&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h3 id="metadata-is-important-too"&gt;Metadata is important too&lt;/h3&gt;
&lt;p&gt;Some of the more advanced concepts are good ideas for docs too.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You can reuse a good meta &lt;code&gt;description&lt;/code&gt; as a page summary, both for your site and social shares.&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;If you already use &lt;a href="https://en.wikipedia.org/wiki/Structured_writing"&gt;structured authoring&lt;/a&gt;,
using Schema.org and JSON-LD might help you classify your content.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;You can use the sitemap to create visualizations of your information flow.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-bad-parts-trying-to-hack-seo"&gt;The bad parts: trying to hack SEO&lt;/h2&gt;
&lt;p&gt;All documentation should be readable and findable.
A little SEO should help readers find your docs on search engines.
But if you blindly follow the latest recommendations, they&amp;rsquo;ll struggle to actually read the text.&lt;/p&gt;
&lt;p&gt;Here are some &lt;em&gt;2022 SEO best practices&lt;/em&gt;™ that create bad docs.&lt;/p&gt;
&lt;h3 id="dubious-intentionally-create-long-pages"&gt;Dubious: intentionally create long pages&lt;/h3&gt;
&lt;p&gt;In SEO jargon, &lt;em&gt;dwell time&lt;/em&gt; is how long a user stays on a site.&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;
The idea is that, if a user stays on a page for a long time, then that page is probably useful and interesting.
In practice, people try to increase dwell time by padding out content.&lt;/p&gt;
&lt;p&gt;Perhaps that makes sense for blog content, but &amp;ldquo;lengthy&amp;rdquo; is pretty much an anti-goal for documentation writers.
I don&amp;rsquo;t want readers to stay on my documentation site for a long time.
I want them to get in, find what they need, leave quickly, and get on with their lives.&lt;/p&gt;
&lt;p&gt;Fortunately, I don&amp;rsquo;t think many documentarians are trying to make arbitrarily long docs.
But many web-content writers are.
In fact, I would say that the goal of &amp;ldquo;dwell time&amp;rdquo; has created far more poor, bloated content than useful longform content.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use Google to search for a recipe of your favorite food.&lt;/li&gt;
&lt;li&gt;Click any result in the first page.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Is the page longer than a recipe in a book?
Are the actual ingredients and procedures buried two-thirds of the way down the page?&lt;/p&gt;
&lt;p&gt;If there&amp;rsquo;s a topic that&amp;rsquo;s worthy of more than a few short paragraphs, then the writer&amp;rsquo;s big challenge is figuring out what information to leave out, not how to cram more in.&lt;/p&gt;
&lt;h3 id="dubious-use-a-lot-of-synonyms"&gt;Dubious: use a lot of synonyms&lt;/h3&gt;
&lt;p&gt;Some SEO guidelines I&amp;rsquo;ve looked at say to sprinkle synonymous and related terms throughout your content to help the &lt;em&gt;latent semantic indices(LSIs)&lt;/em&gt;.
An LSI is essentially a thesaurus.
It helps search engines return results for &amp;ldquo;on-premises&amp;rdquo; when you search for &amp;ldquo;self-hosted&amp;rdquo;.&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Technical communication should use language that is precise and unambiguous.
Ideally, one term to match to one thing.
Terminology is a hard thing to nail down, and coherent documentation should use consistent names.&lt;/p&gt;
&lt;p&gt;However, it does make sense to incorporate some synonyms, precisely to support searching.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In introductory sentences that start like &amp;ldquo;In some places, this term is also called&amp;hellip;&amp;rdquo;&lt;/li&gt;
&lt;li&gt;In some kind of thesaurus or &lt;a href="https://www.oreilly.com/library/view/information-architecture-4th/9781491913529/ch10.html"&gt;other controlled vocabulary&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other places, a document that uses different terms for the same thing is just confusing.
In a world where &amp;ldquo;namespace&amp;rdquo; already means so many different things, why increase ambiguity?&lt;/p&gt;
&lt;h2 id="be-wary-of-for-profit-seo-advice"&gt;Be wary of for-profit SEO advice&lt;/h2&gt;
&lt;p&gt;SEO is an arms race.
Google tries to figure out what makes good content; marketers figure that out and exploit it; Google penalizes overuse of the metric and comes up with something else.
If you try to be &amp;ldquo;optimal,&amp;rdquo; you might end up with an unreadable site that has bad SEO anyway.
It doesn&amp;rsquo;t help that ninety-five percent of the SEO advice that you&amp;rsquo;ll find is either spam or a scam.&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://wellshapedwords.com/posts/seo-the-docs/spam-scam-continuum.svg" alt="A hand-drawn diagram"&gt;
&lt;figcaption&gt;Ninety-five percent of SEO content falls within the spam-scam continuum.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;A lot of these SEO &amp;ldquo;best practice&amp;rdquo; sites don&amp;rsquo;t even know what they are talking about.
If they did, they&amp;rsquo;d probably keep their knowledge hidden.
Even Google engineers use machine learning to support results,
which means there&amp;rsquo;s always some part of the rankings that is determined within a black box.&lt;/p&gt;
&lt;h2 id="good-seo-and-good-docs"&gt;Good SEO and Good docs&lt;/h2&gt;
&lt;p&gt;As I work on larger sites, I&amp;rsquo;ve come to appreciate how important it is to support searching.&lt;/p&gt;
&lt;p&gt;However, for your docs, don&amp;rsquo;t worry too much about SEO.
Instead, make useful, accessible content that people want to link to.&lt;/p&gt;
&lt;p&gt;For SEO, I also have a great long-term strategy.
Make useful, accessible content that people want to link to.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;&lt;em&gt;SEO in 2022&lt;/em&gt;, by Adam Clarke, and &lt;em&gt;Ultimate Guide to Link Building&lt;/em&gt;, by Garrett French and Eric Ward.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;&lt;a href="https://gitlab.com/MattDodson/site/-/commit/48fbcb3f414f937905d27f5b6a9ba704d48b2177"&gt;This site uses the description as a top-level summary&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;I&amp;rsquo;ve also seen it called &amp;ldquo;stay time&amp;rdquo; and &amp;ldquo;retention&amp;rdquo;.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;Google apparently doesn&amp;rsquo;t use LSI, but something more advanced &lt;a href="https://backlinko.com/hub/seo/lsi"&gt;https://backlinko.com/hub/seo/lsi&lt;/a&gt;&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>Unbundle that giant OpenAPI file!</title><link>https://wellshapedwords.com/posts/split-files-to-save-time/</link><pubDate>Thu, 03 Feb 2022 08:47:01 -0300</pubDate><guid>https://wellshapedwords.com/posts/split-files-to-save-time/</guid><description>&lt;h2 id="awesome-10000-lines-of-yaml-boilerplate"&gt;Awesome! 10,000 lines of YAML boilerplate&lt;/h2&gt;
&lt;p&gt;Have you ever said &amp;ldquo;I love reading 10k-line YAML files?&amp;rdquo;
If you have, I suspect you&amp;rsquo;re a robot.&lt;/p&gt;
&lt;p&gt;Because if you&amp;rsquo;re a human, there&amp;rsquo;s a good chance you don&amp;rsquo;t love reading 10k-line YAML files.
And if you&amp;rsquo;ve ever worked on an large OpenAPI spec,
there&amp;rsquo;s also a good chance you&amp;rsquo;ve gotten lost, at least once, in thousands of lines of YAML spaghetti.&lt;/p&gt;
&lt;p&gt;Fortunately, it doesn&amp;rsquo;t need to be that way.
Because many parsers support multi-file references, you can &lt;em&gt;unbundle&lt;/em&gt;
the YAML file into individual pieces.&lt;/p&gt;
&lt;p&gt;Even if the parser you use looks in only one file, it&amp;rsquo;s simple to take a multi-file definition and &lt;em&gt;bundle&lt;/em&gt; it into a single megafile, sure to please a hungry machine.&lt;/p&gt;
&lt;h2 id="unbundling-solves-multiple-documentation-issues"&gt;Unbundling solves multiple documentation issues&lt;/h2&gt;
&lt;p&gt;In a system with large or multiple REST APIs, there are some common documentation problems that come up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The organization might need separate public and private versions.&lt;/li&gt;
&lt;li&gt;The organization might want to present multiple APIs that have a large overlap in their underlying schema.&lt;/li&gt;
&lt;li&gt;If the definition is hand-written, the writer might be sad about working with a YAML mega-file.&lt;/li&gt;
&lt;li&gt;Maybe, if the writer uses the same hardware that I use, a mega-YAML file might even cause their text editor to lag and crash.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Fortunately, all these problems have one solution:&lt;/p&gt;
&lt;div class="ticks"&gt;
&lt;ul&gt;
&lt;li&gt;Split the definition up into smaller files&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="redoc-to-the-rescue"&gt;Redoc to the rescue&lt;/h2&gt;
&lt;p&gt;Redoc is pretty well-known for their API docs.
Besides their &lt;code&gt;redoc-cli&lt;/code&gt; tool, they also make &lt;a href="https://redoc.ly/openapi-cli/"&gt;&lt;code&gt;openapi&lt;/code&gt;&lt;/a&gt;,
a command-line utility that bundles, unbundles, previews, and lints docs.&lt;/p&gt;
&lt;p&gt;This demo uses &lt;code&gt;openapi&lt;/code&gt;. The topic focuses on the bundling and unbundling, but the other features are nice too.&lt;/p&gt;
&lt;aside aria-label="note" class="note"&gt;
&lt;div&gt;
&lt;svg class="sign" aria-hidden="true" viewBox="0 0 41.667306 41.66729" focusable="false"&gt;
&lt;use xlink:href="#info"&gt;&lt;/use&gt;
&lt;/svg&gt;
I&amp;rsquo;m using Redoc just because I think it&amp;rsquo;s a nice tool, and it&amp;rsquo;s fast.
I&amp;rsquo;m sure there are other good ways to unbundle.
&lt;/div&gt;
&lt;/aside&gt;
&lt;h2 id="tutorial-unbundle-a-large-yaml-file"&gt;Tutorial: Unbundle a large YAML file&lt;/h2&gt;
&lt;p&gt;In this demo, I&amp;rsquo;m going to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Inspect an &lt;code&gt;openapi.yaml&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;openapi&lt;/code&gt; CLI tool to unbundle it into small pieces.&lt;/li&gt;
&lt;li&gt;Separate the API into two top-level definitions.&lt;/li&gt;
&lt;li&gt;Rebundle the API as two distinct documents.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This demonstration should be reproducible.
If you can use the command line, feel free to follow along.
You&amp;rsquo;ll need to use &lt;code&gt;npm&lt;/code&gt; to install the &lt;code&gt;openapi&lt;/code&gt; tool (linked in the preceding section).&lt;/p&gt;
&lt;h3 id="find-a-large-file"&gt;Find a large file&lt;/h3&gt;
&lt;p&gt;First we need to find a heavy-duty API spec.
The &lt;a href="https://github.com/APIs-guru/openapi-directory"&gt;OpenAPI directory&lt;/a&gt; is a good place to look.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m going to choose an API from NASA.
At 373 lines, it&amp;rsquo;s a flyweight in the world of OpenAPI definitions.
But it&amp;rsquo;s enough for a demonstration.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Download your API file. You can do it with &lt;code&gt;cUrl&lt;/code&gt; like this:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="cmd"&gt;
&lt;code&gt;curl -o openapi.yaml https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/nasa.gov/asteroids%20neows/3.4.0/openapi.yaml&lt;/code&gt;
&lt;/pre&gt;
&lt;ol start="2"&gt;
&lt;li&gt;Inspect its contents. This API is quite slim. It has just a few endpoints:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/api&lt;/code&gt;. Calling this endpoint returns the OpenAPI specification (recursive!).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/projects&lt;/code&gt;. This lists all projects&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/projects/{id}&lt;/code&gt;. This returns information about a specific project.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let&amp;rsquo;s pretend this mega API file has two audiences:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tech writers, who are interested in only OpenAPI YAML files.&lt;/li&gt;
&lt;li&gt;Project managers, who are interested in only projects&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For this demonstration, I&amp;rsquo;m going to create two different API definitions.
This way we can have a special dedicated API for the tech writers,
and another one for the project managers.&lt;/p&gt;
&lt;h3 id="unbundle-the-file"&gt;Unbundle the file&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Make sure your file is in a clean directory.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class="language-txt"&gt;.
└── openapi.yaml
0 directories, 1 file
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start="2"&gt;
&lt;li&gt;Create a directory for the output. I call mine &lt;code&gt;unbundled&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="cmd"&gt;
&lt;code&gt;mkdir unbundled&lt;/code&gt;
&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-txt"&gt;.
├── openapi.yaml
└── unbundled
1 directory, 1 file
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Run the &lt;code&gt;openapi split&lt;/code&gt; command.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="cmd"&gt;
&lt;code&gt;openapi split openapi.yaml --outDir unbundled/&lt;/code&gt;
&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-txt"&gt;Document: openapi.yaml is successfully split
and all related files are saved to the directory: unbundled/
openapi.yaml: split processed in 134ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tool is fast, even on very big files.&lt;/p&gt;
&lt;ol start="4"&gt;
&lt;li&gt;Explore the new contents of the &lt;code&gt;bundled&lt;/code&gt; directory.
There are many more files now, one for each path, and one for each reusable schema.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;.
├── openapi.yaml # The original file
└── unbundled # the unbundled directory
├── components # The re-usable schema
│   └── schemas
│   ├── closeoutDocument.yaml
│   ├── coInvestigator.yaml
│   ├── destination.yaml
│   ├── file.yaml
│   ├── libraryItem.yaml
│   ├── organization.yaml
│   ├── principalInvestigator.yaml
│   ├── programDirector.yaml
│   ├── programManager.yaml
│   ├── projectManager.yaml
│   ├── project.yaml
│   ├── technologyArea.yaml
│   └── workLocation.yaml
├── openapi.yaml #the new, top-level definiton
└── paths #the paths
├── api@projects{.format}.yaml
├── api@projects@{id}{.format}.yaml
└── api.yaml
4 directories, 18 files
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="split-the-definition-into-two-apis"&gt;Split the definition into two APIs&lt;/h3&gt;
&lt;p&gt;We&amp;rsquo;re going to make two APIs, one for tech writers, and one for project managers.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Inspect the &lt;code&gt;unbundled/openapi.yaml&lt;/code&gt; file. It now has only 50 lines.
That&amp;rsquo;s because the schemas and endpoints are tucked away in their own files.
The &lt;code&gt;paths&lt;/code&gt; property now looks like this:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class="language-txt"&gt;paths:
/api:
$ref: paths/api.yaml
'/api/projects/{id}{.format}':
$ref: 'paths/api@projects@{id}{.format}.yaml'
'/api/projects{.format}':
$ref: 'paths/api@projects{.format}.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you are familiar with the &lt;code&gt;$ref&lt;/code&gt; keyword, this should look familiar—it&amp;rsquo;s
the same principle, but instead of referencing a position in the file, you&amp;rsquo;re now referencing a directory.&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;
&lt;p&gt;To make two API definitions, copy the top-level definition to another file. Rename both files, if you want.
&lt;pre class="cmd"&gt;
&lt;code&gt;cp unbundled/openapi.yaml unbundled/writersAPI.yaml&lt;/code&gt;
&lt;code&gt;mv unbundled/openapi.yaml unbundled/projectsAPI.yaml&lt;/code&gt;
&lt;/pre&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open &lt;code&gt;writersAPI.yaml&lt;/code&gt;. Delete the two &lt;code&gt;/projects&lt;/code&gt; paths, and their references.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open &lt;code&gt;projectsAPI.yaml&lt;/code&gt;. Delete the &lt;code&gt;/api&lt;/code&gt; path, and its reference.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That&amp;rsquo;s it! Now you have two separate API definitions, which share two underlying schema.
Furthermore, your specification is slim and DRY.
You won&amp;rsquo;t have to wrangle any YAML monsters,
but if different paths use the same schema, you can update multiple definitions by updating only one schema.&lt;/p&gt;
&lt;h3 id="rebundle-the-file"&gt;Rebundle the file&lt;/h3&gt;
&lt;p&gt;Some parsers can handle multi-line definitions.
Redoc is an example (I guess that&amp;rsquo;s not a surprise).&lt;/p&gt;
&lt;p&gt;Others can read only one file.
If that&amp;rsquo;s the case for you, you can still work off an unbundled file.
But you&amp;rsquo;ll just need to bundle it when you&amp;rsquo;re done.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;To keep the directory neat, make a directory for bundles.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="cmd"&gt;
&lt;code&gt;mkdir bundled&lt;/code&gt;
&lt;/pre&gt;
&lt;ol start="2"&gt;
&lt;li&gt;Use the &lt;code&gt;openapi bundle&lt;/code&gt; command to create new, single-file definitions from your multi-file definitions.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="cmd"&gt;
&lt;code&gt;openapi bundle unbundled/writersAPI.yaml -o bundled/BundledWritersAPI.yaml&lt;/code&gt;
&lt;code&gt;openapi bundle unbundled/projectsAPI.yaml -o bundled/BundledProjectsAPI.yaml&lt;/code&gt;
&lt;/pre&gt;
&lt;p&gt;Now you have all kinds of API definitions.
They don&amp;rsquo;t all contain the same information, but they are all made from the same source files.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-txt"&gt;.
├── bundled
│   ├── bundledProjectsAPI.yaml
│   └── bundledWritersAPI.yaml
├── openapi.yaml
└── unbundled
├── components
│   └── schemas
│   ├── closeoutDocument.yaml
│   ├── coInvestigator.yaml
│   ├── destination.yaml
│   ├── file.yaml
│   ├── libraryItem.yaml
│   ├── organization.yaml
│   ├── principalInvestigator.yaml
│   ├── programDirector.yaml
│   ├── programManager.yaml
│   ├── projectManager.yaml
│   ├── project.yaml
│   ├── technologyArea.yaml
│   └── workLocation.yaml
├── paths
│   ├── api@projects{.format}.yaml
│   ├── api@projects@{id}{.format}.yaml
│   └── api.yaml
├── projectsAPI.yaml
└── writersAPI.yaml
5 directories, 21 files
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="where-to-go-from-here"&gt;Where to go from here?&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve written a few OpenAPI definitions by hand.
At the beginning, it usually is easier to just write in one file.
At some point, though, that file is going to get unwieldy.
At that time, think about unbundling.&lt;/p&gt;
&lt;p&gt;Besides writer comfort, unbundled definitions are extremely handy for creating multiple API definitions from a single source.
For example, instead of &amp;ldquo;Writers&amp;rdquo; and &amp;ldquo;Projects,&amp;rdquo; you might have &amp;ldquo;Internal&amp;rdquo; and &amp;ldquo;External&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;You can also use a CI to automate the bundling process.
I had one client with public and private API documents.
Their public docs were built using a tool that didn&amp;rsquo;t support multi-file definitions.&lt;/p&gt;
&lt;p&gt;Using Github actions, I made a &amp;ldquo;script&amp;rdquo; that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Bundled the file in the internal repo&lt;/li&gt;
&lt;li&gt;Sent the bundled file to a public repo, where it was turned into documentation.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;More than a year later, I see the public docs are still getting automatically updated.
Nice!&lt;/p&gt;</description></item></channel></rss>