<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>taiga</title><link>https://justskiv.github.io/taiga/</link><description>the theme's demo site — every mechanism proven on itself</description><language>en</language><atom:link href="https://justskiv.github.io/taiga/index.xml" rel="self" type="application/rss+xml"/><item><title>Widget anatomy</title><link>https://justskiv.github.io/taiga/reference/widget-anatomy/</link><guid>https://justskiv.github.io/taiga/reference/widget-anatomy/</guid><pubDate>Sat, 04 Jul 2026 00:00:00 +0300</pubDate><description>A reference page bundle: the widgets/ folder laid out for both classes of widget — static and interactive — plus the _shared layer, to copy from.</description><content:encoded><![CDATA[<h2 id="classes"><a class="hash" href="#classes" aria-label="Section anchor">#</a>The two classes of widget</h2>
<p>A widget in this theme's <code>widgets/</code> folder is one of two things.</p>
<p><strong>Static</strong> — a <code>figure.html</code> and nothing else. It renders once, at build time,
and stays that way forever; there is no matching <code>widget.js</code> to hand off to.
That is the whole widget below:</p>
<figure class="widget">
<figcaption>Play <span class="wnote">— the widgets/ folder of this very bundle, drawn from itself</span></figcaption>
<div id="w-anatomy-static" class="w-root"><div class="mem">
  <div class="mem-lab">widgets/ <span class="tot">→ this page bundle's own widget folder</span></div>

  <div class="mem-lab">_shared/ <span class="tot">shared across every widget on this page</span></div>
  <div class="byte-strip">
    <div class="byte-seg"><div class="cells"><div class="byte-box f1" data-tip="lib.js · the guard, plus one helper (h) in scope for every widget.js below">js</div></div><div class="seg-tag">lib.js</div></div>
    <div class="byte-seg"><div class="cells"><div class="byte-box f1" data-tip="shared.css · .wa-tag — used by this figure and by w-anatomy-toggle below">css</div></div><div class="seg-tag">shared.css</div></div>
  </div>

  <div class="mem-lab">w-anatomy-static/ <span class="tot">this widget — no JS, ever</span></div>
  <div class="byte-strip">
    <div class="byte-seg"><div class="cells"><div class="byte-box f0" data-tip="figure.html · this very file · the whole widget, rendered once at build time">fig</div></div><div class="seg-tag">figure.html</div></div>
  </div>

  <div class="mem-lab">w-anatomy-toggle/ <span class="tot">the widget below — JS takes over once it loads</span></div>
  <div class="byte-strip">
    <div class="byte-seg"><div class="cells"><div class="byte-box f0" data-tip="figure.html · the no-JS fallback, shown until widget.js runs">fig</div></div><div class="seg-tag">figure.html</div></div>
    <div class="byte-seg"><div class="cells"><div class="byte-box f1" data-tip="widget.js · registers itself with Taiga.widget('w-anatomy-toggle', fn)">js</div></div><div class="seg-tag">widget.js</div></div>
    <div class="byte-seg"><div class="cells"><div class="byte-box f2" data-tip="widget.css · one class only this widget uses">css</div></div><div class="seg-tag">widget.css</div></div>
  </div>

  <span class="wa-tag">static · no JS</span>
</div>
</div>
</figure>

<p><strong>Interactive</strong> — a <code>widget.js</code> that registers itself with <code>Taiga.widget(id, fn)</code>,
usually paired with a <code>figure.html</code> that stands in until JS runs, and a
<code>widget.css</code> for anything the theme's own building-block classes
(<code>.w-row</code>/<code>.w-btn</code>/<code>.w-num</code>/<code>.w-cap</code>) don't already cover:</p>
<figure class="widget">
<figcaption>Play <span class="wnote">— click it: it names the file that&#39;s driving the mount right now</span></figcaption>
<div id="w-anatomy-toggle" class="w-root"><div class="w-row">
  <span class="wa-tag">figure.html</span>
</div>
<div class="w-cap">This is the static fallback. Turn on JS and click "toggle".</div>
</div>
</figure>

