<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>lotek.run</title>
    <link>https://lotek.run</link>
    <description>dispatches from the margins</description>
    <lastBuildDate>Wed, 17 Jun 2026 21:16:13 -0700</lastBuildDate>
    <item>
      <title>Against the Stack</title>
      <link>https://lotek.run/posts/2026-06-30-against-the-stack.html</link>
      <pubDate>2026-06-30</pubDate>
      <description><![CDATA[<p>Every six months someone publishes a guide to the modern web
development stack. It is always the same guide. Pick a framework. Add a
bundler. Wire up a type system. Choose a CSS solution. God no, not that
one! Configure your linting. Set up CI. Deploy to a managed platform
that charges by the build minute.</p>
<p>The guide is 12,000 words long. It has a table of contents. There are
affiliate links, and you need to subscribe on medium to see the bottom
third…</p>
<p>By the time you finish reading it, the framework has released a major
version and the guide is already wrong.</p>
<hr />
<p>It doesn’t need to be like this. There is a different way to build
things on the internet. It has been available the entire time. You write
HTML. You write CSS. You put the files somewhere they can be served.</p>
<p>This is not nostalgic. Fewer moving parts means fewer things that
break. You spend more time writing and less time debugging your
bullshit.</p>
<p>The stack is a solution to a problem most of us don’t have.</p>
<hr />
<p>This site is built with a Python script and pandoc. The Python script
is about two hundred lines. It has no dependencies outside the standard
library. It calls pandoc to convert markdown to HTML and assembles pages
from flat template files. I can do this from any computer that has an
internet connection.</p>
<p>The pandoc binary is large for a single executable. It does one
thing. It does it correctly. It has a command-line interface. It will
probably do the same thing it does now ten years from now.</p>
<p>The HTML output is readable by any browser built in the last 30
years. The CSS is a flat file with no preprocessor. Time is a flat
circle with an continuous preprocessor.</p>
<p>Nothing here requires Node.js. You could read this with wget.</p>
<hr />
<p>The case for the stack is that it scales. True enough. It also
requires three engineers to maintain the build configuration and a
fourth to update the dependencies when a zero-day drops in a package
four levels deep in the tree that nobody has heard of.</p>
<p>Most things don’t need to scale. They need to work, keep working, and
be readable by the person who wrote them when that person comes back two
years later.</p>
<hr />
<p>This site’s entire build system:</p>
<pre><code class="language-python">    <span class="c"># TODO: insert this when you get the repos worked out</span></code></pre>
<p>Two hundred lines total. No lock file. No <code>node_modules</code>.
No CI pipeline to maintain.</p>
]]></description>
    </item>
    <item>
      <title>Friction</title>
      <link>https://lotek.run/posts/2026-06-23-friction.html</link>
      <pubDate>2026-06-23</pubDate>
      <description><![CDATA[<p>The internet got worse when it got easier to use.</p>
<p>This is not a popular opinion. Making things easier is supposed to be
good. Accessibility is good. Lowering barriers is good. More voices is
good. All of that is true and the internet is still worse, and I may be
old but I swear I am not yelling at clouds here.</p>
<hr />
<p>The early internet had friction everywhere. You needed to know what a
DNS record was. You needed a shell account or a dialup connection and a
local ISP that someone had to physically maintain. You needed to learn a
text editor before you could participate in anything. The friction was
not intentional design. It was just the state of things.</p>
<p>But the friction had an effect. It filtered for a certain kind of
person. Not smarter, not better; just someone who wanted to be there
enough to figure out the tools. That changes what kind of participation
happens.</p>
<p>I often recall in the 2010-2015 years (interesting political overlap
there) that people often spoke of a “marketplace of ideas”. I genuinely
wanted to believe in that. Sometime around 2020 I recall seeing the apt
and modern retort: “an Amazon of Concurrency”. I think that better
captures the atmosphere we are left with.</p>
<hr />
<p>Cory Doctorow calls it (or something very adjacent to this)
enshittification. Platforms are good for users, then good for
advertisers, then good only for the platform. Once a platform reaches
scale, its users are the product. Their attention is the inventory. The
platform’s job is to maximize the value extracted from that
inventory.</p>
<p>Friction interferes with extraction. Some users leave before they can
be monetized. Some conversations stay small and weird instead of
becoming content.</p>
<p>So the friction gets removed. And the thing that made the place worth
being in disappears with it.</p>
<hr />
<p>The dark forest theory (under abuse) says that when public spaces
become hostile, interesting people retreat to smaller private ones.
Group chats. Email lists. Invite-only forums. Places where the entry
cost is knowing someone who is already there.</p>
<p>The cost of entry is not money. It is social. You have to be the kind
of person that other people want to invite. This is a form of
friction.</p>
<p>The interesting internet didn’t disappear. It moved to places that
require a little more effort to find.</p>
<hr />
<p>This site has no comment section. There is no reply button, no
reaction, no share count. There is nothing to optimize against. The only
metric I have is whether I think the thing I wrote is worth reading.</p>
<p>We will see if that turns out to be a better hedge against metrics
than I expected.</p>
]]></description>
    </item>
    <item>
      <title>Syntax Highlighting Test</title>
      <link>https://lotek.run/posts/2026-06-16-syntax.html</link>
      <pubDate>2026-06-16</pubDate>
      <description><![CDATA[<p>Fenced code blocks get highlighted at build time. No JavaScript. A
small tokenizer in build.py handles keywords, strings, and comments.</p>
<hr />
<p><strong>Python</strong></p>
<pre><code class="language-python"><span class="c">#!/usr/bin/env python3</span>
<span class="k">from</span> pathlib <span class="k">import</span> Path
<span class="k">from</span> datetime <span class="k">import</span> datetime, timezone


<span class="k">def</span> parse_frontmatter(text: str) -&gt; tuple[dict, str]:
    <span class="k">if</span> <span class="k">not</span> text.startswith(<span class="s">"---\n"</span>):
        <span class="k">return</span> {}, text
    <span class="k">try</span>:
        end = text.index(<span class="s">"\n---\n"</span>, 4)
    <span class="k">except</span> ValueError:
        <span class="k">return</span> {}, text
    meta = {}
    <span class="k">for</span> line <span class="k">in</span> text[4:end].splitlines():
        <span class="k">if</span> <span class="s">":"</span> <span class="k">in</span> line:
            k, _, v = line.partition(<span class="s">":"</span>)
            meta[k.strip()] = v.strip()
    <span class="k">return</span> meta, text[end + 5:]


<span class="k">if</span> __name__ == <span class="s">"__main__"</span>:
    <span class="k">for</span> path <span class="k">in</span> sorted(Path(<span class="s">"content/posts"</span>).glob(<span class="s">"*.md"</span>), reverse=<span class="k">True</span>):
        meta, _ = parse_frontmatter(path.read_text())
        print(f<span class="s">"{meta.get('date', '?')}  {meta.get('title', path.stem)}"</span>)</code></pre>
<hr />
<p><strong>Bash</strong></p>
<pre><code class="language-bash"><span class="c">#!/usr/bin/env bash</span>
<span class="k">set</span> -euo pipefail

SITE=<span class="s">"lotek.run"</span>
OUT=<span class="s">"output"</span>

<span class="k">if</span> [ ! -d <span class="s">"$OUT"</span> ]; <span class="k">then</span>
    echo <span class="s">"run build.py first"</span> &gt;&amp;2
    <span class="k">exit</span> 1
<span class="k">fi</span>

<span class="c"># rsync to remote, delete files that no longer exist locally</span>
rsync -avz --delete <span class="s">"$OUT/"</span> <span class="s">"user@$SITE:/var/www/html/"</span>
echo <span class="s">"deployed."</span></code></pre>
<hr />
<p><strong>Rust</strong></p>
<pre><code class="language-rust"><span class="k">use</span> std::fs;
<span class="k">use</span> std::path::Path;

<span class="k">fn</span> slugify(title: &amp;str) -&gt; String {
    title
        .to_lowercase()
        .chars()
        .map(|c| <span class="k">if</span> c.is_alphanumeric() { c } <span class="k">else</span> { '-' })
        .collect::&lt;String&gt;()
        .trim_matches('-')
        .to_string()
}

<span class="k">fn</span> main() {
    <span class="k">let</span> posts = Path::new(<span class="s">"content/posts"</span>);
    <span class="k">for</span> entry <span class="k">in</span> fs::read_dir(posts).expect(<span class="s">"no posts dir"</span>) {
        <span class="k">let</span> path = entry.unwrap().path();
        <span class="k">if</span> path.extension().map_or(<span class="k">false</span>, |e| e == <span class="s">"md"</span>) {
            println!(<span class="s">"{}"</span>, path.display());
        }
    }
}</code></pre>
<hr />
<p><strong>TOML</strong></p>
<pre><code class="language-toml">[site]
title = <span class="s">"lotek.run"</span>
url   = <span class="s">"https://lotek.run"</span>
desc  = <span class="s">"dispatches from the margins"</span>

[build]
posts_dir    = <span class="s">"content/posts"</span>
output_dir   = <span class="s">"output"</span>
date_format  = <span class="s">"%Y-%m-%d"</span>

[author]
name    = <span class="s">"unknown"</span>
contact = <span class="s">"find a way"</span></code></pre>
<hr />
<p><strong>SQL</strong></p>
<pre><code class="language-sql"><span class="c">-- hypothetical: if this site ever needed a database (it doesn't)</span>
<span class="k">SELECT</span>
    slug,
    title,
    date,
    array_length(string_to_array(tags, ',<span class="s">'), 1) AS tag_count
FROM posts
WHERE date &gt;= current_date - interval '</span>90 days'
<span class="k">ORDER</span> <span class="k">BY</span> date DESC;</code></pre>
]]></description>
    </item>
    <item>
      <title>Image Test</title>
      <link>https://lotek.run/posts/2026-06-16-image-test.html</link>
      <pubDate>2026-06-16</pubDate>
      <description><![CDATA[<p>Images go in <code>static/img/</code> and get referenced like any
other static asset. The build script copies the whole
<code>static/</code> tree to <code>output/static/</code> unchanged.</p>
<figure>
<img src="/static/img/grid.png" alt="amber dot grid" />
<figcaption aria-hidden="true">amber dot grid</figcaption>
</figure>
<hr />
<p>To add an image to a post:</p>
<ol type="1">
<li>Compress it. Phone photos are 4MB. That is not a web image.</li>
<li>Drop it in <code>static/img/</code>.</li>
<li>Reference it in markdown as
<code>![alt text](/static/img/filename.jpg)</code>.</li>
<li>Run <code>python3 build.py</code>.</li>
</ol>
<p>For compression: <code>cwebp -q 80 input.jpg -o output.webp</code> or
<code>jpegoptim --max=85 photo.jpg</code>. Under 200kb for most images,
under 500kb for anything you genuinely need at full resolution. Do not
need things at full resolution.</p>
<p>Write actual alt text.</p>
<pre><code class="language-python">
<span class="c">#TODO: implement auto image compression.  another build step _might_ be okay.</span>
</code></pre>
]]></description>
    </item>
    <item>
      <title>Hello</title>
      <link>https://lotek.run/posts/2026-06-15-hello.html</link>
      <pubDate>2026-06-15</pubDate>
      <description><![CDATA[<p>This is a website. No tracking, no comments, no newsletter. It
doesn’t set cookies so there’s no cookie banner. RSS feed is in the
footer.</p>
<hr />
<p>The name is from William Gibson’s Johnny Mnemonic. The Lo-Tek are an
underground community living in the infrastructure of the city – on
bridges, in access tunnels, in the margins between the corporate system
and nowhere. Not against technology. They use what they need and not
more.</p>
<p>That seems like the right disposition in 2026.</p>
<hr />
<p>No schedule. Posts when something seems worth writing down. Long or
short depending on what it needs.</p>
<p>If you want to say something about something here, find a way to
reach out. The friction is intentional.</p>
]]></description>
    </item>

  </channel>
</rss>