<p>Both are the same <code>{{&lt; widget id=&quot;…&quot; &gt;}}</code> shortcode with no inline body — the
id is all it takes for the shortcode to go looking for <code>widgets/&lt;id&gt;/figure.html</code>
as the page resource to fill the mount with. See &quot;<a href="/taiga/inside/anatomy/guide-is-a-bundle/" data-preview="/taiga/inside/anatomy/guide-is-a-bundle/">A guide is a folder</a>&quot;
for the other form, an inline fallback written straight into the shortcode.</p>
<h2 id="shared"><a class="hash" href="#shared" aria-label="Section anchor">#</a>The _shared layer</h2>
<p>Both widgets above load <code>widgets/_shared/lib.js</code> and <code>widgets/_shared/shared.css</code>
before their own files — the build sorts every <code>widgets/**.js</code> and <code>widgets/**.css</code>
of a bundle by name, and <code>_shared</code> sorts ahead of any <code>w-*</code> folder. Concretely,
in this bundle:</p>
<ul>
<li><code>_shared/lib.js</code> holds the one guard (<code>var W = window.Taiga; if (!W || …) return;</code>)
that every <code>widgets/&lt;id&gt;/widget.js</code> in the bundle relies on, plus a tiny <code>h()</code>
element builder that <code>w-anatomy-toggle/widget.js</code> calls without importing or
redeclaring it — they're concatenated into one script and share a scope.</li>
<li><code>_shared/shared.css</code> holds <code>.wa-tag</code>, the little pill both widgets above use to
label a filename. It earns its place in <code>_shared</code> for the least exotic reason
there is: two widgets on this page actually use it.</li>
</ul>
<p>Nothing here is shared out of necessity — a bundle with one widget could just as
well put the guard straight into <code>widgets/&lt;id&gt;/widget.js</code>. It's split out on
purpose, so this page has an <code>_shared/</code> to point at. A bundle where the split
carries real weight looks the same in shape, just heavier: a shared engine and
its spec table sit in <code>_shared/lib.js</code>, and every id that engine drives gets
its own <code>widgets/&lt;id&gt;/</code> folder for whatever is genuinely its own — a figure, a
<code>widget.css</code> override, sometimes nothing at all.</p>
<h2 id="source"><a class="hash" href="#source" aria-label="Section anchor">#</a>Reading the source</h2>
<p>The whole bundle, next to this file:</p>
<span class="code-label">reference/widget-anatomy/ — this page bundle</span>
<pre data-pagefind-weight="0.5"><code class="nohl">widget-anatomy/
  index.md, index.ru.md    ← this text, in both languages
  widgets/
    _shared/
      lib.js               ← guard &#43; the h() helper
      shared.css            ← .wa-tag
    w-anatomy-static/
      figure.html           ← the whole widget — no widget.js
    w-anatomy-toggle/
      figure.html           ← no-JS fallback
      widget.js             ← Taiga.widget(&#39;w-anatomy-toggle&#39;, …)
      widget.css             ← .wa-hl, only this widget&#39;s class</code></pre><p>Every file in <code>widgets/</code> is optional except the ones that give a widget its
substance: a static id needs <code>figure.html</code>, an interactive id needs <code>widget.js</code>,
and either can add a <code>widget.css</code>. A widget can also skip <code>figure.html</code> entirely
and start from an empty mount — <code>w-ks-counter</code> on the
&quot;<a href="/taiga/reference/kitchen-sink/" data-preview="/taiga/reference/kitchen-sink/">Kitchen sink</a>&quot; page does exactly that.</p>
]]></content:encoded></item><item><title>Hotkeys and panels</title><link>https://justskiv.github.io/taiga/reference/hotkeys/</link><guid>https://justskiv.github.io/taiga/reference/hotkeys/</guid><pubDate>Sat, 27 Jun 2026 00:00:00 +0300</pubDate><description>Reference for reader controls: search and panel keys, focus mode, the View popover, and what's remembered locally.</description><content:encoded><![CDATA[<h2 id="keys"><a class="hash" href="#keys" aria-label="Section anchor">#</a>Keys</h2>
<table>
  <thead>
      <tr>
          <th>keys</th>
          <th>action</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><kbd class="kb">⌘K</kbd> or <kbd class="kb">/</kbd></td>
          <td>search guides</td>
      </tr>
      <tr>
          <td><kbd class="kb">[</kbd> or <kbd class="kb">⌘1</kbd></td>
          <td>show/hide the series panel (left)</td>
      </tr>
      <tr>
          <td><kbd class="kb">]</kbd> or <kbd class="kb">⌘2</kbd></td>
          <td>show/hide the table of contents (right)</td>
      </tr>
      <tr>
          <td><kbd class="kb">F</kbd></td>
          <td>focus mode: hide everything except the article text</td>
      </tr>
      <tr>
          <td><kbd class="kb">Esc</kbd></td>
          <td>close search or the popover</td>
      </tr>
  </tbody>
</table>
<p>Keys are bound to physical key codes, not characters — they work in any layout, including Russian. They don't fire inside input fields; focus mode only applies on article pages.</p>
<h2 id="panels"><a class="hash" href="#panels" aria-label="Section anchor">#</a>Panels and minimaps</h2>
<p>A hidden panel doesn't disappear completely — it collapses into a minimap at the edge of the screen: dots and dashes mirror its content. Hovering expands the panel over the text, and you can pin it back from there. Read parts of a series get marks — so you can see where you've already been.</p>
<h2 id="view-popover"><a class="hash" href="#view-popover" aria-label="Section anchor">#</a>The &quot;View&quot; popover</h2>
<p>The icon in the top-right corner opens the reading settings: palettes with swatches, toggling the side panels on and off, the list layout variant on rubric pages. The header has no separate setting: it always slides away on scroll down and comes back on scroll up.</p>
<h2 id="storage"><a class="hash" href="#storage" aria-label="Section anchor">#</a>What gets remembered</h2>
<p>All settings live locally in the browser — no accounts, no network:</p>
<table>
  <thead>
      <tr>
          <th>setting</th>
          <th>localStorage key</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>palette</td>
          <td><code>taiga.theme</code></td>
      </tr>
      <tr>
          <td>left/right panels</td>
          <td><code>taiga.railL</code>, <code>taiga.railR</code></td>
      </tr>
      <tr>
          <td>rubric list variant</td>
          <td><code>taiga.rubvar</code></td>
      </tr>
      <tr>
          <td>read guides</td>
          <td><code>taiga.visited</code></td>
      </tr>
  </tbody>
</table>
<p>Clearing the browser's site data resets everything to defaults — that's the standard way to &quot;reset settings.&quot;</p>
]]></content:encoded></item><item><title>Make it yours</title><link>https://justskiv.github.io/taiga/howto/handbook/make-it-yours/</link><guid>https://justskiv.github.io/taiga/howto/handbook/make-it-yours/</guid><pubDate>Sat, 20 Jun 2026 00:00:00 +0300</pubDate><description>Customization without a fork: a palette in one toml file, custom.css, hook partials, a section of your own and a second language.</description><content:encoded><![CDATA[<h2 id="own-palette"><a class="hash" href="#own-palette" aria-label="Section anchor">#</a>A palette is one file</h2>
<p>The signature color of a site is a toml file in <code>data/themes/</code> <strong>on the site side</strong>:</p>
<span class="code-label">data/themes/mine.toml (fragment)</span>
<pre data-pagefind-weight="0.5"><code class="nohl">name = &#34;Mine&#34;     # the name in the picker
weight = 80       # position in the picker&#39;s list

&#34;bg-deep&#34; = &#34;#101418&#34;
&#34;bg-surface&#34; = &#34;#1a2027&#34;
&#34;text-primary&#34; = &#34;#d8dee6&#34;</code></pre><p>Hugo merges the data of the theme and of the site (on a name clash the site wins), so the file finds its own way both into the CSS and into the palette picker in the header. Don't need one of the palettes the theme ships with — override its file with <code>disabled = true</code>, and it disappears everywhere. How CSS blocks and swatches come out of the toml is taken apart in &quot;<a href="/taiga/inside/anatomy/palette-is-a-file/" data-preview="/taiga/inside/anatomy/palette-is-a-file/">A palette is one file</a>&quot;.</p>
<h2 id="css-hooks"><a class="hash" href="#css-hooks" aria-label="Section anchor">#</a>custom.css and hooks</h2>
<p>Pointed edits on top of the palette go into the site's <code>assets/css/custom.css</code>: if the file exists, the theme appends it last to the CSS bundle, so your rules win without <code>!important</code>. The design tokens are a public API: the names of the CSS variables are stable and are listed in the theme's <code>docs/customizing.md</code>.</p>
<p>For analytics, meta tags of your own and third-party scripts there are hook partials: <code>hooks/head-extra.html</code> and <code>hooks/foot-extra.html</code> are empty in the theme, the site puts files of the same names into its <code>layouts/_partials/</code> — and their contents land in the head and at the end of the body. And if the render itself needs changing — any partial of the theme is overridden by a site file of the same name: Hugo's lookup order always prefers the site's files. The small named partials — the logo, the footer, the feed card — exist for exactly this kind of pointed override.</p>
<h2 id="own-section"><a class="hash" href="#own-section" aria-label="Section anchor">#</a>A section of your own</h2>
<p>You need a section the theme does not have — a glossary, a catalog, a playground with a drill. It needs neither a new layout nor edits to the theme: it is an ordinary page or bundle, and the interactivity is the same <code>widgets.js</code> next to the text — the mechanism works for any bundle, not only for guides. A menu item is added to <code>[[menus.main]]</code> in the config; the item's <code>params.badge = &quot;β&quot;</code> draws a badge — that is a generic feature of the navigation, not the mark of a special section.</p>
<p>The &quot;<a href="/taiga/playground/">Playground</a>&quot; on this demo is built exactly that way: in the theme there is not a trace of it, the whole section is three files on the site side.</p>
<h2 id="own-strings"><a class="hash" href="#own-strings" aria-label="Section anchor">#</a>Strings of your own</h2>
<p>The texts of the interface are not hardcoded in the templates. The server-side strings live in <code>i18n/*.toml</code>: the site puts down its own file with a partial set of keys, and it merges with the theme's file — you override only what you need. Strings that live in JS — the &quot;View&quot; popover, the hints of the minimap — are overridden with the <code>window.THEME_I18N</code> object; the list of keys is in <code>docs/customizing.md</code>.</p>
<h2 id="second-language"><a class="hash" href="#second-language" aria-label="Section anchor">#</a>A second language</h2>
<p>Adding a language is an additive step, not a migration. A <code>[languages.ru]</code> block is written into the config (plus <code>[languages.ru.params]</code> for the hero line and <code>[languages.ru.menus]</code> for the menu). The translations come as file suffixes: <code>index.ru.md</code> next to <code>index.md</code>, including the <code>_index</code> of the rubrics and the series; Hugo pairs the versions itself. A language switcher appears in the header when the site has more than one language, and Pagefind builds a separate index per language, by the page's <code>lang</code>. Template edits: zero. The step-by-step checklist is in the theme's <code>docs/i18n.md</code>.</p>
<p>This page is the proof: you are reading the English version, and the Russian one lies next to it as <code>index.ru.md</code>.</p>
<div class="callout key">
<span class="clabel">Key idea</span>
<p>The invariant of this whole part: customization is the files of your site. Not one edit lands in the theme's directory, so updating the theme is a <code>git pull</code> or a bump of the module version, with no hand-merging of your own changes.</p>

</div>

<p><em>You can now take your site as far from this demo as you like — without forking the theme.</em></p>
<h2 id="what-next"><a class="hash" href="#what-next" aria-label="Section anchor">#</a>What's next</h2>
<p>The handbook ends here: the theme is installed, guides are being written, the site looks like its own. If you are curious why it is built this way, &quot;Inside&quot; has the &quot;Anatomy of the theme&quot; series: the leaf bundle, the arithmetic of series, palettes-as-data. And every component at once is laid out on the kitchen-sink page in &quot;Reference&quot;.</p>
]]></content:encoded></item><item><title>Write your first guide</title><link>https://justskiv.github.io/taiga/howto/handbook/first-guide/</link><guid>https://justskiv.github.io/taiga/howto/handbook/first-guide/</guid><pubDate>Sat, 13 Jun 2026 00:00:00 +0300</pubDate><description>A guide scaffold in one command, the front matter canon, shortcodes, captions for code and a live widget in two touches.</description><content:encoded><![CDATA[<h2 id="scaffold"><a class="hash" href="#scaffold" aria-label="Section anchor">#</a>A scaffold in one command</h2>
<span class="code-label">scaffold a guide</span>
<div data-pagefind-weight="0.5"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hugo new content guides/my-first --kind guides</span></span></code></pre></div></div><p>The <code>guides</code> archetype creates a leaf bundle: an <code>index.md</code> with the front matter skeleton filled in, and a starter <code>widgets.js</code> beside it. Files that lie in the guide's folder are picked up by the theme itself — nothing to wire up.</p>
<div class="callout trap">
<span class="clabel">Trap</span>
<p>The path in the command carries no <code>/index.md</code>. Write <code>guides/my-first/index.md</code> and Hugo switches into single-file mode and silently declines to copy the archetype's sibling files. A folder with no <code>widgets.js</code> is the first sign of this mistake.</p>

</div>

<h2 id="front-matter"><a class="hash" href="#front-matter" aria-label="Section anchor">#</a>Front matter: what to fill in</h2>
<span class="code-label">index.md — front matter</span>
<div data-pagefind-weight="0.5"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;My first guide&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;One sentence — for the feed, for search and for meta.&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">lead</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;The lead paragraph under the title; it may run longer than description.&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">tags</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">example, setup]</span><span class="w">
</span></span></span><span class="line hl"><span class="cl"><span class="nt">mins</span><span class="p">:</span><span class="w"> </span><span class="m">4</span><span class="w">
</span></span></span><span class="line hl"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;v0.0.1&#34;</span></span></span></code></pre></div></div><p>The two highlighted fields have non-obvious rules. <code>mins</code> is a handwritten, honest estimate of reading time: it lands in the article's chips and in the arithmetic of the series, which is why it is not trusted to an automatic count (omit the field and the theme will count for you). <code>version</code> is an arbitrary string for the version chip: the version of a language, of a DBMS, of your product — the theme renders it as-is. The archetype fills in <code>date</code> and <code>slug</code> too; the date lives only in the feed and the RSS — it appears nowhere in the text of the article.</p>
<p>A guide becomes a series part by where it lives: its folder sits inside the series folder (<code>guides/track/my-part/</code>), and <code>weight</code> sets its order among the parts; <code>rail_title</code> gives the panel its short name. How &quot;part N of M&quot; comes out of that is taken apart in &quot;<a href="/taiga/inside/anatomy/series-knows-itself/" data-preview="/taiga/inside/anatomy/series-knows-itself/">A series knows itself</a>&quot;.</p>
<p>Mark up headings with explicit anchors: <code>## Section {#section-id}</code>, in Latin letters — then links and the table of contents do not depend on how the heading text transliterates. That matters the day the page gets a translation.</p>
<h2 id="shortcodes"><a class="hash" href="#shortcodes" aria-label="Section anchor">#</a>Shortcodes</h2>
<p>Four of them, and that is enough for most guides:</p>
<ul>
<li><code>callout</code> — an inset; types <code>key</code>, <code>trap</code>, <code>internals</code>, <code>note</code>;</li>
<li><code>widget</code> — the mount point of a live illustration (below);</li>
<li><code>raw</code> — a multi-line HTML fragment as-is, for diagrams;</li>
<li><code>bigcard</code> — a large card link to a neighboring piece.</li>
</ul>
<span class="code-label">an inset in the text of a guide</span>
<pre data-pagefind-weight="0.5"><code class="nohl">{{&lt; callout type=&#34;trap&#34; label=&#34;Trap&#34; &gt;}}
Inside it — plain markdown.
{{&lt; /callout &gt;}}</code></pre><p>Code blocks take attributes right in the fence: <code>```toml {label=&quot;config.toml (fragment)&quot;, hl_lines=[2]}</code> — <code>label</code> draws a caption above the block, <code>hl_lines</code> highlights lines. You have met both already in this article: the caption on every block and the highlighting in the front matter example are done exactly this way.</p>
<h2 id="widget-two-touches"><a class="hash" href="#widget-two-touches" aria-label="Section anchor">#</a>A widget in two touches</h2>
<p>A live illustration is added without editing the theme. Touch one — the shortcode in the text; inside it goes a static fallback, which is what readers without JS are left with:</p>
<span class="code-label">touch 1 — the shortcode in the text</span>
<pre data-pagefind-weight="0.5"><code class="nohl">{{&lt; widget id=&#34;w-demo&#34; note=&#34;— a task for the reader&#34; &gt;}}
&lt;p&gt;A static fallback for readers without JS.&lt;/p&gt;
{{&lt; /widget &gt;}}</code></pre><p>Touch two — an initializer with the same id in the <code>widgets.js</code> next to <code>index.md</code>:</p>
<span class="code-label">touch 2 — widgets.js next to index.md</span>
<div data-pagefind-weight="0.5"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">Taiga</span><span class="p">.</span><span class="nx">widget</span><span class="p">(</span><span class="s2">&#34;w-demo&#34;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">root</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">root</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="s2">&#34;&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// you build the widget out of the theme&#39;s building-block classes:
</span></span></span><span class="line"><span class="cl">  <span class="c1">// .w-row, .w-btn, .w-num, .w-cap — no styles of your own needed
</span></span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div><p>The theme's runtime waits for the DOM, finds the mount point and calls the callback inside a <code>try/catch</code>: a widget that falls over writes to the console and does not bring the page down with it. The one thing to watch is that the id in the shortcode and the id in <code>Taiga.widget(…)</code> match letter for letter.</p>
<p>Here is the working proof — an estimate of an honest <code>mins</code> for a guide not yet written:</p>
<figure class="widget">
<figcaption>Play <span class="wnote">— stack up the sections of your future guide and see which mins would be honest</span></figcaption>
<div id="w-mins" class="w-root">
<div class="w-cap">A mins estimate from the number of sections. Right now this is the static fallback: turn JS on and we'll count.</div>
</div>
</figure>

<p>It is built exactly as in the examples above: the shortcode here, <code>Taiga.widget(&quot;w-mins&quot;, …)</code> in the <code>widgets.js</code> in this article's folder.</p>
<p><em>You can now write a guide with a live illustration without opening a single file of the theme.</em></p>
<h2 id="what-next"><a class="hash" href="#what-next" aria-label="Section anchor">#</a>What's next</h2>
<p>The guide exists, the site builds — but it still looks like this demo, not like your project. The third part is about a palette in one file, <code>custom.css</code>, sections of your own and a second language.</p>
]]></content:encoded></item><item><title>Install and run</title><link>https://justskiv.github.io/taiga/howto/handbook/get-started/</link><guid>https://justskiv.github.io/taiga/howto/handbook/get-started/</guid><pubDate>Sat, 06 Jun 2026 00:00:00 +0300</pubDate><description>What the theme does, three ways to install it, the minimal config and the content layout — from an empty folder to a built site.</description><content:encoded><![CDATA[<h2 id="what-it-is"><a class="hash" href="#what-it-is" aria-label="Section anchor">#</a>What this theme is</h2>
<p>A theme for learning guides that are read in order, not at random. Content sits on three levels: rubrics → series → guides. A series is a folder inside its rubric — membership is where the guide lies; everything else — &quot;part N of M&quot; in the kicker, the panel with the table of contents on the left, the bridge with &quot;~N min left&quot; at the bottom — is counted at build time. A guide is a folder: its interactive widgets, styles and cover lie next to the text, and it moves as a whole, one folder. Everything you see on this demo — the feed, search, palettes, cover generation — is a stock mechanism of the theme: the demo is built on nothing but it.</p>
<p>Two binaries, and that is the whole toolchain. The first is plain <code>hugo</code>, not extended: there is no Sass in the theme, and esbuild for JS is built into the standard binary. The second is <code>pagefind</code>, full-text search over the built site. Node, npm and <code>node_modules</code> take part in neither the build nor development. The minimum Hugo version is declared in the theme's config: a binary that is too old stops the build with a clear error rather than with strange behavior.</p>
<h2 id="three-ways"><a class="hash" href="#three-ways" aria-label="Section anchor">#</a>Three ways to install</h2>
<p>The paths below are placeholders: substitute the address of the theme's repository.</p>
<p><strong>Hugo Module</strong> — the main way. Into the site config:</p>
<span class="code-label">hugo.toml — the theme as a module</span>
<pre data-pagefind-weight="0.5"><code class="nohl">[module]
  [[module.imports]]
    path = &#34;github.com/user/theme&#34;</code></pre><p>Then <code>hugo mod get -u</code> (if the site is not a module yet — <code>hugo mod init github.com/user/site</code>, once). Versions resolve against the theme's semver tags; to edit the theme locally, use a <code>replace</code> in the site's <code>go.mod</code>.</p>
<p><strong>Git submodule</strong> — if you would rather not have modules:</p>
<span class="code-label">the theme as a submodule</span>
<div data-pagefind-weight="0.5"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git submodule add https://github.com/user/theme themes/theme</span></span></code></pre></div></div><p>plus <code>theme = &quot;theme&quot;</code> in the config. In CI, do not forget to check out with <code>submodules: recursive</code>.</p>
<p><strong>A copy in <code>themes/</code></strong> — a clone or an unpacked archive in <code>themes/theme</code>, plus the same <code>theme = &quot;theme&quot;</code>. Updates are manual; on the other hand this is the way to look at the theme with no commitment.</p>
<div class="callout key">
<span class="clabel">Key idea</span>
<p>All three ways are equal: the theme keeps its assets in the standard directories and does not require building itself. The only difference is how updates arrive.</p>

</div>

<h2 id="minimal-config"><a class="hash" href="#minimal-config" aria-label="Section anchor">#</a>The minimal config</h2>
<span class="code-label">hugo.toml</span>
<pre data-pagefind-weight="0.5"><code class="nohl">baseURL = &#34;https://example.org/&#34;
title = &#34;My handbook&#34;
theme = &#34;theme&#34;                # not needed when installed as a module
defaultContentLanguage = &#34;en&#34;

[languages.en]
  languageCode = &#34;en&#34;
  languageName = &#34;English&#34;
  weight = 1

[taxonomies]
  tag = &#34;tags&#34;

[[menus.main]]
  name = &#34;Guides&#34;
  url = &#34;/guides/&#34;
  weight = 10</code></pre><p>The <code>[languages.en]</code> block looks redundant while there is one language, but it makes adding a second one an additive step rather than a config migration. The rest of the behavior is tuned through <code>[params]</code> — the home hero line, how many feed posts are visible, the &quot;guide of the week&quot;, the default palette — and everything has a default: the site builds without a single param. The full registry is in the theme's <code>docs/params.md</code>.</p>
<h2 id="content-layout"><a class="hash" href="#content-layout" aria-label="Section anchor">#</a>Content layout</h2>
<span class="code-label">content/</span>
<pre data-pagefind-weight="0.5"><code class="nohl">content/
├── _index.md              # home: the hero line and data
└── guides/
    ├── _index.md          # rubric: label, lead
    ├── my-first/
    │   └── index.md       # a standalone guide = a folder (leaf bundle)
    └── track/             # a series = a sub-folder of the rubric
        ├── _index.md      # series metadata: title, description
        └── part-one/
            └── index.md   # a part — the same guide folder, one level deeper</code></pre><p>The section names are yours: the theme deals in &quot;sections&quot;, &quot;series&quot; and &quot;guides&quot; and hardcodes neither rubric names nor subject words. A handbook about databases and a handbook about watercolor lay out the same way.</p>
<h2 id="build"><a class="hash" href="#build" aria-label="Section anchor">#</a>Build</h2>
<span class="code-label">development and production</span>
<div data-pagefind-weight="0.5"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hugo server                        <span class="c1"># development</span>
</span></span><span class="line"><span class="cl">hugo <span class="o">&amp;&amp;</span> pagefind --site public     <span class="c1"># production: site + search index</span></span></span></code></pre></div></div><p>There are two commands because pagefind indexes a site that is already built. Search does not break in dev without an index: the modal quietly tells you which command builds it.</p>
<p><em>You can now stand up a site on the theme from scratch and build it in one pass — all that is left is to fill it.</em></p>
<h2 id="what-next"><a class="hash" href="#what-next" aria-label="Section anchor">#</a>What's next</h2>
<p>An empty site builds, but there is nothing to show yet. The next part is about the first guide: a scaffold in one command, the front matter canon and a live widget in two touches.</p>
]]></content:encoded></item><item><title>Kitchen sink: every component</title><link>https://justskiv.github.io/taiga/reference/kitchen-sink/</link><guid>https://justskiv.github.io/taiga/reference/kitchen-sink/</guid><pubDate>Sat, 30 May 2026 00:00:00 +0300</pubDate><description>A regression testbed: every shortcode, callout, code block, diagram and widget on one page.</description><content:encoded><![CDATA[<h2 id="headings"><a class="hash" href="#headings" aria-label="Section anchor">#</a>Headings and anchors</h2>
<p>Level two — with the <code>#</code> character, level three — with <code>##</code>. Inline: <em>italic</em>, <strong>bold</strong>,
<code>code</code>, an internal link to the <a href="/taiga/inside/anatomy/guide-is-a-bundle/" data-preview="/taiga/inside/anatomy/guide-is-a-bundle/">reference guide</a>.</p>
<h3 id="sub"><a class="hash" href="#sub" aria-label="Subsection anchor">##</a>Third-level subheading</h3>
<p>Checks that render-heading attaches <code>##</code> and an anchor to h3.</p>
<h2 id="code"><a class="hash" href="#code" aria-label="Section anchor">#</a>Code</h2>
<p>Go is highlighted server-side (Chroma in the palette's colors):</p>
<div data-pagefind-weight="0.5"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span><span class="w"> </span><span class="nx">Point</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">X</span><span class="p">,</span><span class="w"> </span><span class="nx">Y</span><span class="w"> </span><span class="kt">int64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">dist</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span><span class="w"> </span><span class="nx">b</span><span class="w"> </span><span class="nx">Point</span><span class="p">)</span><span class="w"> </span><span class="kt">int64</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="p">(</span><span class="nx">a</span><span class="p">.</span><span class="nx">X</span><span class="o">-</span><span class="nx">b</span><span class="p">.</span><span class="nx">X</span><span class="p">)</span><span class="o">*</span><span class="p">(</span><span class="nx">a</span><span class="p">.</span><span class="nx">X</span><span class="o">-</span><span class="nx">b</span><span class="p">.</span><span class="nx">X</span><span class="p">)</span><span class="w"> </span><span class="p">}</span></span></span></code></pre></div></div><p>With a file caption and line highlighting (<code>{label=… hl_lines=[2]}</code>):</p>
<span class="code-label">runtime/malloc.go (simplified)</span>
<div data-pagefind-weight="0.5"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">mallocgc</span><span class="p">(</span><span class="nx">size</span><span class="w"> </span><span class="kt">uintptr</span><span class="p">)</span><span class="w"> </span><span class="nx">unsafe</span><span class="p">.</span><span class="nx">Pointer</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line hl"><span class="cl"><span class="w">	</span><span class="nx">c</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">getMCache</span><span class="p">()</span><span class="w">          </span><span class="c1">// hot path — no locks</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">c</span><span class="p">.</span><span class="nf">alloc</span><span class="p">(</span><span class="nx">size</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><p>Other lit languages wear the same roles — blue for a structural name, copper
for a number, one tone for strings, muted italic for a comment:</p>
<div data-pagefind-weight="0.5"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;items&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="nt">&#34;price&#34;</span><span class="p">:</span> <span class="mi">123</span><span class="p">,</span> <span class="nt">&#34;shipped&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="nt">&#34;price&#34;</span><span class="p">:</span> <span class="mi">101</span><span class="p">}</span>  <span class="c1">// a trailing comment is fine
</span></span></span><span class="line"><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div data-pagefind-weight="0.5"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">GODEBUG</span><span class="o">=</span><span class="nv">schedtrace</span><span class="o">=</span><span class="m">1000</span>
</span></span><span class="line"><span class="cl">go build ./... <span class="o">&amp;&amp;</span> ./app --addr <span class="s2">&#34;</span><span class="nv">$HOST</span><span class="s2">:8080&#34;</span>   <span class="c1"># both, or nothing</span></span></span></code></pre></div></div><p>A bare fence, <code>text</code> or <code>txt</code> is the plain form — flat <code>.nohl</code>, which is what a
transcript wants:</p>
<pre data-pagefind-weight="0.5"><code class="nohl">$ go build ./...
ok  taiga/internals  0.312s</code></pre><h2 id="table"><a class="hash" href="#table" aria-label="Section anchor">#</a>Table</h2>
<p>A Markdown table; render-table attaches the <code>.tbl-wrap</code> wrapper:</p>
<table>
  <thead>
      <tr>
          <th>Type</th>
          <th style="text-align: right">Size</th>
          <th style="text-align: center">Alignment</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>bool</code></td>
          <td style="text-align: right">1</td>
          <td style="text-align: center">1</td>
      </tr>
      <tr>
          <td><code>int64</code></td>
          <td style="text-align: right">8</td>
          <td style="text-align: center">8</td>
      </tr>
      <tr>
          <td><code>[]T</code></td>
          <td style="text-align: right">24</td>
          <td style="text-align: center">8</td>
      </tr>
  </tbody>
</table>
<h2 id="callouts"><a class="hash" href="#callouts" aria-label="Section anchor">#</a>Callouts</h2>
<div class="callout key">
<span class="clabel">Key idea</span>
<p>The main idea with the default label. Inside — <strong>markdown</strong> and <code>code</code>.</p>

</div>

<div class="callout trap">
<span class="clabel">Trap</span>
<p>A trap with the default label.</p>

</div>

<div class="callout internals">
<span class="clabel">Under the hood: a custom label</span>
<p>An &quot;under the hood&quot; callout with an overridden label.</p>

</div>

<div class="callout note">
<span class="clabel">Historical note</span>
<p>A historical footnote — the quietest type.</p>

</div>

<div class="callout warn">
<span class="clabel">Caution</span>
<p>A caution — the loudest type, in red.</p>

</div>

<h2 id="fold"><a class="hash" href="#fold" aria-label="Section anchor">#</a>Fold</h2>
<p>A collapsible inline note — icon, summary, and a toggle; the panel slides open
by height. <code>title=</code> is inline Markdown, the body is Markdown.</p>
<details class="fold" data-size="note">
<summary><svg class="fold-ic" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="3"/><path d="M10 8.4v7.2l6-3.6z" fill="currentColor" stroke="none"/></svg><span class="fold-title">There is also a <a href="https://youtu.be/dQw4w9WgXcQ" target="_blank" rel="noopener" class="lp-nb" data-link="yt" data-yt="https://youtu.be/dQw4w9WgXcQ"><span class="lp-w">video </span>version</a> of this guide.</span>
<span class="fold-toggle"><span class="fold-more">Подробнее</span><span class="fold-less">Свернуть</span><svg class="fold-chev" width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 6l4.5 4.5L12.5 6"/></svg></span>
</summary>
<div class="fold-body">
<button class="fold-rail" type="button" tabindex="-1" aria-hidden="true"></button>
<p>Watch first, then read to cement it — some things land better on screen,
others in text. The link above opens the video <strong>without</strong> toggling the panel.</p>


</div>
</details>
<details class="fold" data-size="note">
<summary><svg class="fold-ic" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 18h6"/><path d="M10 21h4"/><path d="M12 3a6 6 0 0 0-4 10.5c.7.7 1 1.2 1 2.5h6c0-1.3.3-1.8 1-2.5A6 6 0 0 0 12 3z"/></svg><span class="fold-title">A simplification made on purpose.</span>
<span class="fold-toggle"><span class="fold-more">Подробнее</span><span class="fold-less">Свернуть</span><svg class="fold-chev" width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 6l4.5 4.5L12.5 6"/></svg></span>
</summary>
<div class="fold-body">
<button class="fold-rail" type="button" tabindex="-1" aria-hidden="true"></button>
<p>The real thing is a little more involved, but the simpler model is right in
the essentials — the details come later.</p>


</div>
</details>
<details class="fold" data-size="note" open>
<summary><svg class="fold-ic" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m16 18 4-4-4-4"/><path d="m8 6-4 4 4 4"/><path d="m13 5-2 14"/></svg><span class="fold-title">A full listing, rendered already open.</span>
<span class="fold-toggle"><span class="fold-more">Show code</span><span class="fold-less">Hide</span><svg class="fold-chev" width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 6l4.5 4.5L12.5 6"/></svg></span>
</summary>
<div class="fold-body">
<button class="fold-rail" type="button" tabindex="-1" aria-hidden="true"></button>
<div data-pagefind-weight="0.5"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">main</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nb">println</span><span class="p">(</span><span class="s">&#34;hello&#34;</span><span class="p">)</span><span class="w"> </span><span class="p">}</span></span></span></code></pre></div></div>

</div>
</details>
<p>Past roughly half a screen of prose the fold switches register on its own: the
summary is set at reading size, the block takes its own air, and the panel ends
with an explicit toggle so a long aside can be closed from where the reading
stopped. <code>size=</code> forces either register — here it is used to show the section
one without pasting a wall of text.</p>
<details class="fold" data-size="section">
<summary><svg class="fold-ic" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 7v13"/><path d="M3 5.5A1.5 1.5 0 0 1 4.5 4H9a3 3 0 0 1 3 3 3 3 0 0 1 3-3h4.5A1.5 1.5 0 0 1 21 5.5v11a1.5 1.5 0 0 1-1.5 1.5H14a2 2 0 0 0-2 2 2 2 0 0 0-2-2H4.5A1.5 1.5 0 0 1 3 16.5z"/></svg><span class="fold-title">Primer: the aside that grew into a chapter</span>
<span class="fold-toggle"><span class="fold-more">Expand</span><span class="fold-less">Collapse</span><svg class="fold-chev" width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 6l4.5 4.5L12.5 6"/></svg></span>
</summary>
<div class="fold-body">
<button class="fold-rail" type="button" tabindex="-1" aria-hidden="true"></button>
<p>An aside that runs for screens is not a footnote any more, and setting it two
sizes down punished the reader for opening it: the click <em>is</em> them agreeing to
read this.</p>
<p>So the revealed panel is prose — same size, same colour as the article around
it — and what says &quot;inset&quot; is structure: this summary row, the rail down the
left, and the closing row below.</p>
<p>The rail is the part that survives scrolling. Past the first screen the summary
is gone, and the line is the only thing left saying you are still inside
something the article offered to skip.</p>

<button class="fold-end" type="button"><svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 6l4.5 4.5L12.5 6"/></svg>Collapse</button>
</div>
</details>
<h2 id="terms"><a class="hash" href="#terms" aria-label="Section anchor">#</a>Terms</h2>
<p>Hover an underlined word and its definition opens as a card; a click pins the
card so you can read it and follow the link inside. With JavaScript off every
definition sits as a list at the foot of the page, and each word links to its own.</p>
<p>The plainest form — a word and a definition, default type:
every P owns an <a class="term" href="#term-fce53a3b84" data-term="term-fce53a3b84">mcache</a>, and hitting it is the cheapest allocation there is.</p>
<p>With a type label, its colour, and a link in the card's footer:
the compiler inserts a <a class="term c-copper" href="#term-0347695b81" data-term="term-0347695b81">write barrier</a> — which is why a pointer write costs more than an <code>int</code> write.</p>
<p>A definition renders through the same pipeline as the article, so a listing
inside a card is lit exactly as it would be in the prose:
the path is a <a class="term c-blue" href="#term-f8a01a40f2" data-term="term-f8a01a40f2">JSON Pointer</a>, not a dotted path.</p>
<p>When the word reads differently in the sentence than it should in the card's
heading, <code>title=</code> splits the two:
the data lives in the <a class="term c-green" href="#term-c816e741e1" data-term="term-c816e741e1">core's caches</a>, not in RAM.</p>
<h2 id="diagrams"><a class="hash" href="#diagrams" aria-label="Section anchor">#</a>Diagrams</h2>
<p>A memory diagram and a byte strip — raw HTML via <code>{{&lt; raw &gt;}}</code>:</p>

<div class="mem">
  <div class="mem-lab"><code>Point{X, Y int64}</code> <span class="tot">→ 16 bytes, zero padding</span></div>
  <div class="byte-strip" aria-hidden="true">
    <div class="byte-seg"><div class="cells"><div class="byte-box f1" data-tip="X · int64 · 8 B">0</div><div class="byte-box f1" data-tip="X · int64 · 8 B">1</div></div><div class="seg-tag">X (int64)</div></div>
    <div class="byte-seg"><div class="cells"><div class="byte-box f2" data-tip="Y · int64 · 8 B">8</div><div class="byte-box f2" data-tip="Y · int64 · 8 B">9</div></div><div class="seg-tag">Y (int64)</div></div>
  </div>
</div>

<h2 id="widgets"><a class="hash" href="#widgets" aria-label="Section anchor">#</a>Widgets</h2>
<p>Form 1 — an empty mount, brought to life by <code>Taiga.widget</code> from <code>widgets.js</code>:</p>
<figure class="widget">
<figcaption>Play <span class="wnote">— click it, the number ticks</span></figcaption>
<div id="w-ks-counter" class="w-root"></div>
</figure>

<p>Form 2 — a mount with a static fallback inside (visible without JS):</p>
<figure class="widget">
<figcaption>Play <span class="wnote">— a static diagram sits below it</span></figcaption>
<div id="w-ks-fallback" class="w-root"><div class="mem"><div class="mem-lab">static fallback</div><div class="mem-row"><div class="word live" data-tip="visible without JS">e0</div></div></div></div>
</figure>

<p>Form 3 — no id, arbitrary HTML directly in the figure:</p>
<figure class="widget">
<figcaption>Play <span class="wnote">— custom HTML, no mount</span></figcaption>
<div class="w-row"><span class="w-cap">arbitrary widget markup</span></div>
</figure>

<h2 id="card"><a class="hash" href="#card" aria-label="Section anchor">#</a>Card</h2>
<a class="bigcard" href="/taiga/inside/anatomy/guide-is-a-bundle/">
<span class="bc-k">To the reference →</span>
<span class="bc-t">A guide is a folder</span>
<span class="bc-s">A big CTA card: kicker, title, caption.</span>
</a>

<h2 id="link-previews"><a class="hash" href="#link-previews" aria-label="Section anchor">#</a>Link previews</h2>
<p>On a fine pointer, hover any link below to see its preview card — the physics of
the term card, one popover, built from a build-time fragment (internal) or a
pre-rendered card (Telegram, YouTube).</p>
<p>Internal links open a window into the page: a whole <a href="/taiga/inside/anatomy/guide-is-a-bundle/" data-preview="/taiga/inside/anatomy/guide-is-a-bundle/">guide preview</a>,
a <a href="/taiga/inside/anatomy/guide-is-a-bundle/#bundle" data-preview="/taiga/inside/anatomy/guide-is-a-bundle/">section anchor</a> that scopes the card
to one section, and a <a href="/taiga/inside/anatomy/" data-preview="/taiga/inside/anatomy/">series index</a> that lists the parts.</p>
<p>Telegram — a post from the author's own channel earns a card scraped at build
time: <a href="https://t.me/ntuzov/409" target="_blank" rel="noopener" class="lp-nb" data-link="tg" data-tg="https://t.me/ntuzov/409"><span class="lp-w">post </span>409</a> and <a href="https://t.me/ntuzov/410" target="_blank" rel="noopener" class="lp-nb" data-link="tg" data-tg="https://t.me/ntuzov/410"><span class="lp-w">post </span>410</a>.
A <a href="https://t.me/ntuzov/99999" target="_blank" rel="noopener" class="lp-nb" data-link="tg" data-tg="https://t.me/ntuzov/99999"><span class="lp-w">missing </span>post</a> degrades to the channel card, and a
<a href="https://t.me/telegram/999" target="_blank" rel="noopener" class="lp-nb" data-link="tg"><span class="lp-w">foreign </span>channel</a> gets only the ✈ mark, no card.</p>
<p>YouTube — <a href="https://www.youtube.com/watch?v=kedW1xO3Zbo" target="_blank" rel="noopener" class="lp-nb" data-link="yt" data-yt="https://www.youtube.com/watch?v=kedW1xO3Zbo"><span class="lp-w">a talk on the Go </span>scheduler</a>
opens a video card with its thumbnail, duration and view count.</p>
<p>An ordinary <a href="https://go.dev/blog/" target="_blank" rel="noopener" class="lp-nb" data-link="ext"><span class="lp-w">outbound </span>link</a> carries only the ↗ mark.</p>
<p>Wikipedia — a <a href="https://en.wikipedia.org/wiki/Work_stealing" target="_blank" rel="noopener" class="lp-nb" data-link="wiki" data-wiki="https://en.wikipedia.org/wiki/Work_stealing"><span class="lp-w">work-stealing </span>scheduler</a>
and <a href="https://ru.wikipedia.org/wiki/%d0%92%d1%8b%d1%82%d0%b5%d1%81%d0%bd%d1%8f%d1%8e%d1%89%d0%b0%d1%8f_%d0%bc%d0%bd%d0%be%d0%b3%d0%be%d0%b7%d0%b0%d0%b4%d0%b0%d1%87%d0%bd%d0%be%d1%81%d1%82%d1%8c" target="_blank" rel="noopener" class="lp-nb" data-link="wiki" data-wiki="https://ru.wikipedia.org/wiki/Вытесняющая_многозадачность"><span class="lp-w">preemptive </span>multitasking</a>
each open a summary card built from Wikimedia's REST API, carrying the W mark.</p>
<p>The Go blog — a <a href="https://go.dev/blog/loopvar-preview" target="_blank" rel="noopener" class="lp-nb" data-link="gob" data-gob="https://go.dev/blog/loopvar-preview"><span class="lp-w">preview of loop-variable </span>scoping</a>
and the classic <a href="https://go.dev/blog/error-handling-and-go" target="_blank" rel="noopener" class="lp-nb" data-link="gob" data-gob="https://go.dev/blog/error-handling-and-go"><span class="lp-w">error handling in </span>Go</a>
open a card with the italic Go mark, the byline and the post's opening.</p>
<h2 id="newsletter"><a class="hash" href="#newsletter" aria-label="Section anchor">#</a>Newsletter</h2>
<p>The subscription blocks, on a site that runs a newsletter
(<code>params.newsletter.enable</code> — <a href="https://github.com/justskiv/taiga/blob/main/docs/newsletter.md" target="_blank" rel="noopener" class="lp-nb" data-link="ext">newsletter.md</a>).
This demo has no endpoint behind them, so a submission ends in the error state —
which is itself the degradation worth seeing.</p>
<p>The mid-text block, placed by hand, with a close button (<code>dismiss=</code>) whose
verdict is remembered per block:</p>
<aside class="nl-inline" aria-label="Newsletter subscription" data-pagefind-ignore data-nl-dismiss="ks-demo" tabindex="-1">
  <div class="nl-label">series · next</div>
  <div class="nl-txt"><p>This is where a guide makes its own case for the letter — in its own voice, not the theme's.</p>
</div>
  <div class="nl-unit">
  <form class="nl-form" novalidate>
    <label class="nl-vh" for="nl-email-mid-18">Email address</label>
    <input class="nl-field" id="nl-email-mid-18" type="email" name="email" inputmode="email" autocomplete="email" spellcheck="false" placeholder="your@mail.com" required>
    <span class="nl-hp" aria-hidden="true"><input type="text" name="website" tabindex="-1" autocomplete="off"></span>
    <button class="nl-go" type="submit"><span class="nl-spin" aria-hidden="true"></span><span class="nl-go-t">Subscribe</span></button>
  </form>
  <p class="nl-note">A demo form: there is no endpoint behind it.</p>
  <p class="nl-msg" role="status" aria-live="polite"></p>
</div>

  
  <button class="nl-x" type="button" aria-label="Collapse this block" title="Collapse this block"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="m5 5 14 14M19 5 5 19"/></svg></button>
  
  <p class="nl-ghost" aria-label="Collapsed subscription form" hidden>Subscription form collapsed — <button class="nl-ghost-a" type="button" title="Bring the subscription form back">bring it back</button></p>
</aside>
<p>The bare form for a page written as prose (<code>newsletter-inline</code>), the same
component the subscribe page uses:</p>
<div class="nl-prose" data-pagefind-ignore>
  <div class="nl-unit">
  <form class="nl-form" novalidate>
    <label class="nl-vh" for="nl-email-prose-19">Email address</label>
    <input class="nl-field" id="nl-email-prose-19" type="email" name="email" inputmode="email" autocomplete="email" spellcheck="false" placeholder="your@mail.com" required>
    <span class="nl-hp" aria-hidden="true"><input type="text" name="website" tabindex="-1" autocomplete="off"></span>
    <button class="nl-go" type="submit"><span class="nl-spin" aria-hidden="true"></span><span class="nl-go-t">Subscribe</span></button>
  </form>
  <p class="nl-note">A demo form: there is no endpoint behind it.</p>
  <p class="nl-msg" role="status" aria-live="polite"></p>
</div>

</div>
<p>The card at the foot of this page is the third one — <code>newsletter-cta</code>, inserted
automatically here by <code>placements.articleEnd</code>.</p>
]]></content:encoded></item><item><title>Covers, search and other build-time magic</title><link>https://justskiv.github.io/taiga/howto/standalone/build-magic/</link><guid>https://justskiv.github.io/taiga/howto/standalone/build-magic/</guid><pubDate>Sat, 23 May 2026 00:00:00 +0300</pubDate><description>What the theme does at build time: OG covers from a style folder, Pagefind full-text search and the version chip.</description><content:encoded><![CDATA[<h2 id="og-covers"><a class="hash" href="#og-covers" aria-label="Section anchor">#</a>A cover without a designer</h2>
<p>The naive model: covers for sharing are drawn by hand. By the thirtieth article that is a pain, and after a title is renamed it is twice the pain: the picture now lies, go redraw it.</p>
<p>Here the build generates the covers, and the look is described by a <strong>style</strong> — a folder in <code>assets/og/</code>, folder name = style key. Two files inside are mandatory: <code>base.png</code>, the 1200×630 backdrop where all the graphics live, and <code>layout.toml</code>, the layout of the text blocks on top of it:</p>
<span class="code-label">assets/og/dots/layout.toml (fragment)</span>
<pre data-pagefind-weight="0.5"><code class="nohl">[[block]]
text = &#34;{title}&#34;
font = &#34;Inter-ExtraBold.ttf&#34;
size = 66
color = &#34;#e6dfd2&#34;
x = 76
y = 270
width = 1010     # long titles wrap on their own</code></pre><p>The placeholders — <code>{title}</code>, <code>{kicker}</code>, <code>{meta}</code>, <code>{domain}</code>, <code>{part}</code> — are assembled by a partial, so a style is language-neutral: the texts come from the article, the layout from the style. Blocks are drawn in order; unknown keys are ignored — an old style survives a new version of the theme. The optional <code>home.png</code> is a fully baked poster for the home page and the rubrics; without it, the shared cover is generated from <code>base.png</code> by the same pipeline. And a folder with no <code>layout.toml</code> simply does not count as a style.</p>
<p>The style is chosen with the site param <code>ogImages.style</code>, and for a single page with <code>og_style</code> in the front matter. On top of any style the author's override works: drop an <code>og.png</code> into the article's bundle and the generator gives way to your picture. A site adds a style of its own with a folder: the theme's assets and the site's are merged, and the site wins.</p>
<div class="callout trap">
<span class="clabel">Trap</span>
<p>The text on the cover is drawn by Hugo's image engine, and it has limits: letter-spacing is not supported (it is approximated with the font size), and right alignment works only for monospace fonts — the width of a line is counted in characters. Design your styles with this in mind rather than finding it out on a finished cover.</p>

</div>

<h2 id="search"><a class="hash" href="#search" aria-label="Section anchor">#</a>Search that reads everything</h2>
<p>The naive model of search on a static site is an index of titles and descriptions: it finds an article by its name and stays silent about a term from the middle of the text. The engine here is a different one: <strong>Pagefind</strong> builds a full-text index over the already-built site. Which is why a build is two commands:</p>
<span class="code-label">a full site build</span>
<div data-pagefind-weight="0.5"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hugo <span class="o">&amp;&amp;</span> pagefind --site public</span></span></code></pre></div></div><p>Pagefind is a standalone binary; Node is not needed. It walks the finished HTML and takes only what the theme marked up: the body of the article in full, the breadcrumb and the minutes for the result card as separate metadata, while panels, widgets and the rest of the chrome do not get into the index at all. Code blocks are indexed with a lowered weight: a search by function name has to work, but code must not clog the results. Stemming comes out of the box, by the language of the page: a query for &quot;covers&quot; finds &quot;cover&quot;, and on the Russian version of this page the Russian morphology does the same job. The search script and the index itself load lazily, on the first opening of the modal: search nothing and you download not a byte.</p>
<p>Check it on this page: open search and type a word from the middle of the text — &quot;monospace&quot;, say. It will be found, with highlighting and an excerpt. And if you run a bare <code>hugo server</code> without the second command, the modal honestly tells you what builds the index — quietly, without red errors.</p>
<div class="callout key">
<span class="clabel">Key idea</span>
<p>All three mechanisms are build-time. The browser gets a finished picture, a finished index and a finished string; at runtime nobody computes anything and nobody goes off to any service.</p>

</div>

<h2 id="version-chip"><a class="hash" href="#version-chip" aria-label="Section anchor">#</a>The version chip</h2>
<p>The meta of this article carries a &quot;v0.0.1&quot; chip. The theme has no idea what that means: the chip is an arbitrary string from <code>version</code> in the front matter, or, if the field is absent, from the site's <code>versionDefault</code>. A site about a programming language writes the version of the language there, a site about a database writes &quot;PostgreSQL 17&quot;, and this demo writes the version of the theme itself — and the theme renders all of it the same way, as-is. A small detail, but agnosticism is made of exactly these: the theme does not know what your site is about.</p>
<p><em>You can now build a site with covers and full-text search in two commands — and explain why there are exactly two.</em></p>
<h2 id="what-next"><a class="hash" href="#what-next" aria-label="Section anchor">#</a>What's next</h2>
<p>That was the view from outside — what the build does for the reader. The view from inside — how the pages themselves are put together — is in the &quot;Anatomy of the theme&quot; series: leaf bundles, the arithmetic of series, palettes as files.</p>
]]></content:encoded></item><item><title>A palette is one file</title><link>https://justskiv.github.io/taiga/inside/anatomy/palette-is-a-file/</link><guid>https://justskiv.github.io/taiga/inside/anatomy/palette-is-a-file/</guid><pubDate>Sat, 16 May 2026 00:00:00 +0300</pubDate><description>Palettes as data: a toml file in data/themes/, out of which the build makes CSS blocks and the picker's swatches.</description><content:encoded><![CDATA[<h2 id="no-fork"><a class="hash" href="#no-fork" aria-label="Section anchor">#</a>Your own theme without a fork</h2>
<p>The naive model: I want to repaint the site my way — I'll open the theme's CSS and change the colors. The pain arrives with the very first theme update: your edits lie in its files, and every update is a merge conflict. The classic choice of &quot;fork it and drop off upstream — or put up with somebody else's colors&quot;. Both options are bad, and both grow from one mistake: color is treated as <em>styling</em>, when in fact it is <em>data</em> — a table of &quot;variable name → value&quot; that has nowhere to live except somebody else's CSS.</p>
<h2 id="one-toml"><a class="hash" href="#one-toml" aria-label="Section anchor">#</a>One toml</h2>
<p>Here a palette is data, not styles. Each one is a file <code>data/themes/&lt;id&gt;.toml</code>: a couple of housekeeping fields plus color variables whose keys are literally the names of the CSS variables without the <code>--</code>:</p>
<span class="code-label">data/themes/mine.toml</span>
<pre data-pagefind-weight="0.5"><code class="nohl">name = &#34;Mine&#34;     # name in the picker
weight = 80       # position in the picker&#39;s list

&#34;bg-deep&#34; = &#34;#101418&#34;
&#34;bg-surface&#34; = &#34;#1a2027&#34;
&#34;border&#34; = &#34;#2b3138&#34;
&#34;text-primary&#34; = &#34;#d8dee6&#34;</code></pre><p>Highlighted are the only two lines that are not CSS variables. All the rest is a straight &quot;name → color&quot; table. The key ones:</p>
<table>
  <thead>
      <tr>
          <th>Key</th>
          <th>What it paints</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>bg-deep</code></td>
          <td>the deepest background of the page</td>
      </tr>
      <tr>
          <td><code>bg-surface</code></td>
          <td>surfaces: cards, callouts, widgets</td>
      </tr>
      <tr>
          <td><code>border</code></td>
          <td>frames and hairlines</td>
      </tr>
      <tr>
          <td><code>text-primary</code></td>
          <td>the main text</td>
      </tr>
      <tr>
          <td><code>text-muted</code></td>
          <td>captions, metas, the secondary</td>
      </tr>
  </tbody>
</table>
<p>At build time the partial <code>head/palettes.html</code> turns these files into CSS: a <code>:root { … }</code> block for the default palette plus one <code>[data-theme=&quot;&lt;id&gt;&quot;] { … }</code> block per palette — and pours the result into the common CSS bundle first. Which palette is the default is decided by the site parameter <code>defaultTheme</code>: it is just the id of one of the files. Switching the theme is switching one <code>data-theme</code> attribute on <code>&lt;html&gt;</code>; the cascade does the rest. That is also why the switch is instant: the browser has nothing left to fetch, every palette is already in the CSS it got with the page's first byte.</p>
<div class="callout note">
<span class="clabel">Historical note</span>
<p>In the theme's prototype the palettes lived as a JS table: the &quot;View&quot; popover held an array of colors and swapped the variables through <code>style.setProperty</code> right on <code>&lt;html&gt;</code>. During the move the table was taken apart into toml files, and CSS generation was carried off to the build. Now JS does not know the palette values at all — it only reads their list.</p>

</div>

<h2 id="picker-json"><a class="hash" href="#picker-json" aria-label="Section anchor">#</a>The picker and its JSON</h2>
<p>Where does the picker learn which palettes exist? From the same data: the partial puts a <code>&lt;script type=&quot;application/json&quot; id=&quot;dg-themes&quot;&gt;</code> on the page with a list of <code>{id, name, group, weight, swatch colors}</code> — the swatches are taken from four fixed keys of the palette (<code>bg-deep</code>, <code>bg-surface</code>, <code>text-primary</code>, <code>text-muted</code>), so the thumbnail in the picker honestly shows the canvas, the card and the text to come. The popover in the header reads that list and draws the buttons. The widget below reads <strong>the very same</strong> list — and on a click sets the very same attribute:</p>
<figure class="widget">
<figcaption>Play <span class="wnote">— repaint the page; the picker in the header does exactly the same, only it also remembers the choice</span></figcaption>
<div id="w-palette" class="w-root">
<div class="w-cap">The swatches of every palette in the theme go here — clickable, with real switching. Turn JS on to repaint the page.</div>
</div>
</figure>

<p>Notice the honest difference: the widget changes only the attribute, while the picker also writes the choice into storage — which is why after a reload its version wins. And the saved choice is applied before the first paint, by a tiny inline script in <code>&lt;head&gt;</code>: the page does not flash the default palette, not for a single frame.</p>
<p>Now the main thing — extensibility. Your site needs a house palette? Put <code>data/themes/mine.toml</code> <strong>on the site side</strong>: Hugo merges the theme's data with the site's (the site wins on a name clash), and the file finds its own way into both the CSS and the picker. Don't like a palette that ships with the theme — override its file with <code>disabled = true</code>, and it disappears everywhere. Not a line in the theme, no fork.</p>
<p><em>Now you can give a site a palette of its own with one file — without touching the theme's CSS, JS or templates.</em></p>
<h2 id="what-next"><a class="hash" href="#what-next" aria-label="Section anchor">#</a>What's next</h2>
<p>The &quot;Anatomy of the theme&quot; series ends here: a guide is a folder, a series is a folder of guides, a palette is a file. Everything shown — and a dozen components we never got to — is gathered on one proving-ground page:</p>
<a class="bigcard" href="/taiga/reference/kitchen-sink/">
<span class="bc-k">Proving ground →</span>
<span class="bc-t">Kitchen-sink</span>
<span class="bc-s">Every component of the theme on one page — to look at and to copy</span>
</a>

<p>And as for what happens at build time — covers, search, the version chip — there is a deep dive of its own in &quot;How-to&quot;.</p>
]]></content:encoded></item><item><title>A series knows itself</title><link>https://justskiv.github.io/taiga/inside/anatomy/series-knows-itself/</link><guid>https://justskiv.github.io/taiga/inside/anatomy/series-knows-itself/</guid><pubDate>Sat, 09 May 2026 00:00:00 +0300</pubDate><description>How series work: a series is a folder in its rubric — and the “part N of M” kicker, the panel and the bridge count themselves at build time.</description><content:encoded><![CDATA[<h2 id="links-by-hand"><a class="hash" href="#links-by-hand" aria-label="Section anchor">#</a>Contents by hand</h2>
<p>The naive model: a series is when every article ends with a list of links to the rest. It works right up to the second change. Add a fourth part — go update three old articles. Reorder the parts — &quot;part 3 of 7&quot; in the headings now lies, and lucky you if you remembered all seven. Contents smeared across the articles go stale the moment you edit anything — because there are as many sources of truth as there are parts.</p>
<p>Worse: half of the series machinery cannot be expressed by hand-written links at all. &quot;~9 minutes left to the end of the series&quot; is arithmetic over every part at once; no article knows that about itself alone.</p>
<h2 id="membership"><a class="hash" href="#membership" aria-label="Section anchor">#</a>A folder, not a field</h2>
<p>Here a series has one source of truth. A part declares almost nothing about itself — it simply lies in the series folder, a sub-section of the rubric:</p>
<span class="code-label">content/inside/ — the folder of this very series</span>
<pre data-pagefind-weight="0.5"><code class="nohl">content/inside/
  anatomy/                  ← the series
    _index.md               ← series metadata: title, description, order
    guide-is-a-bundle/      ← part, weight: 10
    series-knows-itself/    ← part, weight: 20 — you are here
    palette-is-a-file/      ← part, weight: 30</code></pre><p>This is the folder of the article you are reading — check it against the kicker above the title. Membership is location; the one thing a part carries itself is <code>weight</code> in its front matter, its order among the parts. Everything else is computed at build time: the parts are sorted by <code>weight</code>, the &quot;N&quot; is the position in the sorted list, the &quot;M&quot; is its length. Notice: the order is set by weight, not by publication dates — you can reorder the parts without rewriting the feed's history.</p>
<p>From that same sorted list everything series-shaped on the page is assembled. The kicker is glued from three independent sources: the rubric label (the section's front matter), the series title (the front matter of the series' <code>_index.md</code>) and the computed &quot;part N of M&quot;. The panel on the left is the contents with the current part marked. The bridge at the bottom is the prev/next neighbors plus a progress scale. One list, four consumers.</p>
<p>Here is our series through the eyes of the build:</p>

<div class="mem">
  <div class="mem-lab">inside/anatomy/ <span class="tot">→ 3 parts · ~14 min in total</span></div>
  <div class="header">
    <div class="header-word ptr" data-tip="folder · the series slug, in latin: anatomy"><span class="wk">folder</span><span class="wv">→</span></div>
    <div class="header-word" data-tip="parts in the series: 3"><span class="wk">parts</span><span class="wv">3</span></div>
    <div class="header-word" data-tip="the mins of all parts summed: 14"><span class="wk">mins</span><span class="wv">14</span></div>
  </div>
  <div class="mem-arrow">│ folder ▼</div>
  <div class="mem-row">
    <div class="word ro" data-tip="part 1 · «A guide is a folder» · weight 10 · ~5 min">w10</div>
    <div class="word live" data-tip="part 2 · this very article · weight 20 · ~4 min">w20</div>
    <div class="word spare" data-tip="part 3 · «A palette is one file» · weight 30 · ~5 min">w30</div>
  </div>
</div>

<p>The highlighted cell is the article you are standing inside. Hover the neighbors: each tooltip carries its weight and its minutes.</p>
<p>The series title, its description and its order on the rubric page are no magic either: that is the front matter of the folder's <code>_index.md</code> — here, <code>content/inside/anatomy/_index.md</code> (scaffold one with <code>hugo new content inside/anatomy/_index.md --kind series</code>). The same file gives the series a <strong>landing page</strong> at <code>/inside/anatomy/</code>: the title with its tagline, the description, the file's own body as an epigraph, the list of parts and a &quot;start the series&quot; button. The &quot;whole series&quot; links in the panel and the bridge lead exactly there; on the rubric page the same series stands as an anchored block. And a series announced with an <code>_index.md</code> but no parts yet is not an error: the rubric shows it as an &quot;in the works&quot; teaser, and its landing renders the announcement.</p>
<div class="callout internals">
<span class="clabel">Under the hood</span>
<p>The series context is resolved by the helper partial <code>article/series-ctx.html</code>: the series is simply the page's <code>.CurrentSection</code> whenever the page sits deeper than its rubric, and the parts are that section's regular pages — <code>weight</code>-ascending, Hugo's default sort. The bridge takes its position from there: prev/next are the neighbors in the list, the scale is <code>flex-grow: mins</code> of each part, and &quot;~N min left&quot; is the sum of <code>mins</code> over the parts after the current one.</p>

</div>

<h2 id="check-math"><a class="hash" href="#check-math" aria-label="Section anchor">#</a>Check the arithmetic</h2>
<p>The bridge's formula is short enough to recompute right here. The widget below runs the same logic over the numbers of this very series:</p>
<figure class="widget">
<figcaption>Play <span class="wnote">— put yourself on part 3 and see what the bridge says about the remainder</span></figcaption>
<div id="w-series-math" class="w-root">
<div class="w-cap">The “you are on part k” slider recomputes “~N min left” and the bridge's scale. Turn JS on and we'll count together.</div>
</div>
</figure>

<p>Notice: the numbers in the widget match the diagram above and the real bridge at the bottom of the page. That is the point — all three have one source.</p>
<p>And what about articles outside series? They get the same kicker, only instead of &quot;part N of M&quot; it says &quot;deep dive&quot;, and the panel on the left is filled from the <code>related</code> field in the front matter: three to five links to nearby guides. A series is an option, not an obligation.</p>
<p><em>Now you can assemble a series out of any guides by gathering them into one folder — and reorder the parts by changing one <code>weight</code>, not N articles.</em></p>
<h2 id="what-next"><a class="hash" href="#what-next" aria-label="Section anchor">#</a>What's next</h2>
<p>The bridge below has already worked out that one part is left. It is about color: why all seven palettes of this page live in data files, and how to add your own without touching a line of the theme.</p>
]]></content:encoded></item><item><title>A guide is a folder</title><link>https://justskiv.github.io/taiga/inside/anatomy/guide-is-a-bundle/</link><guid>https://justskiv.github.io/taiga/inside/anatomy/guide-is-a-bundle/</guid><pubDate>Sat, 02 May 2026 00:00:00 +0300</pubDate><description>The theme's content model: an article is a leaf bundle, where its widgets, styles and cover sit next to the text.</description><content:encoded><![CDATA[<h2 id="naive-picture"><a class="hash" href="#naive-picture" aria-label="Section anchor">#</a>I want a live picture</h2>
<p>The naive model goes like this: I'm writing a text, and at one spot I need an interactive illustration. A button, a counter, anything. Feels like ten lines of JS.</p>
<p>Now the pain. In an ordinary theme there is nowhere to put those ten lines. The script has to be wired into <code>&lt;head&gt;</code> — so you edit a template. The template is shared — so the script ships to every page at once. To stop it, you need a registry of which script belongs to which page — so you add config. A month later you have three articles with widgets and one template file nobody dares to touch. The text lives in one place, its code in three others, and you can no longer move the article as a whole: it has grown roots into the theme.</p>
<h2 id="bundle"><a class="hash" href="#bundle" aria-label="Section anchor">#</a>An article is a folder</h2>
<p>Here an article is a <strong>leaf bundle</strong>: a folder that holds everything it owns. Here is the file strip of the bundle — the very folder this page was built from:</p>

<div class="byte-strip" aria-hidden="true">
  <div class="byte-seg"><div class="cells"><div class="byte-box f0" data-tip="index.md · text and front matter · required">md</div></div><div class="seg-tag">index.md</div></div>
  <div class="byte-seg"><div class="cells"><div class="byte-box f1" data-tip="widgets.js · the page's widgets · the theme wires it in itself">js</div></div><div class="seg-tag">widgets.js</div></div>
  <div class="byte-seg"><div class="cells"><div class="byte-box f2" data-tip="widgets.css · styles of your own · when the building-block classes run out">css</div></div><div class="seg-tag">widgets.css</div></div>
  <div class="byte-seg"><div class="cells"><div class="byte-box pad" data-tip="og.png · your own cover · overrides the generated one">og</div></div><div class="seg-tag padtag">↳ opt.</div></div>
</div>

<p>Only <code>index.md</code> is required. Everything else is an optional neighbor: drop <code>widgets.js</code> beside it and the theme finds it, minifies it, fingerprints it and wires it in <strong>on this page only</strong>. Drop <code>widgets.css</code> — the same for styles. Drop <code>og.png</code> — the social cover is taken from you instead of being generated.</p>
<div class="callout key">
<span class="clabel">Key idea</span>
<p>Everything an article needs lies next to the article. Moving a guide to another site means moving one folder. Deleting it means deleting one folder. No loose ends in templates and configs.</p>

</div>

<h2 id="two-touches"><a class="hash" href="#two-touches" aria-label="Section anchor">#</a>Two touches</h2>
<p>The full cycle of &quot;add a live illustration&quot; is two touches. First: in the text you place a shortcode with an id and a prompt. Second: in <code>widgets.js</code> you register an initializer with the same id:</p>
<span class="code-label">inside/guide-is-a-bundle/widgets.js (excerpt)</span>
<div data-pagefind-weight="0.5"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">Taiga</span><span class="p">.</span><span class="nx">widget</span><span class="p">(</span><span class="s2">&#34;w-hello&#34;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">root</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">root</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="s2">&#34;&#34;</span><span class="p">;</span> <span class="c1">// drop the static fallback
</span></span></span><span class="line"><span class="cl">  <span class="c1">// …button, counter, badge — all the code in the file next door
</span></span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div></div><p>No templates, no <code>&lt;head&gt;</code>, no registries. The shortcode has a paired form: inside it goes a static fallback that lives in the mount point until JS replaces it — and if JS is off, it stays for good. A page without scripts degrades into meaningful text, not into a hole.</p>
<p>Notice what the example above does not have: styles of its own. The building-block classes — <code>.w-row</code> for a row of controls, <code>.w-btn</code> for buttons, <code>.w-num</code> for a big number, <code>.w-cap</code> for the output line — live in the theme's CSS, and the widget looks native without hauling in a single rule of its own. <code>widgets.css</code> is needed only when that set runs out — and then it, like everything else, simply goes into the folder.</p>
<h2 id="runtime"><a class="hash" href="#runtime" aria-label="Section anchor">#</a>The runtime catches the fall</h2>
<p><code>Taiga.widget</code> is a small runtime in the theme. It waits for the DOM, finds the mount point by id and calls your callback wrapped in a <code>try/catch</code>: a widget that throws writes to the console and does not take its neighbors down. And if there is no mount point — the shortcode was taken out of the text — the initializer is skipped in silence.</p>
<div class="callout trap">
<span class="clabel">Trap</span>
<p>&quot;In silence&quot; cuts both ways. The id in the shortcode and in <code>Taiga.widget(…)</code> must match letter for letter: on a typo the runtime decides the shortcode is gone, and you get neither a widget nor an error. An empty square where the illustration should be — check the id first.</p>

</div>

<p>Now check all of it on a live specimen. The widget below was mounted by that very runtime, out of that very <code>widgets.js</code> that lies in this article's folder:</p>
<figure class="widget">
<figcaption>Play <span class="wnote">— click the button a few times, then reload the page: the counter lives in a closure, not in the theme</span></figcaption>
<div id="w-hello" class="w-root">
<div class="w-cap">A click counter lives here. Right now you see the static fallback: turn JS on and it comes alive.</div>
</div>
</figure>

<p>One last thing: the mechanism is not tied to guides. <code>widgets.js</code> is picked up from <strong>any</strong> bundle — an interactive page here is not a separate type with a template of its own, but an ordinary markdown page with a script lying next to it.</p>
<p><em>Now you can add a live illustration to any guide without opening a single file of the theme.</em></p>
<h2 id="what-next"><a class="hash" href="#what-next" aria-label="Section anchor">#</a>What's next</h2>
<p>This article is part of a series, and the panel to the left of the text already carries its contents, while the kicker says &quot;part 1 of 3&quot;. Nobody typed that by hand. Where the theme knows it all from — in part two.</p>
]]></content:encoded></item></channel></rss>