<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/assets/atom.xsl" type="text/xsl"?><feed
	xmlns="http://www.w3.org/2005/Atom"
	xmlns:thr="http://purl.org/syndication/thread/1.0"
	xml:lang="en-US"
	><title>Sacha Chua - category - 11ty</title>
	<subtitle>Emacs, sketches, and life</subtitle>
	<link rel="self" type="application/atom+xml" href="https://sachachua.com/blog/category/11ty/feed/atom/index.xml" />
  <link rel="alternate" type="text/html" href="https://sachachua.com/blog/category/11ty" />
  <id>https://sachachua.com/blog/category/11ty/feed/atom/index.xml</id>
  <generator uri="https://11ty.dev">11ty</generator>
	<updated>2025-04-03T17:56:09Z</updated>
<entry>
		<title type="html">Adding subheadings and sketches to my blog page navigation</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2025/04/adding-subheadings-and-sketches-to-my-blog-page-navigation/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2025-04-04T13:52:18Z</updated>
    <published>2025-04-03T17:56:09Z</published>
    <category term="11ty" />
<category term="blogging" />
		<id>https://sachachua.com/blog/2025/04/adding-subheadings-and-sketches-to-my-blog-page-navigation/</id>
		<content type="html"><![CDATA[<div class="update" id="orgebcac51">
<p>
<span class="timestamp-wrapper"><span class="timestamp">[2025-04-04 Fri]</span></span>: Fixed link to onThisPage.cjs. Thanks to John Rakestraw for pointing it out!
</p>

</div>

<div class="assumed_audience" id="org81e1a06">
<p>
Assumed audience:
</p>
<ul class="org-ul">
<li>My future self, when I'm trying to figure out where to change things if I want to implement something similar</li>
<li>People who like tweaking their blog's CSS, especially if they also use Org Mode or 11ty</li>
</ul>

</div>

<p>
Headings help us make sense of longer blog posts.
Heading links are like signposts letting you know
what's ahead and where you can take a shortcut to
get to what you're interested in.
</p>

<p>
Headings are useful for me too. Sometimes I browse
my blog and come across things I've completely
forgotten writing about, so the headings can help
me remember without having to reread long posts.
If I use headings more often, I might be able to
work with <a href="https://sachachua.com/blog/2025/03/playing-with-chunk-size-when-writing/">bigger chunks of thoughts</a>. If I can work
with bigger chunks of thoughts, then maybe I can
think about more things that are hard to fit
within the limits of my working memory. Making
headings more navigable also means I might not
have to worry about the tangents I go on and the
number of different thoughts I try to smoosh
together, if people can jump straight to the parts
that sound relevant to them.
</p>

<p>
I particularly like the way Karthik uses a sticky
table of contents for long blog posts like <a href="https://karthinks.com/software/emacs-window-management-almanac/">The
Emacs Window Management Almanac | Karthinks</a>. I
also like the way <a href="https://sphinx-rtd-theme.readthedocs.io/">the Read the Docs Sphinx Theme</a>
displays a nested table of contents on the left
side on wide screens. I use <a href="https://github.com/fniessen/org-html-themes">org-html-themes</a> to
export Org Mode files with that theme when I want
to be fancy, like <a href="https://sachachua.com/dotemacs">my Emacs configuration</a> (usually
works, although sometimes my config is broken).
</p>

<p>
The last time I <a href="https://sachachua.com/blog/2024/11/thinking-about-webpage-margins/">tinkered with my webpage margins</a>,
I put my "On this page" list on the left side and
the blog post headings (if any) on the right side,
mostly because it was easy to do. I just changed
the margin and float attributes of the element
with the subheadings. I'd like to clear up more
space for potential <a href="https://gwern.net/sidenote">sidenotes</a> or doodles, though.
This time, I experimented with nesting the blog
navigation inside the "On this page" navigation on
the left side.
</p>


<figure id="org4166041">
<img src="https://sachachua.com/blog/2025/04/adding-subheadings-and-sketches-to-my-blog-page-navigation/2025-04-02_14-04-30.png" alt="2025-04-02_14-04-30.png">

<figcaption><span class="figure-number">Figure 1: </span>A screenshot of my blog showing the nested links</figcaption>
</figure>

<p>
Here's how I did it:
</p>
<div id="outline-container-adding-subheadings-and-sketches-to-my-blog-page-navigation-org-mode-where-i-start-writing" class="outline-3">
<h3 id="adding-subheadings-and-sketches-to-my-blog-page-navigation-org-mode-where-i-start-writing">Org Mode: where I start writing</h3>
<div class="outline-text-3" id="text-adding-subheadings-and-sketches-to-my-blog-page-navigation-org-mode-where-i-start-writing">
<p>
I use Org Mode's table of contents directive to
include a table of contents in blog posts. I wrap
it inside a <code>sticky-toc</code> block to indicate when I
want it to be part of the sticky table of contents
on my blog. In Org Mode, the syntax looks like
this:
</p>


<div class="org-src-container">
<pre class="src src-org"><span class="org-org-block-begin-line">#+begin_sticky-toc</span>
#+TOC: headlines 2 local
<span class="org-org-block-end-line">#+end_sticky-toc</span>
</pre>
</div>


<p>
I put it in a <a href="https://joaotavora.github.io/yasnippet/">yasnippet</a> so that I don't have to
remember it. I just type <code>tocs</code> (for TOC, sticky)
and then press <code>TAB</code> to complete it.
</p>
</div>
</div>
<div id="outline-container-adding-subheadings-and-sketches-to-my-blog-page-navigation-11ty-static-site-generator-on-this-page" class="outline-3">
<h3 id="adding-subheadings-and-sketches-to-my-blog-page-navigation-11ty-static-site-generator-on-this-page">11ty static site generator: on this page</h3>
<div class="outline-text-3" id="text-adding-subheadings-and-sketches-to-my-blog-page-navigation-11ty-static-site-generator-on-this-page">
<p>
After exporting individual HTML files from Org
Mode, I turn them into my blog using the <a href="https://www.11ty.dev/">11ty</a>
static site generator. To simplify my archive
pages, I have an <a href="https://github.com/sachac/eleventy-blog-setup/blob/master/_includes/shortcodes/onThisPage.cjs"><code>onThisPage</code></a> shortcode which
lists the posts on that page. I changed it to
include the <code>sticky-toc</code> contents from the items'
<code>templateContent</code> attributes.
</p>


<div class="org-src-container">
<pre class="src src-js"><span class="org-keyword">const</span> { JSDOM } = require(<span class="org-string">'jsdom'</span>);

module.exports = <span class="org-keyword">function</span> (<span class="org-variable-name">eleventyConfig</span>) {
  <span class="org-keyword">function</span> <span class="org-function-name">formatPostLine</span>(<span class="org-variable-name">item</span>, <span class="org-variable-name">index</span>) {
    <span class="org-keyword">let</span> <span class="org-variable-name">subtoc</span> = <span class="org-string">''</span>;
    <span class="org-keyword">if</span> (item.templateContent?.match(<span class="org-string">/sticky-toc/</span>)) {
      <span class="org-keyword">const</span> <span class="org-variable-name">doc</span> = <span class="org-keyword">new</span> <span class="org-type">JSDOM</span>(item.templateContent).window.document;
      <span class="org-keyword">const</span> <span class="org-variable-name">sub</span> = doc.querySelector(<span class="org-string">'.sticky-toc ul, .sticky-toc div, .sticky-toc-after-scrolling div'</span>);
      <span class="org-keyword">if</span> (sub) {
        <span class="org-keyword">if</span> (sub.querySelector(<span class="org-string">'.panzoom'</span>)) {
          console.log(<span class="org-string">'remove panzoom'</span>);
          sub.querySelector(<span class="org-string">'.panzoom'</span>).classList.remove(<span class="org-string">'panzoom'</span>);  <span class="org-comment-delimiter">// </span><span class="org-comment">don't do panzoom for now</span>
        }
        sub.classList.remove(<span class="org-string">'panzoom'</span>);
        subtoc = sub.outerHTML;
      }
    }
    <span class="org-keyword">return</span> <span class="org-string">`&lt;li&gt;&lt;a class="toc-link" data-index="index${index}" href="${item.url}"&gt;${item.data.title}&lt;/a&gt;${subtoc}&lt;/li&gt;`</span>;
  }
  eleventyConfig.addShortcode(<span class="org-string">'onThisPage'</span>, <span class="org-keyword">function</span> (<span class="org-variable-name">list</span>) {
    <span class="org-keyword">return</span> <span class="org-string">`&lt;nav class="on-this-page"&gt;</span>
<span class="org-string">On this page:</span>
<span class="org-string">&lt;ul&gt;</span>
<span class="org-string">${list.map(formatPostLine).join("\n")}</span>
<span class="org-string">&lt;/ul&gt;</span>
<span class="org-string">&lt;/nav&gt;`</span>;
  });
};
</pre>
</div>


<p>
And then there's a bunch of CSS in <a href="https://github.com/sachac/eleventy-blog-setup/blob/master/assets/css/style.css">assets/css/style.css</a>:
</p>

<p>
</p><details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;">
                  <summary><strong>CSS</strong></summary>

<div class="org-src-container">
<pre class="src src-css"><span class="org-comment-delimiter">/* </span><span class="org-comment">tables of contents</span><span class="org-comment-delimiter"> */</span>
<span class="org-css-selector">.on-this-page &gt; ul &gt; li &gt; ul, .on-this-page &gt; ul &gt; li &gt; div</span> { <span class="org-css-property">display</span>: none }

<span class="org-builtin">@media</span> only screen and (width &gt;= 95em) {
    <span class="org-css-selector">html, body</span> { <span class="org-css-property">overflow-x</span>: unset; }

    <span class="org-css-selector">.sticky-toc, .sticky-left, .sticky-right</span> {
        <span class="org-css-property">font-size</span>: var(<span class="org-variable-name">&#45;&#45;fs-sm</span>);
        <span class="org-css-property">width</span>: calc((100vw - var(<span class="org-variable-name">&#45;&#45;body-max-width</span>) - 5rem)/2);
        <span class="org-css-property">position</span>: sticky;
        <span class="org-css-property">max-height</span>: calc(100vh - 2rem);
        <span class="org-css-property">overflow-y</span>: auto;
        <span class="org-css-property">scroll-behavior</span>: smooth;
        <span class="org-css-property">background</span>: var(<span class="org-variable-name">&#45;&#45;modus-bg-main</span>);
        <span class="org-css-property">top</span>: 0;
        <span class="org-css-property">padding</span>: 1rem;
    }

    <span class="org-css-selector">article .sticky-toc</span> {
        <span class="org-css-property">display</span>: none
    }

    <span class="org-css-selector">.single-post article .sticky-toc</span> {
        <span class="org-css-property">display</span>: block;
    }

    <span class="org-css-selector">.sticky-toc, .sticky-left, .single-post article .sticky-toc</span> {
        <span class="org-css-property">margin-left</span>: calc((-100vw + var(<span class="org-variable-name">&#45;&#45;body-max-width</span>))/2);
        <span class="org-css-property">float</span>: left;
    }

    <span class="org-css-selector">.sticky-right</span> {
        <span class="org-css-property">margin-right</span>: calc((-100vw + var(<span class="org-variable-name">&#45;&#45;body-max-width</span>))/2);
        <span class="org-css-property">float</span>: right;
    }

    <span class="org-comment-delimiter">/* </span><span class="org-comment">Hide the TOCs for non-active posts, but only if JS is enabled</span><span class="org-comment-delimiter"> */</span>
    <span class="org-css-selector">.js .on-this-page &gt; ul &gt; li &gt; ul, .js .on-this-page &gt; ul &gt; li &gt; div</span> { <span class="org-css-property">display</span>: none }
    <span class="org-css-selector">.on-this-page &gt; ul &gt; li.post-active &gt; ul, .on-this-page &gt; ul &gt; li.post-active &gt; div</span> { <span class="org-css-property">display</span>: block }

    <span class="org-css-selector">.active</span> { <span class="org-css-property">background-color</span>: var(<span class="org-variable-name">&#45;&#45;modus-bg-tab-bar</span>) }

    <span class="org-css-selector">.sticky-toc svg .active rect</span> {
        <span class="org-css-property">fill</span>: var(<span class="org-variable-name">&#45;&#45;modus-bg-tab-bar</span>) <span class="org-builtin">!important</span>;
        <span class="org-css-property">fill-opacity</span>: 1 <span class="org-builtin">!important</span>;
        <span class="org-css-property">mix-blend-mode</span>: darken;
        <span class="org-css-property">stroke-dash-array</span>: unset <span class="org-builtin">!important</span>;
        <span class="org-css-property">stroke-width</span>: 4px;
    }

    <span class="org-css-selector">.link-to-nonsticky-toc</span> {
        <span class="org-css-property">display</span>: none
    }
}


</pre>
</div>




</details>

<p></p>

<p>
I also have some Javascript to highlight the active post and show the subheadings for it in <a href="https://github.com/sachac/eleventy-blog-setup/blob/master/assets/js/misc.js">assets/js/misc.js</a>.
</p>

<p>
</p><details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;">
                  <summary><strong>Javascript</strong></summary>

<div class="org-src-container">
<pre class="src src-js"><span class="org-comment-delimiter">/* </span><span class="org-comment">Table of contents</span><span class="org-comment-delimiter"> */</span>

<span class="org-keyword">function</span> <span class="org-function-name">stickyTocAfterScrolling</span>() {
  <span class="org-keyword">const</span> <span class="org-variable-name">elements</span> = document.querySelectorAll(<span class="org-string">'.single-post .sticky-toc-after-scrolling'</span>);
  <span class="org-keyword">let</span> <span class="org-variable-name">lastScroll</span> = window.scrollY;

  elements.forEach(element =&gt; {
    <span class="org-keyword">const</span> <span class="org-variable-name">clone</span> = element.cloneNode(<span class="org-constant">true</span>);
    clone.setAttribute(<span class="org-string">'class'</span>, <span class="org-string">'sticky-toc'</span>);
    clone.querySelector(<span class="org-string">'.panzoom'</span>)?.classList.remove(<span class="org-string">'panzoom'</span>);
    element.parentNode.insertBefore(clone, element.nextSibling);
  });

  <span class="org-keyword">const</span> <span class="org-variable-name">observer</span> = <span class="org-keyword">new</span> <span class="org-type">IntersectionObserver</span>(
    (entries) =&gt; {
      <span class="org-keyword">const</span> <span class="org-variable-name">currentScroll</span> = window.scrollY;
      <span class="org-keyword">const</span> <span class="org-variable-name">scrollingDown</span> = currentScroll &gt; lastScroll;
      lastScroll = currentScroll;

      entries.forEach(entry =&gt; {
        <span class="org-keyword">const</span> <span class="org-variable-name">element</span> = entry.target;
        <span class="org-keyword">const</span> <span class="org-variable-name">clone</span> = cloneMap.get(element);

        <span class="org-keyword">if</span> (!entry.isIntersecting &amp;&amp; scrollingDown) {
          clone.setAttribute(<span class="org-string">'class'</span>, <span class="org-string">'sticky-toc'</span>);
          clone.style.display = <span class="org-string">'block'</span>;
        } <span class="org-keyword">else</span> <span class="org-keyword">if</span> (entry.isIntersecting &amp;&amp; !scrollingDown) {
          element.style.visibility = <span class="org-string">'visible'</span>;
          clone.style.display = <span class="org-string">'none'</span>;
        }
      });
    },
    {
      root: <span class="org-constant">null</span>,
      threshold: 0,
      rootMargin: <span class="org-string">'-10px 0px 0px 0px'</span>
    }
  );

  elements.forEach(element =&gt; {
    observer.observe(element);
  });

  window.addEventListener(<span class="org-string">'resize'</span>, () =&gt; {
    elements.forEach(element =&gt; {
      <span class="org-keyword">const</span> <span class="org-variable-name">clone</span> = cloneMap.get(element);
      <span class="org-keyword">if</span> (clone.style.display != <span class="org-string">'none'</span>) {
        <span class="org-comment-delimiter">// </span><span class="org-comment">reset didn't seem to work</span>
        svgPanZoom(clone.querySelector(<span class="org-string">'svg'</span>)).destroy();
        addPanZoomToElement(clone.querySelector(<span class="org-string">'svg'</span>));
      }
    });
  }, { passive: <span class="org-constant">true</span> });
}

stickyTocAfterScrolling();

<span class="org-keyword">function</span> <span class="org-function-name">scrollToActiveTocLink</span>() {
  <span class="org-keyword">const</span> <span class="org-variable-name">activeLink</span> = document.querySelector(<span class="org-string">'.sticky-toc .active'</span>);
  <span class="org-keyword">const</span> <span class="org-variable-name">tocContainer</span> = document.querySelector(<span class="org-string">'.sticky-toc'</span>);
  <span class="org-keyword">if</span> (!activeLink || !tocContainer) <span class="org-keyword">return</span>;
  <span class="org-keyword">const</span> <span class="org-variable-name">tocRect</span> = tocContainer.getBoundingClientRect();
  <span class="org-keyword">const</span> <span class="org-variable-name">linkRect</span> = activeLink.getBoundingClientRect();
  <span class="org-keyword">if</span> (linkRect.top &lt; tocRect.top || linkRect.bottom &gt; tocRect.bottom) {
    <span class="org-keyword">const</span> <span class="org-variable-name">scrollPosition</span> = linkRect.top + tocContainer.scrollTop -
                          (tocRect.height / 2) + (linkRect.height / 2);
    tocContainer.scrollTo({
      top: scrollPosition,
      behavior: <span class="org-string">'smooth'</span>
    });
  }
}
<span class="org-keyword">function</span> <span class="org-function-name">getVisibleArticle</span>() {
  <span class="org-keyword">const</span> <span class="org-variable-name">viewportHeight</span> = window.innerHeight || document.documentElement.clientHeight;
  <span class="org-keyword">return</span> [...document.querySelectorAll(<span class="org-string">'article'</span>)].find((article) =&gt; {
    <span class="org-keyword">const</span> <span class="org-variable-name">rect</span> = article.getBoundingClientRect();
    <span class="org-keyword">const</span> <span class="org-variable-name">visibleTop</span> = Math.max(0, rect.top);
    <span class="org-keyword">const</span> <span class="org-variable-name">visibleBottom</span> = Math.min(viewportHeight, rect.bottom);
    <span class="org-keyword">const</span> <span class="org-variable-name">visibleHeight</span> = Math.max(0, visibleBottom - visibleTop);
    <span class="org-keyword">return</span> visibleHeight &gt; 0; <span class="org-comment-delimiter">// </span><span class="org-comment">find the first visible one</span>
  });
}

<span class="org-keyword">function</span> <span class="org-function-name">handleActiveTOCLink</span>() {
  <span class="org-keyword">const</span> <span class="org-variable-name">updateActive</span> = <span class="org-keyword">function</span>(<span class="org-variable-name">links</span>, <span class="org-variable-name">active</span>) {
    <span class="org-keyword">const</span> <span class="org-variable-name">activeFragment</span> = active.includes(<span class="org-string">'#'</span>) ?
          active.substring(active.indexOf(<span class="org-string">'#'</span>)) : <span class="org-string">''</span>;
    links.forEach(link =&gt; {
      <span class="org-keyword">const</span> <span class="org-variable-name">href</span> = link.getAttribute(<span class="org-string">'href'</span>);
      <span class="org-keyword">if</span> (href.includes(window.location.origin)) {
        link.classList.toggle(<span class="org-string">'active'</span>, href == active)
      } <span class="org-keyword">else</span> <span class="org-keyword">if</span> (href.startsWith(<span class="org-string">'#'</span>)) {
        link.classList.toggle(<span class="org-string">'active'</span>, href == activeFragment);
      }
    });
  };
  <span class="org-keyword">const</span> <span class="org-variable-name">posts</span> = document.querySelectorAll(<span class="org-string">'.post'</span>);
  <span class="org-keyword">const</span> <span class="org-variable-name">tocLinks</span> = document.querySelectorAll(<span class="org-string">'.on-this-page .toc-link'</span>);
  <span class="org-keyword">const</span> <span class="org-variable-name">options</span> = {
    root: <span class="org-constant">null</span>,
    rootMargin: <span class="org-string">'-20% 0px -70% 0px'</span>,
    threshold: 0
  };
  <span class="org-keyword">const</span> <span class="org-variable-name">observer</span> = <span class="org-keyword">new</span> <span class="org-type">IntersectionObserver</span>((entries) =&gt; {
    entries.forEach(entry =&gt; {
      <span class="org-keyword">if</span> (entry.isIntersecting) {
        <span class="org-keyword">const</span> <span class="org-variable-name">id</span> = entry.target.id;
        <span class="org-keyword">const</span> <span class="org-variable-name">link</span> = document.querySelector(<span class="org-string">`.toc-link[data-index="${id}"]`</span>);
        document.querySelectorAll(<span class="org-string">'.sticky-toc .active'</span>).forEach((o) =&gt; o.classList.remove(<span class="org-string">'active'</span>));
        document.querySelectorAll(<span class="org-string">'.post-active'</span>).forEach((o) =&gt; o.classList.remove(<span class="org-string">'post-active'</span>));
        <span class="org-keyword">if</span> (link) {
          link.classList.add(<span class="org-string">'active'</span>);
          <span class="org-keyword">const</span> <span class="org-variable-name">item</span> = link.closest(<span class="org-string">'li'</span>);
          item.classList.add(<span class="org-string">'post-active'</span>);
        }
      }
    });
    scrollToActiveTocLink();
  }, options);
  posts.forEach((post) =&gt; { observer.observe(post); });

  <span class="org-keyword">const</span> <span class="org-variable-name">stickyTocLinks</span> = document.querySelectorAll(<span class="org-string">'article .sticky-toc a, .on-this-page a'</span>);
  <span class="org-keyword">const</span> <span class="org-variable-name">postTocObserver</span> = <span class="org-keyword">new</span> <span class="org-type">IntersectionObserver</span>((entries) =&gt; {
    entries.forEach(entry =&gt; {
      <span class="org-keyword">if</span> (entry.isIntersecting) {
        <span class="org-keyword">const</span> <span class="org-variable-name">id</span> = entry.target.id;
        <span class="org-keyword">const</span> <span class="org-variable-name">url</span> = window.location.origin + window.location.pathname + <span class="org-string">'#'</span> + id.replace(<span class="org-string">/^outline-container-/</span>, <span class="org-string">''</span>);
        updateActive(stickyTocLinks, url);
      }
    });
    scrollToActiveTocLink();
  }, options);

  document.querySelectorAll(<span class="org-string">'article .sticky-toc, article .sticky-toc-after-scrolling'</span>).forEach((toc) =&gt; {
    <span class="org-keyword">const</span> <span class="org-variable-name">post</span> = toc.closest(<span class="org-string">'article'</span>);
    <span class="org-keyword">if</span> (post) {
      post.querySelectorAll(<span class="org-string">'.outline-2, .outline-3'</span>).forEach((section) =&gt; { postTocObserver.observe(section) });
    }
  });
  <span class="org-keyword">const</span> <span class="org-variable-name">visible</span> = getVisibleArticle();
  <span class="org-keyword">const</span> <span class="org-variable-name">id</span> = visible?.id;
  <span class="org-keyword">if</span> (id) {
    <span class="org-keyword">const</span> <span class="org-variable-name">activeLink</span> = document.querySelector(<span class="org-string">`.toc-link[data-index="${id}"]`</span>);
    <span class="org-keyword">if</span> (activeLink) {
      activeLink.classList.add(<span class="org-string">'active'</span>);
      activeLink.closest(<span class="org-string">'li'</span>).classList.add(<span class="org-string">'post-active'</span>);
      scrollToActiveTocLink();
    }
  }
}
handleActiveTOCLink();

</pre>
</div>




</details>

<p></p>
</div>
</div>
<div id="outline-container-adding-subheadings-and-sketches-to-my-blog-page-navigation-i-can-use-a-sketch-as-a-map-too" class="outline-3">
<h3 id="adding-subheadings-and-sketches-to-my-blog-page-navigation-i-can-use-a-sketch-as-a-map-too">I can use a sketch as a map, too</h3>
<div class="outline-text-3" id="text-adding-subheadings-and-sketches-to-my-blog-page-navigation-i-can-use-a-sketch-as-a-map-too">
<p>
I sometimes want to use sketchnotes as overviews,
especially if I've added <a href="https://sachachua.com/blog/2025/01/hyperlinking-svgs/">hyperlinks to them</a>. I
used to make the images show up on the right side,
but now I want them to show up in the left-side
navigation instead. Also, I wanted any links to
headings to automatically get recoloured as I
scroll to that heading.
</p>


<figure id="orga78f37e">
<img src="https://sachachua.com/blog/2025/04/adding-subheadings-and-sketches-to-my-blog-page-navigation/2025-04-02-scrolling-svg.gif" alt="2025-04-02-scrolling-svg.gif">

<figcaption><span class="figure-number">Figure 2: </span>Animated GIF showing how the SVG highlights change as you scroll down</figcaption>
</figure>

<p>
I added a special case to the
<code>handleActiveTOCLink</code> function to handle anchor
hyperlinks (just <code>#anchor</code>) in the SVG. It
probably makes sense to make those absolute URLs,
which means slightly changing my workflows for
hyperlinking SVGs and writing about sketches.
</p>

<p>
So on both the category page (ex: the Hyperlinking
SVGs entry in <a href="https://sachachua.com/blog/category/drawing/">category - drawing</a>, which might have
moved off the first page of results if you're
reading this far in the future) and the
single-post page (ex: <a href="https://sachachua.com/blog/2025/01/hyperlinking-svgs/">Hyperlinking SVGs</a>), there's
a full-sized version of the image in the main blog
post, and then a small copy of it in the margin on
the left. The sidebar copy is probably too small
to read, but it might be enough to get a sense of
spatial relationships, and the links also have
<code>title</code> attributes that are displayed as tooltips
when you hover.
</p>


<figure id="orgc5263b9">
<img src="https://sachachua.com/blog/2025/04/adding-subheadings-and-sketches-to-my-blog-page-navigation/2025-04-03_14-28-48.png" alt="2025-04-03_14-28-48.png">

<figcaption><span class="figure-number">Figure 3: </span>Screenshot of small image in sidebar on the single post page</figcaption>
</figure>

<p>
I use Javascript to duplicate the image and make a
small, sticky version because I haven't quite
figured out how to properly make it sticky when
off-screen with just CSS. Even my JS feels a
little tangled. Maybe this would be a good excuse
to learn about web components; someone's probably
figured out something polished.
</p>

<p>
I'm curious about using more drawings to anchor my
thinking and structure my blog posts.
</p>
</div>
</div>
<div id="outline-container-adding-subheadings-and-sketches-to-my-blog-page-navigation-progressive-enhancement" class="outline-3">
<h3 id="adding-subheadings-and-sketches-to-my-blog-page-navigation-progressive-enhancement">Progressive enhancement</h3>
<div class="outline-text-3" id="text-adding-subheadings-and-sketches-to-my-blog-page-navigation-progressive-enhancement">
<p>
Some people read my blog using <a href="https://www.gnu.org/software/emacs/manual/html_mono/eww.html">EWW</a> (the Emacs Web
Wowser, of course), so I want my blog to be
reasonable even without CSS and JS.
</p>

<p>
A number of people read my blog without Javascript
enabled. I installed the <a href="https://addons.mozilla.org/en-US/firefox/addon/script-switch/">Firefox extension Script
Switch</a> so that I can test my blog with and without
Javascript whenever I remember.
</p>

<p>
I sometimes look up my blog posts on my phone and
there's no space for any of this fanciness there,
so it'll only kick in on large screens. My CSS
file is littered with various breakpoints I've
cargo-culted over the years and I should simplify
it at some point. At the moment, if it looks fine
on my Lenovo P52, I'm happy.
</p>
</div>
</div>
<div id="outline-container-adding-subheadings-and-sketches-to-my-blog-page-navigation-other-ideas-and-next-steps" class="outline-3">
<h3 id="adding-subheadings-and-sketches-to-my-blog-page-navigation-other-ideas-and-next-steps">Other ideas and next steps</h3>
<div class="outline-text-3" id="text-adding-subheadings-and-sketches-to-my-blog-page-navigation-other-ideas-and-next-steps">
<p>
Theoretically, the right margin is now available
for <a href="https://gwern.net/sidenote">sidenotes</a>, so I might be able to look at
<a href="https://github.com/ox-tufte/ox-tufte">ox-tufte</a> and <a href="https://eleventufte.netlify.app/features/">Eleventufte</a> and get something going.
Then I'll have a way to add small notes that are
shorter than a paragraph. Longer tangents can go
in a details/summary element instead, although I
have it on good authority that <a href="https://maggieappleton.com/narrative-essays/#:~:text=He%20taught%20me%20it%20is%20100%25%20okay%20to%20write%20an%20entire%20side%2Dnovel%20in%20your%20footnotes%20if%20you%20need%20to.">one can write at
length in footnotes</a>. I love the footnotes in the
Bartimaeus series, and apparently there are quite
a few books where the <a href="https://reactormag.com/for-the-love-of-footnotes-when-fantasy-gets-extra-nerdy/">footnotes are part of the
storytelling</a>.)
</p>

<p>
It might be nice to let tables extend into the
right sidebar when I know I won't have a doodle
nearby. Incidentally, <a href="https://gwern.net/sidenote">Sidenotes In Web Design ·
Gwern.net</a> uses breadcrumbs in the left sidebar
instead of a table of contents, so there's more
space for tables and sidenotes.
</p>

<p>
I thought about using CSS breakpoints so that on a
medium-sized screen, we can have the left sidebar
even if there's no space for something on the
right. I haven't gotten around to experimenting
with it yet, though. Besides, I don't know yet if
I want to prioritize the stuff I want in the right
sidebar (side notes, doodles) over fairly-static
navigation.
</p>

<p>
As I mentioned, it might be handy to tweak my SVG
linking workflow to use absolute URLs.
</p>

<p>
Sometimes I look up my notes within Emacs, but
surprisingly often, I look them up on the Web.
Navigation isn't just cosmetic. I want to get
better at using my blog as a tool for thought, so
tinkering with layout isn't just window dressing.
It's (very slowly) experimenting with scaffolding
for my brain. Little things can help!</p>
</div>
</div>
<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2025%2F04%2Fadding-subheadings-and-sketches-to-my-blog-page-navigation%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">Using Emacs Lisp to batch-demote HTML headings for my static site</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2025/04/using-emacs-lisp-to-batch-demote-html-headings-for-my-static-site/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2025-04-03T17:23:43Z</updated>
    <published>2025-04-03T17:23:43Z</published>
    <category term="blogging" />
<category term="11ty" />
<category term="emacs" />
		<id>https://sachachua.com/blog/2025/04/using-emacs-lisp-to-batch-demote-html-headings-for-my-static-site/</id>
		<content type="html"><![CDATA[<div class="assumed_audience" id="orgb8392f2">
<p>
Assumed audience: People who have lots of HTML
files used as input for a static site generator,
might need to do a batch operation on them, and
are open to doing that with Emacs Lisp. Which
might just be me, but who knows? =)
</p>

</div>

<p>
HTML defines a hierarchy of headings going from
<code>&lt;h1&gt;</code> to <code>&lt;h6&gt;</code>, which comes in especially handy
when people are <a href="https://usability.yale.edu/web-accessibility/articles/headings">navigating with a screenreader</a> or
<a href="https://github.com/alphapapa/org-web-tools">converting web pages to Org Mode</a>. I think search
engines might use them to get a sense of the
page's structure, too. On my blog, the hierarchy
usually goes like this:
</p>

<ul class="org-ul">
<li><code>&lt;h1&gt;</code>: site title,</li>
<li><code>&lt;h2&gt;</code>: blog post titles, since I put multiple
blog posts on the <a href="https://sachachua.com/blog/">main page</a> and category pages
(ex: <a href="https://sachachua.com/blog/category/blogging">blogging</a>)</li>
<li><code>&lt;h3&gt;</code>: blog post's subheadings, if any</li>
<li><code>&lt;h4&gt;</code>: I rarely need subsubheadings in my main blog posts, but they're there just in case</li>
</ul>

<p>
While fiddling with my blog's CSS so that I could
try this <a href="https://www.fluid-type-scale.com/calculate?minFontSize=16&amp;minWidth=400&amp;minRatio=1.25&amp;maxFontSize=22&amp;maxWidth=1280&amp;maxRatio=1.333&amp;steps=sm%2Cbase%2Cmd%2Clg%2Cxl%2Cxxl%2Cxxxl&amp;baseStep=base&amp;prefix=fs&amp;useContainerWidth=false&amp;includeFallbacks=true&amp;useRems=true&amp;remValue=16&amp;decimals=2&amp;previewFont=Inter&amp;previewText=Almost+before+we+knew+it%2C+we+had+left+the+ground&amp;previewWidth=1280">fluid type scale</a>, I realized that the
subheadings in my exported blog entries started at
<code>&lt;h2&gt;</code> instead of <code>&lt;h3&gt;</code>. This meant that the outline was this:
</p>

<ul class="org-ul">
<li>Site title
<ul class="org-ul">
<li>Blog post 1</li>
<li>Subheading 1</li>
<li>Subheading 2</li>
<li>Blog post 2</li>
<li>Subheading 1</li>
<li>Subheading 2</li>
<li>Blog post 3</li>
</ul></li>
</ul>

<p>
I wanted the outline to be this:
</p>

<ul class="org-ul">
<li>Site title
<ul class="org-ul">
<li>Blog post 1
<ul class="org-ul">
<li>Subheading 1</li>
<li>Subheading 2</li>
</ul></li>
<li>Blog post 2
<ul class="org-ul">
<li>Subheading 1</li>
<li>Subheading 2</li>
</ul></li>
<li>Blog post 3</li>
</ul></li>
</ul>

<p>
This was because I hadn't changed
<code>org-html-toplevel-hlevel</code> during my 11ty export
process. To solve this for new posts, I added a
new option <code>org-11ty-toplevel-hlevel</code> that
defaults to 3 in <a href="https://github.com/sachac/ox-11ty/blob/master/ox-11ty.el">ox-11ty.el</a>, re-exported
one of my long blog posts to test it, and
confirmed that my headings now started at <code>&lt;h3&gt;</code>.
</p>

<p>
I still had all my old HTML files with the wrong
levels of headings. I wrote some Emacs Lisp to
shift the headings downwards (h5 to h6, h4 to h5,
h3 to h4, h2 to h3) in a file if it had an <code>&lt;h2&gt;</code>
in it. Regular expressions are usually not a good
idea when it comes to HTML because there might be
exceptions, but I figured it was a pretty small
and low-risk change, so I decided not to use the
full XML/DOM parsing functions. I saved all the
blog posts under version control just in case I
messed things up. Here's my function:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-html-shift-headings</span> (filename)
  <span class="org-doc">"Shift heading tags in FILENAME."</span>
  (<span class="org-keyword">interactive</span> <span class="org-string">"FFile: "</span>)
  (<span class="org-keyword">let</span> ((case-fold-search t)) <span class="org-comment-delimiter">; </span><span class="org-comment">make the search case-insensitive</span>
    (<span class="org-keyword">with-temp-buffer</span>
      (insert-file-contents filename)
      (goto-char (point-min))
      <span class="org-comment-delimiter">;; </span><span class="org-comment">Only modify the files where we have an h2</span>
      (<span class="org-keyword">when</span> (<span class="org-keyword">or</span> (search-forward <span class="org-string">"&lt;h2"</span> nil t)
                (search-forward <span class="org-string">"&lt;/h2&gt;"</span> nil t))
        (goto-char (point-min))
        <span class="org-comment-delimiter">;; </span><span class="org-comment">Handle both opening and closing tags</span>
        (<span class="org-keyword">while</span> (re-search-forward <span class="org-string">"&lt;</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string">/</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string">?h</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string">[2-5]</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string">\\&gt;"</span> nil t)
          (<span class="org-keyword">let*</span> ((closing-tag (match-string 1))
                 (heading-level (string-to-number (match-string 2)))
                 (new-level (1+ heading-level)))
            (replace-match (concat <span class="org-string">"&lt;"</span> closing-tag <span class="org-string">"h"</span> (number-to-string new-level)))))
        (write-file filename)
        filename))))
</pre>
</div>


<p>
Running it on all the source HTML files in
specific subdirectories was easy with
<code>directory-files-recursively</code>.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">dolist</span> (dir <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"~/proj/static-blog/blog"</span>
               <span class="org-string">"~/proj/static-blog/content"</span>))
  (mapc <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">my-html-shift-headings</span>
        (directory-files-recursively
         dir
         <span class="org-string">"\\.html\\'"</span>)))
</pre>
</div>


<p>
Then I could just rebuild my blog and get all the
right heading levels. Spot-checks with Inspect
Element show that the headings now have the right
tags, and <code>org-web-tools-read-url-as-org</code> now
picks up the right hierarchy for the page.
</p>

<p>
Correcting the input files was easier and more
efficient than modifying my 11ty template engine
to shift the heading levels whenever I build my
site (probably by defining a <a href="https://www.11ty.dev/docs/config-preprocessors/">preprocessor</a>). I
could've written a NodeJS script to do that kind
of file manipulation, but writing it in Emacs Lisp
matched how I might think of doing it
interactively. Using Emacs Lisp was also easy to
test on one or two files, check the list of files
matched by <code>directory-files-recursively</code>, and then
run it on everything.
</p>

<p>
Going forward, the new <code>org-11ty-toplevel-hlevel</code>
variable should properly modify the behaviour of
Org's HTML export to get the headings at the right
level. We'll see!</p>
<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2025%2F04%2Fusing-emacs-lisp-to-batch-demote-html-headings-for-my-static-site%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">Moving 18 years of comments out of Disqus and into my 11ty static site</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2025/03/moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2025-03-30T13:11:08Z</updated>
    <published>2025-03-30T13:11:08Z</published>
    <category term="11ty" />
<category term="blogging" />
		<id>https://sachachua.com/blog/2025/03/moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site/</id>
		<content type="html"><![CDATA[<div class="sticky-toc" id="org78e19a3">
<div id="text-table-of-contents" role="doc-toc">
<ul>
<li><a href="https://sachachua.com/blog/feed/atom/index.xml#moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site-exploring-my-disqus-xml-with-xq-org-babel-and-seaborn">Exploring my disqus.xml with xq, Org Babel, and seaborn</a></li>
<li><a href="https://sachachua.com/blog/feed/atom/index.xml#moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site-deleting-spam-comments-via-the-disqus-web-interface-and-spookfox">Deleting spam comments via the Disqus web interface and Spookfox</a></li>
<li><a href="https://sachachua.com/blog/feed/atom/index.xml#moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site-modifying-eleventy-import-disqus-for-my-site">Modifying eleventy-import-disqus for my site</a></li>
<li><a href="https://sachachua.com/blog/feed/atom/index.xml#moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site-next-steps">Next steps</a></li>
</ul>
</div>

</div>

<div class="assumed_audience" id="orgdecdaa5">
<p>
<a href="https://maggieappleton.com/assumed-audience/">Assumed audience</a>: Technical bloggers who like:
</p>
<ul class="org-ul">
<li>static site generators: this post is about moving more things into my SSG</li>
<li>XML: check out the mention of xq, which offers a jq-like interface</li>
<li>or Org Mode: some notes here about Org Babel source blocks and graphing</li>
</ul>

</div>

<p>
I've been thinking of getting rid of the <a href="https://disqus.com/">Disqus</a>
blog commenting system for a while. I used to use
it in the hopes that it would handle spam
filtering and the "someone has replied to your
comment" notification for me. Getting rid of
Disqus means one less thing that needs Javascript,
one less thing that tracks people in ways we don't
want, one less thing that shows ads and wants to
sell our attention. Comments are rare enough these
days, so I think I can handle e-mailing people
when there are replies.
</p>

<p>
There are plenty of alternative commenting systems
to choose from. <a href="https://gitlab.com/comentario/comentario">Comentario</a> and <a href="https://isso-comments.de/">Isso</a> are
self-hosted, while <a href="https://commento.io/pricing">Commento</a> (USD 10/month) and
<a href="https://talk.hyvor.com/pricing">Hyvor Talk</a> (12 euro/month) are services.
<a href="https://utteranc.es/">Utterances</a> uses Github issues, which is probably
not something I'll try as quite a few people in
the Emacs community are philosophically opposed to
Github. Along those lines, if I can find something
that works without Javascript, that would be even
better.
</p>

<p>
I could spend a few years trying to figure out
which system I might like in terms of user
interface, integration, and spam-filtering, but
for now, I want to:
</p>
<ul class="org-ul">
<li>remove Disqus</li>
<li>keep the comments, since they add a lot to the page (ex: the conversation on <a href="https://sachachua.com/blog/2021/01/a-list-of-sharks-that-are-obligate-ram-ventilators/">A list of sharks that are obligate ram ventilators</a>)</li>
</ul>

<p>
Fortunately, there's <a href="https://github.com/11ty/eleventy-import-disqus">11ty/eleventy-import-disqus</a> (<a href="https://www.zachleat.com/web/disqus-import/">see zachleat's blog post: Import your Disqus Comments to Eleventy</a>)
</p>
<div id="outline-container-moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site-exploring-my-disqus-xml-with-xq-org-babel-and-seaborn" class="outline-2">
<h3 id="moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site-exploring-my-disqus-xml-with-xq-org-babel-and-seaborn">Exploring my disqus.xml with xq, Org Babel, and seaborn</h3>
<div class="outline-text-2" id="text-moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site-exploring-my-disqus-xml-with-xq-org-babel-and-seaborn">
<p>
One challenge: there are a <i>lot</i> of comments. How
many? I got curious about analyzing the XML, and
then of course I wanted to do that from Emacs. I
used <code>pipx install yq</code> to install <a href="https://github.com/mikefarah/yq">yq</a> so that I
could use the xq tool to <a href="https://www.ashbyhq.com/blog/engineering/jq-and-yq">query the XML</a>, much like
<a href="https://jqlang.org/">jq</a> works.
</p>

<p>
My uncompressed Disqus XML export was 28MB. I spent
some time deleting spam comments through the web
interface, which helped with the filtering. I also
deleted some more comments from the XML file as I
noticed them. I needed to change <code>/wp/</code> to <code>/blog/</code>, too.
</p>

<p>
This is how I analyzed the archive for non-deleted
posts, uniquified based on message. I'll include
the full Org source of that block (including the
header lines) in my blog post so that you can see
how I call it later.
</p>


<div class="org-src-container">
<pre class="src src-org"><span class="org-org-meta-line">#+NAME: analyze-disqus</span>
<span class="org-org-block-begin-line">#+begin_src shell :var rest="| length | \"\\(.) unique comments\"" :exports results</span>
<span class="org-org-block">~/.local/bin/xq -r </span><span class="org-org-block"><span class="org-string">"[.disqus.post[] |</span></span>
<span class="org-org-block"><span class="org-string">   select(.isDeleted != \"true\" and .message) |</span></span>
<span class="org-org-block"><span class="org-string">   {key: .message, value: .}] |</span></span>
<span class="org-org-block"><span class="org-string">  map(.value) |</span></span>
<span class="org-org-block"><span class="org-string">  unique_by(.message) ${rest}"</span></span><span class="org-org-block"> &lt; disqus.xml</span>
<span class="org-org-block-end-line">#+end_src</span>
</pre>
</div>


<p>
When I evaluate that with <code>C-c C-c</code>, I get:
</p>

<p>
8265 unique comments
</p>

<p>
I was curious about how it broke down by year. Because I named the source code block and used a variable to specify how to process the filtered results earlier, I can call that with a different value.
</p>

<p>
Here's the call in my Org Mode source:
</p>


<div class="org-src-container">
<pre class="src src-org"><span class="org-org-meta-line">#+CALL: analyze-disqus(rest="| map(.createdAt[0:4]) | group_by(.) | map([(.[0]), length]) | reverse | [\"Year\", \"Count\"], .[] | @csv") :results table output :wrap my_details Table of comment count by year</span>
</pre>
</div>


<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;">
                  <summary><strong>Table of comment count by year</strong></summary>
<table>


<colgroup>
<col class="org-right">

<col class="org-right">
</colgroup>
<tbody>
<tr>
<td class="org-right">Year</td>
<td class="org-right">Count</td>
</tr>

<tr>
<td class="org-right">2025</td>
<td class="org-right">26</td>
</tr>

<tr>
<td class="org-right">2024</td>
<td class="org-right">43</td>
</tr>

<tr>
<td class="org-right">2023</td>
<td class="org-right">34</td>
</tr>

<tr>
<td class="org-right">2022</td>
<td class="org-right">40</td>
</tr>

<tr>
<td class="org-right">2021</td>
<td class="org-right">55</td>
</tr>

<tr>
<td class="org-right">2020</td>
<td class="org-right">131</td>
</tr>

<tr>
<td class="org-right">2019</td>
<td class="org-right">107</td>
</tr>

<tr>
<td class="org-right">2018</td>
<td class="org-right">139</td>
</tr>

<tr>
<td class="org-right">2017</td>
<td class="org-right">186</td>
</tr>

<tr>
<td class="org-right">2016</td>
<td class="org-right">196</td>
</tr>

<tr>
<td class="org-right">2015</td>
<td class="org-right">593</td>
</tr>

<tr>
<td class="org-right">2014</td>
<td class="org-right">740</td>
</tr>

<tr>
<td class="org-right">2013</td>
<td class="org-right">960</td>
</tr>

<tr>
<td class="org-right">2012</td>
<td class="org-right">784</td>
</tr>

<tr>
<td class="org-right">2011</td>
<td class="org-right">924</td>
</tr>

<tr>
<td class="org-right">2010</td>
<td class="org-right">966</td>
</tr>

<tr>
<td class="org-right">2009</td>
<td class="org-right">1173</td>
</tr>

<tr>
<td class="org-right">2008</td>
<td class="org-right">1070</td>
</tr>

<tr>
<td class="org-right">2007</td>
<td class="org-right">98</td>
</tr>
</tbody>
</table>


</details>

<p>
I tried fiddling around with <a href="https://orgmode.org/manual/Org-Plot.html">Org's #+PLOT keyword</a>,
but I couldn't figure out how to get the bar graph
the way I wanted it to be. Someday, if I ever
figure that out, I'll definitely save the <a href="http://yummymelon.com/devnull/beautifying-org-plot-with-yasnippet-and-context-menus.html">Gnuplot
setup as a snippet</a>. For now, I visualized it using
<a href="https://seaborn.pydata.org/">seaborn</a> instead.
</p>

<details><summary>Code for graphing comments by year</summary>
<div class="org-src-container">
<pre class="src src-python"><span class="org-keyword">import</span> pandas <span class="org-keyword">as</span> pd
<span class="org-keyword">import</span> seaborn <span class="org-keyword">as</span> sns
<span class="org-keyword">import</span> matplotlib.pyplot <span class="org-keyword">as</span> plt
<span class="org-keyword">import</span> numpy <span class="org-keyword">as</span> np

<span class="org-variable-name">df</span> <span class="org-operator">=</span> pd.DataFrame(data[1:], columns<span class="org-operator">=</span>data[0])
<span class="org-variable-name">df</span>[<span class="org-string">'Count'</span>] <span class="org-operator">=</span> df[<span class="org-string">'Count'</span>].astype(<span class="org-builtin">int</span>)
<span class="org-variable-name">df</span>[<span class="org-string">'Year'</span>] <span class="org-operator">=</span> df[<span class="org-string">'Year'</span>].astype(<span class="org-builtin">int</span>)
<span class="org-variable-name">df</span> <span class="org-operator">=</span> df.sort_values(<span class="org-string">'Year'</span>)
plt.figure(figsize<span class="org-operator">=</span>(12, 6))
<span class="org-variable-name">ax</span> <span class="org-operator">=</span> sns.barplot(x<span class="org-operator">=</span><span class="org-string">'Year'</span>, y<span class="org-operator">=</span><span class="org-string">'Count'</span>, data<span class="org-operator">=</span>df)
plt.title(<span class="org-string">'Comments by Year (2007-2025)'</span>, fontsize<span class="org-operator">=</span>16, fontweight<span class="org-operator">=</span><span class="org-string">'bold'</span>)
plt.xlabel(<span class="org-string">'Year'</span>)
plt.ylabel(<span class="org-string">'Comments'</span>)
plt.xticks(rotation<span class="org-operator">=</span>45)
plt.grid(axis<span class="org-operator">=</span><span class="org-string">'y'</span>)
<span class="org-keyword">for</span> i, v <span class="org-keyword">in</span> <span class="org-builtin">enumerate</span>(df[<span class="org-string">'Count'</span>]):
<span class="org-highlight-indentation"> </span>   ax.text(i, v <span class="org-operator">+</span> 20, <span class="org-builtin">str</span>(v), ha<span class="org-operator">=</span><span class="org-string">'center'</span>, fontsize<span class="org-operator">=</span>9)
plt.tight_layout()
plt.savefig(<span class="org-string">'year_count_plot.svg'</span>)
<span class="org-keyword">return</span> <span class="org-string">'year_count_plot.svg'</span>
</pre>
</div>

</details>


<figure id="org7651eec">
<img src="https://sachachua.com/blog/2025/03/moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site/year_count_plot.svg" alt="year_count_plot.svg" class="org-svg" data-link="t">

</figure>

<p>
Ooooooh, I can probably cross-reference this with
the number of posts from my <a href="https://sachachua.com/blog/all/index.json">/blog/all/index.json</a> file.
I used <a href="https://claude.ai">Claude AI</a>'s help to come up with the code below, since merging data and plotting them nicely is still challenging for me. Now that I have the example, though, maybe I can do other graphs more easily. (This looks like a related tutorial on <a href="https://www.geeksforgeeks.org/combining-barplots-and-lineplots-with-different-y-axes-a-technical-guide/">combining barplots and lineplots</a>.)
</p>

<details><summary>Code for graphing</summary>
<div class="org-src-container">
<pre class="src src-python"><span class="org-keyword">import</span> pandas <span class="org-keyword">as</span> pd
<span class="org-keyword">import</span> seaborn <span class="org-keyword">as</span> sns
<span class="org-keyword">import</span> matplotlib.pyplot <span class="org-keyword">as</span> plt
<span class="org-keyword">import</span> numpy <span class="org-keyword">as</span> np
<span class="org-keyword">import</span> json
<span class="org-keyword">from</span> matplotlib.ticker <span class="org-keyword">import</span> FuncFormatter
<span class="org-keyword">from</span> datetime <span class="org-keyword">import</span> datetime

<span class="org-keyword">with</span> <span class="org-builtin">open</span>(<span class="org-string">'/home/sacha/proj/static-blog/_site/blog/all/index.json'</span>, <span class="org-string">'r'</span>) <span class="org-keyword">as</span> f:
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">posts_data</span> <span class="org-operator">=</span> json.load(f)

<span class="org-comment-delimiter"># </span><span class="org-comment">Process post data</span>
<span class="org-variable-name">posts_df</span> <span class="org-operator">=</span> pd.DataFrame(posts_data)
<span class="org-variable-name">posts_df</span>[<span class="org-string">'Year'</span>] <span class="org-operator">=</span> pd.to_datetime(posts_df[<span class="org-string">'date'</span>]).dt.year
<span class="org-variable-name">post_counts</span> <span class="org-operator">=</span> posts_df.groupby(<span class="org-string">'Year'</span>).size().reset_index(name<span class="org-operator">=</span><span class="org-string">'post_count'</span>)

<span class="org-comment-delimiter"># </span><span class="org-comment">Convert to DataFrame</span>
<span class="org-variable-name">comments_df</span> <span class="org-operator">=</span> pd.DataFrame(comment_data[1:], columns<span class="org-operator">=</span>comment_data[0])
<span class="org-variable-name">comments_df</span>[<span class="org-string">'Count'</span>] <span class="org-operator">=</span> comments_df[<span class="org-string">'Count'</span>].astype(<span class="org-builtin">int</span>)
<span class="org-variable-name">comments_df</span>[<span class="org-string">'Year'</span>] <span class="org-operator">=</span> comments_df[<span class="org-string">'Year'</span>].astype(<span class="org-builtin">int</span>)

<span class="org-comment-delimiter"># </span><span class="org-comment">Merge the two dataframes</span>
<span class="org-variable-name">merged_df</span> <span class="org-operator">=</span> pd.merge(post_counts, comments_df, on<span class="org-operator">=</span><span class="org-string">'Year'</span>, how<span class="org-operator">=</span><span class="org-string">'outer'</span>).fillna(0)
<span class="org-variable-name">merged_df</span> <span class="org-operator">=</span> merged_df.sort_values(<span class="org-string">'Year'</span>)

<span class="org-comment-delimiter"># </span><span class="org-comment">Calculate comments per post ratio</span>
<span class="org-variable-name">merged_df</span>[<span class="org-string">'comments_per_post'</span>] <span class="org-operator">=</span> merged_df[<span class="org-string">'Count'</span>] <span class="org-operator">/</span> merged_df[<span class="org-string">'post_count'</span>]
<span class="org-variable-name">merged_df</span>[<span class="org-string">'comments_per_post'</span>] <span class="org-operator">=</span> merged_df[<span class="org-string">'comments_per_post'</span>].replace([np.inf, <span class="org-operator">-</span>np.inf], np.nan).fillna(0)

<span class="org-comment-delimiter"># </span><span class="org-comment">Create a single figure instead of two subplots</span>
<span class="org-variable-name">fig</span>, <span class="org-variable-name">ax1</span> <span class="org-operator">=</span> plt.subplots(figsize<span class="org-operator">=</span>(15, 8))

<span class="org-comment-delimiter"># </span><span class="org-comment">Custom colors</span>
<span class="org-variable-name">post_color</span> <span class="org-operator">=</span> <span class="org-string">"#1f77b4"</span>    <span class="org-comment-delimiter"># </span><span class="org-comment">blue</span>
<span class="org-variable-name">comment_color</span> <span class="org-operator">=</span> <span class="org-string">"#ff7f0e"</span> <span class="org-comment-delimiter"># </span><span class="org-comment">orange</span>
<span class="org-variable-name">ratio_color</span> <span class="org-operator">=</span> <span class="org-string">"#2ca02c"</span>   <span class="org-comment-delimiter"># </span><span class="org-comment">green</span>

<span class="org-comment-delimiter"># </span><span class="org-comment">Setting up x-axis positions</span>
<span class="org-variable-name">x</span> <span class="org-operator">=</span> np.arange(<span class="org-builtin">len</span>(merged_df))
<span class="org-variable-name">width</span> <span class="org-operator">=</span> 0.35

<span class="org-comment-delimiter"># </span><span class="org-comment">Bar charts on first y-axis</span>
<span class="org-variable-name">bars1</span> <span class="org-operator">=</span> ax1.bar(x <span class="org-operator">-</span> width<span class="org-operator">/</span>2, merged_df[<span class="org-string">'post_count'</span>], width, color<span class="org-operator">=</span>post_color, label<span class="org-operator">=</span><span class="org-string">'Posts'</span>)
<span class="org-variable-name">bars2</span> <span class="org-operator">=</span> ax1.bar(x <span class="org-operator">+</span> width<span class="org-operator">/</span>2, merged_df[<span class="org-string">'Count'</span>], width, color<span class="org-operator">=</span>comment_color, label<span class="org-operator">=</span><span class="org-string">'Comments'</span>)
ax1.set_ylabel(<span class="org-string">'Count (Posts &amp; Comments)'</span>, fontsize<span class="org-operator">=</span>12)

<span class="org-comment-delimiter"># </span><span class="org-comment">Add post count values above bars</span>
<span class="org-keyword">for</span> i, bar <span class="org-keyword">in</span> <span class="org-builtin">enumerate</span>(bars1):
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">height</span> <span class="org-operator">=</span> bar.get_height()
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> height <span class="org-operator">&gt;</span> 0:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   ax1.text(bar.get_x() <span class="org-operator">+</span> bar.get_width()<span class="org-operator">/</span>2., height <span class="org-operator">+</span> 5,
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   f<span class="org-string">'</span>{<span class="org-builtin">int</span>(height)}<span class="org-string">'</span>, ha<span class="org-operator">=</span><span class="org-string">'center'</span>, va<span class="org-operator">=</span><span class="org-string">'bottom'</span>, color<span class="org-operator">=</span>post_color, fontsize<span class="org-operator">=</span>9)

<span class="org-comment-delimiter"># </span><span class="org-comment">Add comment count values above bars</span>
<span class="org-keyword">for</span> i, bar <span class="org-keyword">in</span> <span class="org-builtin">enumerate</span>(bars2):
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">height</span> <span class="org-operator">=</span> bar.get_height()
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> height <span class="org-operator">&gt;</span> 20:  <span class="org-comment-delimiter"># </span><span class="org-comment">Only show if there's enough space</span>
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   ax1.text(bar.get_x() <span class="org-operator">+</span> bar.get_width()<span class="org-operator">/</span>2., height <span class="org-operator">+</span> 5,
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   f<span class="org-string">'</span>{<span class="org-builtin">int</span>(height)}<span class="org-string">'</span>, ha<span class="org-operator">=</span><span class="org-string">'center'</span>, va<span class="org-operator">=</span><span class="org-string">'bottom'</span>, color<span class="org-operator">=</span>comment_color, fontsize<span class="org-operator">=</span>9)

<span class="org-comment-delimiter"># </span><span class="org-comment">Line graph on second y-axis</span>
<span class="org-variable-name">ax2</span> <span class="org-operator">=</span> ax1.twinx()
<span class="org-variable-name">line</span> <span class="org-operator">=</span> ax2.plot(x, merged_df[<span class="org-string">'comments_per_post'</span>], marker<span class="org-operator">=</span><span class="org-string">'o'</span>, color<span class="org-operator">=</span>ratio_color,
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span> linewidth<span class="org-operator">=</span>2, label<span class="org-operator">=</span><span class="org-string">'Comments per Post'</span>)
ax2.set_ylabel(<span class="org-string">'Comments per Post'</span>, color<span class="org-operator">=</span>ratio_color, fontsize<span class="org-operator">=</span>12)
ax2.tick_params(axis<span class="org-operator">=</span><span class="org-string">'y'</span>, labelcolor<span class="org-operator">=</span>ratio_color)
ax2.set_ylim(bottom<span class="org-operator">=</span>0)

<span class="org-comment-delimiter"># </span><span class="org-comment">Add ratio values near line points</span>
<span class="org-keyword">for</span> i, ratio <span class="org-keyword">in</span> <span class="org-builtin">enumerate</span>(merged_df[<span class="org-string">'comments_per_post'</span>]):
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> ratio <span class="org-operator">&gt;</span> 0:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   ax2.text(i, ratio <span class="org-operator">+</span> 0.2, f<span class="org-string">'</span>{ratio:.1f}<span class="org-string">'</span>, ha<span class="org-operator">=</span><span class="org-string">'center'</span>, color<span class="org-operator">=</span>ratio_color, fontsize<span class="org-operator">=</span>9)

<span class="org-comment-delimiter"># </span><span class="org-comment">Set x-axis labels</span>
ax1.set_xticks(x)
ax1.set_xticklabels(merged_df[<span class="org-string">'Year'</span>], rotation<span class="org-operator">=</span>45)
ax1.set_title(<span class="org-string">'Blog Posts, Comments, and Comments per Post by Year'</span>, fontsize<span class="org-operator">=</span>16, fontweight<span class="org-operator">=</span><span class="org-string">'bold'</span>)
ax1.grid(axis<span class="org-operator">=</span><span class="org-string">'y'</span>)

<span class="org-comment-delimiter"># </span><span class="org-comment">Add combined legend</span>
<span class="org-variable-name">lines1</span>, <span class="org-variable-name">labels1</span> <span class="org-operator">=</span> ax1.get_legend_handles_labels()
<span class="org-variable-name">lines2</span>, <span class="org-variable-name">labels2</span> <span class="org-operator">=</span> ax2.get_legend_handles_labels()
ax1.legend(lines1 <span class="org-operator">+</span> lines2, labels1 <span class="org-operator">+</span> labels2, loc<span class="org-operator">=</span><span class="org-string">'upper left'</span>)

<span class="org-comment-delimiter"># </span><span class="org-comment">Layout and save</span>
plt.tight_layout()
plt.savefig(<span class="org-string">'posts_comments_analysis.svg'</span>)
<span class="org-keyword">return</span> <span class="org-string">'posts_comments_analysis.svg'</span>
</pre>
</div>

</details>


<figure id="org8363737">
<img src="https://sachachua.com/blog/2025/03/moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site/posts_comments_analysis.svg" alt="posts_comments_analysis.svg" class="org-svg" data-link="t">

</figure>

<p>
Timeline notes:
</p>

<ul class="org-ul">
<li>In this graph, comments are reported by the
timestamp of the comment, not the date of the
post.</li>
<li>In 2007 or so, I moved to Wordpress from
planner-rss.el. I think I eventually imported
those Wordpress comments into Disqus when I got
annoyed with Wordpress comments (Akismet?
notifications?).</li>
<li>In 2008 and 2009, I was working on enterprise
social computing at IBM. I made a few
presentations that were popular. Also, mentors
and colleagues posted lots of comments.</li>
<li>In 2012, I started my <a href="https://sachachua.com/blog/category/experiment/">5-year experiment with semi-retirement</a>.</li>
<li>In 2016, A+ was born, so I wrote much fewer posts.</li>
<li>In 2019/2020, I wrote a lot of blog posts
documenting how I was <a href="https://sachachua.com/blog/category/emacs/">running EmacsConf with
Emacs</a>, and other Emacs tweaks along the way. The
code is probably very idiosyncratic (&#x2026; unless
you happen to know other conference organizers
who like to do as much as possible within Emacs?
Even then, there are lots of assumptions in the
code), but maybe people picked up useful ideas
anyway. =)</li>
</ul>

<p>
What were my top 20 most-commented posts?
</p>

<details><summary>Emacs Lisp code for most-commented posts</summary>
<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">let*</span> ((json-object-type <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">alist</span>)
       (json-array-type <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">list</span>)
       (comments-json (json-read-file <span class="org-string">"~/proj/static-blog/_data/commentsCounts.json"</span>))
       (posts-json (json-read-file <span class="org-string">"~/proj/static-blog/_site/blog/all/index.json"</span>))
       (post-map (make-hash-table <span class="org-builtin">:test</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">equal</span>)))
  <span class="org-comment-delimiter">;; </span><span class="org-comment">map permalink to title</span>
  (<span class="org-keyword">dolist</span> (post posts-json)
    (<span class="org-keyword">let</span> ((permalink (cdr (assoc <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">permalink</span> post)))
          (title (cdr (assoc <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">title</span> post))))
      (puthash permalink title post-map)))
  <span class="org-comment-delimiter">;; </span><span class="org-comment">Sort comments by count (descending)</span>
  (mapcar
   (<span class="org-keyword">lambda</span> (row)
     (list
      (cdr row)
            (org-link-make-string
       (concat <span class="org-string">"https://sachachua.com"</span> (symbol-name (car row)))
       (<span class="org-keyword">with-temp-buffer</span>
         (insert (<span class="org-keyword">or</span> (gethash (symbol-name (car row)) post-map) (symbol-name (car row))))
         (mm-url-decode-entities)
         (buffer-string)))))
   (seq-take
    (sort comments-json
          (<span class="org-keyword">lambda</span> (a b) (&gt; (cdr a) (cdr b))))
    n)))
</pre>
</div>

</details>

<table>


<colgroup>
<col class="org-right">

<col class="org-left">
</colgroup>
<tbody>
<tr>
<td class="org-right">97</td>
<td class="org-left"><a href="https://sachachua.com/blog/contact/"><i>blog/contact</i></a></td>
</tr>

<tr>
<td class="org-right">88</td>
<td class="org-left"><a href="https://sachachua.com/blog/2010/05/even-more-awesome-lotusscript-mail-merge-for-lotus-notes-microsoft-excel/">Even more awesome LotusScript mail merge for Lotus Notes + Microsoft Excel</a></td>
</tr>

<tr>
<td class="org-right">75</td>
<td class="org-left"><a href="https://sachachua.com/blog/about/"><i>blog/about</i></a></td>
</tr>

<tr>
<td class="org-right">45</td>
<td class="org-left"><a href="https://sachachua.com/blog/2013/05/how-to-learn-emacs-a-hand-drawn-one-pager-for-beginners/">How to Learn Emacs: A Hand-drawn One-pager for Beginners / A visual tutorial</a></td>
</tr>

<tr>
<td class="org-right">42</td>
<td class="org-left"><a href="https://sachachua.com/blog/2011/11/planning-an-emacs-based-personal-wiki-org-muse-hmm/">Planning an Emacs-based personal wiki – Org? Muse? Hmm…</a></td>
</tr>

<tr>
<td class="org-right">38</td>
<td class="org-left"><a href="https://sachachua.com/blog/2010/10/married/">Married!</a></td>
</tr>

<tr>
<td class="org-right">37</td>
<td class="org-left"><a href="https://sachachua.com/blog/2010/02/moving-from-testing-to-development/">Moving from testing to development</a></td>
</tr>

<tr>
<td class="org-right">36</td>
<td class="org-left"><a href="https://sachachua.com/blog/2009/12/what-can-i-help-you-learn-looking-for-mentees/">What can I help you learn? Looking for mentees</a></td>
</tr>

<tr>
<td class="org-right">33</td>
<td class="org-left"><a href="https://sachachua.com/blog/2009/07/lotus-notes-mail-merge-from-a-microsoft-excel-spreadsheet/">Lotus Notes mail merge from a Microsoft Excel spreadsheet</a></td>
</tr>

<tr>
<td class="org-right">30</td>
<td class="org-left"><a href="https://sachachua.com/blog/2009/04/nothing-quite-like-org-for-emacs/">Nothing quite like Org for Emacs</a></td>
</tr>

<tr>
<td class="org-right">30</td>
<td class="org-left"><a href="https://sachachua.com/blog/2012/05/org-mode-and-habits/">Org-mode and habits</a></td>
</tr>

<tr>
<td class="org-right">29</td>
<td class="org-left"><a href="https://sachachua.com/blog/2012/08/zomg-evernote-emacs/">zomg, Evernote and Emacs</a></td>
</tr>

<tr>
<td class="org-right">25</td>
<td class="org-left"><a href="https://sachachua.com/blog/2012/06/literate-programming-emacs-configuration-file/">Literate programming and my Emacs configuration file</a></td>
</tr>

<tr>
<td class="org-right">25</td>
<td class="org-left"><a href="https://sachachua.com/blog/2014/04/reinvesting-time-and-money-into-emacs/">Reinvesting time and money into Emacs</a></td>
</tr>

<tr>
<td class="org-right">23</td>
<td class="org-left"><a href="https://sachachua.com/blog/2008/05/the-gen-y-guide-to-web-20-at-work/">The Gen Y Guide to Web 2.0 at Work</a></td>
</tr>

<tr>
<td class="org-right">22</td>
<td class="org-left"><a href="https://sachachua.com/blog/2011/08/drupal-overriding-drupal-autocompletion-to-pass-more-parameters/">Drupal: Overriding Drupal autocompletion to pass more parameters</a></td>
</tr>

<tr>
<td class="org-right">21</td>
<td class="org-left"><a href="https://sachachua.com/blog/2011/07/rhetoric-and-the-manila-zoo-reflections-on-conversations-and-a-request-for-insight/">Rhetoric and the Manila Zoo; reflections on conversations and a request for insight</a></td>
</tr>

<tr>
<td class="org-right">20</td>
<td class="org-left"><a href="https://sachachua.com/blog/2010/07/this-is-a-test-post-from-org2blog/">This is a test post from org2blog</a></td>
</tr>

<tr>
<td class="org-right">19</td>
<td class="org-left"><a href="https://sachachua.com/blog/2010/01/agendas/">Agendas</a></td>
</tr>

<tr>
<td class="org-right">19</td>
<td class="org-left"><a href="https://sachachua.com/blog/2012/09/paper-tablet-and-tablet-pc-comparing-tools-for-sketchnoting/">Paper, Tablet, and Tablet PC: Comparing tools for sketchnoting</a></td>
</tr>
</tbody>
</table>

<p>
Top 3 by year. Note that this goes by the
timestamp of the post, not the comment, so even
old posts are in here.
</p>

<details><summary>Emacs Lisp code for most-commented posts by year</summary>
<div class="org-src-container">
<pre class="src src-emacs-lisp" id="org9bd8774">(<span class="org-keyword">let*</span> ((json-object-type <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">alist</span>)
       (json-array-type <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">list</span>)
       (comments-json (json-read-file <span class="org-string">"~/proj/static-blog/_data/commentsCounts.json"</span>))
       (posts-json (json-read-file <span class="org-string">"~/proj/static-blog/_site/blog/all/index.json"</span>))
       by-year)
  (<span class="org-keyword">setq</span> posts-json
        (mapcar
         (<span class="org-keyword">lambda</span> (post)
           (<span class="org-keyword">let</span> ((comments (alist-get (intern (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">permalink</span> post)) comments-json)))
             (<span class="org-keyword">if</span> comments
                 (cons (cons <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">comments</span> (alist-get (intern (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">permalink</span> post)) comments-json 0))
                       post)
               post)))
         posts-json))
  (<span class="org-keyword">setq</span> by-year
        (seq-group-by
         (<span class="org-keyword">lambda</span> (o)
           (format-time-string <span class="org-string">"%Y"</span>
                               (date-to-time
                                (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">date</span> o))
                               <span class="org-string">"America/Toronto"</span>))
         (seq-filter (<span class="org-keyword">lambda</span> (o) (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">comments</span> o)) posts-json)))
  (org-list-to-org
   (cons <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">unordered</span>
         (seq-keep
          (<span class="org-keyword">lambda</span> (year)
            (list
             (org-link-make-string (concat <span class="org-string">"https://sachachua.com/blog/"</span> (car year))
                                   (car year))
             (cons <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">unordered</span>
                   (mapcar
                    (<span class="org-keyword">lambda</span> (entry)
                      (list (format <span class="org-string">"%s (%d)"</span>
                                    (org-link-make-string
                                     (concat <span class="org-string">"https://sachachua.com"</span> (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">permalink</span> entry))
                                     (<span class="org-keyword">with-temp-buffer</span>
                                       (insert (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">title</span> entry))
                                       (mm-url-decode-entities)
                                       (buffer-string)))
                                    (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">comments</span> entry))))
                    (seq-take
                     (sort
                      (cdr year)
                      (<span class="org-keyword">lambda</span> (a b) (&gt; (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">comments</span> a)
                                       (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">comments</span> b))))
                     n)))))
          (nreverse by-year)))))
</pre>
</div>

</details>

<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2025">2025</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2025/01/using-image-dired-to-browse-the-latest-screenshots-from-multiple-directories/">Using image-dired to browse the latest screenshots from multiple directories</a> (2)</li>
<li><a href="https://sachachua.com/blog/2025/01/changing-planet-emacslife-com/">Changing planet.emacslife.com</a> (2)</li>
<li><a href="https://sachachua.com/blog/2025/03/2025-03-03-emacs-news/">2025-03-03 Emacs news</a> (2)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2024">2024</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2024/01/using-an-emacs-lisp-macro-to-define-quick-custom-org-mode-links-to-project-files/">Using an Emacs Lisp macro to define quick custom Org Mode links to project files; plus URLs and search</a> (6)</li>
<li><a href="https://sachachua.com/blog/2024/11/excerpts-from-a-conversation-with-john-wiegley-johnw-and-adam-porter-alphapapa-about-personal-information-management/">Excerpts from a conversation with John Wiegley (johnw) and Adam Porter (alphapapa) about personal information management</a> (5)</li>
<li><a href="https://sachachua.com/blog/2024/01/yay-emacs-2024-01-12-emacsconf-2023-report-svg-animation-embark-org-mode-links/">Yay Emacs 1: EmacsConf 2023 report, SVG animation, Embark, Org Mode links</a> (4)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2023">2023</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2023/09/how-i-keep-track-of-new-emacs-packages/">How I keep track of new Emacs packages</a> (3)</li>
<li><a href="https://sachachua.com/blog/2023/12/automatically-refiling-org-mode-headings-based-on-tags/">Automatically refiling Org Mode headings based on tags</a> (3)</li>
<li><a href="https://sachachua.com/blog/2023/01/building-up-my-tech-notes/">Building up my tech notes</a> (2)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2022">2022</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2022/08/recoloring-my-sketches-with-python/">Recoloring my sketches with Python</a> (5)</li>
<li><a href="https://sachachua.com/blog/2022/11/late-night-braindumps-by-talking-to-myself/">Late-night braindumps by talking to myself</a> (3)</li>
<li><a href="https://sachachua.com/blog/2022/01/2022-01-24-emacs-news/">2022-01-24 Emacs news</a> (2)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2021">2021</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2021/01/a-list-of-sharks-that-are-obligate-ram-ventilators/">A list of sharks that are obligate ram ventilators</a> (6)</li>
<li><a href="https://sachachua.com/blog/2021/04/org-mode-insert-youtube-video-with-separate-captions/">Org Mode: Insert YouTube video with separate captions</a> (6)</li>
<li><a href="https://sachachua.com/blog/2021/12/thinking-about-emacs-community-maintenance/">Thinking about Emacs community maintenance</a> (6)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2020">2020</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2020/07/why-i-love-free-software/">Why I love free software</a> (9)</li>
<li><a href="https://sachachua.com/blog/2020/06/pythonfontforgeorg-i-made-a-font-based-on-my-handwriting/">Python+FontForge+Org: I made a font based on my handwriting!</a> (7)</li>
<li><a href="https://sachachua.com/blog/2020/12/2020-12-14-emacs-news/">2020-12-14 Emacs news</a> (5)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2019">2019</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2019/06/turning-an-org-mode-outline-into-an-html-table-with-a-column-for-more-notes/">Turning an Org Mode outline into an HTML table with a column for more notes</a> (10)</li>
<li><a href="https://sachachua.com/blog/2019/07/tweaking-emacs-on-android-via-termux-xclip-xdg-open-syncthing-conflicts/">Tweaking Emacs on Android via Termux: xclip, xdg-open, syncthing conflicts</a> (9)</li>
<li><a href="https://sachachua.com/blog/2019/03/visual-book-notes-no-drama-discipline-2014/">Visual Book Notes: No-Drama Discipline (2014)</a> (6)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2018">2018</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2018/04/2018-04-09-emacs-news/">2018-04-09 Emacs news</a> (6)</li>
<li><a href="https://sachachua.com/blog/2018/01/a-and-household-life/">A- and household life</a> (5)</li>
<li><a href="https://sachachua.com/blog/2018/03/using-org-mode-latex-beamer-and-medibang-paint-to-make-a-childrens-book/">Using Org Mode, LaTeX, Beamer, and Medibang Paint to make a children's book</a> (4)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2017">2017</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2017/06/quick-notes-on-my-current-interface-for-time-tracking/">Quick notes on my current interface for time-tracking</a> (9)</li>
<li><a href="https://sachachua.com/blog/2017/12/working-around-my-phone-plans-lack-of-roaming/">Working around my phone plan’s lack of roaming</a> (8)</li>
<li><a href="https://sachachua.com/blog/2017/04/emacs-pasting-with-the-mouse-without-moving-the-point-mouse-yank-at-point/">Emacs: Pasting with the mouse without moving the point – mouse-yank-at-point</a> (5)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2016">2016</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2016/12/using-categories-organize-org-agenda/">Using categories to organize your Org agenda</a> (16)</li>
<li><a href="https://sachachua.com/blog/2016/01/2016-01-16-emacs-hangout/">2016-01-16 Emacs Hangout</a> (9)</li>
<li><a href="https://sachachua.com/blog/2016/03/microphthalmia-small-eye/">Microphthalmia: small eye</a> (8)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2015">2015</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2015/02/lets-virtual-emacs-conference-august-help-make-happen/">Let’s have a virtual Emacs conference in August – help me make it happen!</a> (18)</li>
<li><a href="https://sachachua.com/blog/2015/01/thinking-make-better-use-yasnippet-emacs-workflow/">Thinking about how to make better use of Yasnippet in my Emacs workflow</a> (17)</li>
<li><a href="https://sachachua.com/blog/2015/07/july-2015-emacs-hangout/">July 2015 Emacs Hangout</a> (13)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2014">2014</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2014/04/reinvesting-time-and-money-into-emacs/">Reinvesting time and money into Emacs</a> (25)</li>
<li><a href="https://sachachua.com/blog/2014/12/can-improve-organize-notes-org-mode/">Improving how I organize notes with Org Mode</a> (19)</li>
<li><a href="https://sachachua.com/blog/2014/12/emacs-m-y-helm-show-kill-ring/">Emacs: M-y as helm-show-kill-ring</a> (18)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2013">2013</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2013/05/how-to-learn-emacs-a-hand-drawn-one-pager-for-beginners/">How to Learn Emacs: A Hand-drawn One-pager for Beginners / A visual tutorial</a> (45)</li>
<li><a href="https://sachachua.com/blog/2013/04/brainstorming-ways-to-help-build-the-emacs-community/">Brainstorming ways to help build the Emacs community</a> (19)</li>
<li><a href="https://sachachua.com/blog/2013/08/emacs-how-i-organize-my-org-files/">Emacs: How I organize my Org files</a> (19)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2012">2012</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2012/05/org-mode-and-habits/">Org-mode and habits</a> (30)</li>
<li><a href="https://sachachua.com/blog/2012/08/zomg-evernote-emacs/">zomg, Evernote and Emacs</a> (29)</li>
<li><a href="https://sachachua.com/blog/2012/06/literate-programming-emacs-configuration-file/">Literate programming and my Emacs configuration file</a> (25)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2011">2011</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2011/11/planning-an-emacs-based-personal-wiki-org-muse-hmm/">Planning an Emacs-based personal wiki – Org? Muse? Hmm…</a> (42)</li>
<li><a href="https://sachachua.com/blog/2011/08/drupal-overriding-drupal-autocompletion-to-pass-more-parameters/">Drupal: Overriding Drupal autocompletion to pass more parameters</a> (22)</li>
<li><a href="https://sachachua.com/blog/2011/07/rhetoric-and-the-manila-zoo-reflections-on-conversations-and-a-request-for-insight/">Rhetoric and the Manila Zoo; reflections on conversations and a request for insight</a> (21)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2010">2010</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2010/05/even-more-awesome-lotusscript-mail-merge-for-lotus-notes-microsoft-excel/">Even more awesome LotusScript mail merge for Lotus Notes + Microsoft Excel</a> (88)</li>
<li><a href="https://sachachua.com/blog/2010/10/married/">Married!</a> (38)</li>
<li><a href="https://sachachua.com/blog/2010/02/moving-from-testing-to-development/">Moving from testing to development</a> (37)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2009">2009</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2009/12/what-can-i-help-you-learn-looking-for-mentees/">What can I help you learn? Looking for mentees</a> (36)</li>
<li><a href="https://sachachua.com/blog/2009/07/lotus-notes-mail-merge-from-a-microsoft-excel-spreadsheet/">Lotus Notes mail merge from a Microsoft Excel spreadsheet</a> (33)</li>
<li><a href="https://sachachua.com/blog/2009/04/nothing-quite-like-org-for-emacs/">Nothing quite like Org for Emacs</a> (30)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2008">2008</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2008/05/the-gen-y-guide-to-web-20-at-work/">The Gen Y Guide to Web 2.0 at Work</a> (23)</li>
<li><a href="https://sachachua.com/blog/2008/01/outlining-your-notes-with-org/">Outlining Your Notes with Org</a> (18)</li>
<li><a href="https://sachachua.com/blog/2008/01/tagging-in-org-plus-bonus-code-for-timeclocks-and-tags/">Tagging in Org – plus bonus code for timeclocks and tags!</a> (14)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2007">2007</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2007/12/clocking-time-with-emacs-org/">Clocking Time with Emacs Org</a> (18)</li>
<li><a href="https://sachachua.com/blog/2007/12/emacs-choosing-between-org-and-planner/">Emacs: Choosing between Org and Planner</a> (13)</li>
<li><a href="https://sachachua.com/blog/2007/12/wicked-cool-emacs-get-in-on-the-action/">Wicked Cool Emacs: get in on the action!</a> (10)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2006">2006</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2006/04/how-to-wear-a-malong/">How to wear a malong</a> (6)</li>
<li><a href="https://sachachua.com/blog/2006/09/what-makes-a-good-life/">What makes a good life?</a> (6)</li>
<li><a href="https://sachachua.com/blog/2006/09/emacs-linkedin-another-totally-idiosyncratic-bit-of-code/">Emacs + LinkedIn: Another totally idiosyncratic bit of code</a> (3)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2005">2005</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2005/11/emacs-its-all-about-people/">Emacs: It’s all about people</a> (4)</li>
<li><a href="https://sachachua.com/blog/2005/12/learning-bisaya/">Learning Bisaya</a> (4)</li>
<li><a href="https://sachachua.com/blog/2005/03/networking/">Networking</a> (3)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2004">2004</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2004/02/nethack-el/">nethack-el</a> (4)</li>
<li><a href="https://sachachua.com/blog/2004/03/rmail-labels/">RMAIL labels</a> (4)</li>
<li><a href="https://sachachua.com/blog/2004/11/sketch-website-design/">Sketch website design</a> (3)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2003">2003</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2003/11/cookordie-day-2-veal-sausage-and-potatoes/">CookOrDie Day 2: Veal sausage and potatoes</a> (4)</li>
<li><a href="https://sachachua.com/blog/2003/06/look-into-literate-programming/">Look into literate programming</a> (3)</li>
<li><a href="https://sachachua.com/blog/2003/08/automatically-detecting-proxy-settings/">Automatically detecting proxy settings</a> (3)</li>
</ul></li>
<li><a href="https://sachachua.com/blog/2002">2002</a>
<ul class="org-ul">
<li><a href="https://sachachua.com/blog/2002/06/hello-world-school-teaching-games/">Hello world, school, teaching, games ()</a> (1)</li>
</ul></li>
</ul>

<p>
As you can probably tell, I love writing about
Emacs, especially when people drop by in the
comments to:
</p>

<ul class="org-ul">
<li>share that they'd just learned about some small
thing I mentioned in passing and that it was
really useful for this other part of their
workflow that I totally wouldn't have guessed</li>
<li>point out a simpler package or built-in Emacs
function that also does whatever clever hack I
wrote about, just in a more polished way</li>
<li>link to a blog post or code snippet where
they've borrowed the idea and added their own
spin</li>
</ul>

<p>
I want to keep having those sorts of
conversations.
</p>
</div>
</div>
<div id="outline-container-moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site-deleting-spam-comments-via-the-disqus-web-interface-and-spookfox" class="outline-2">
<h3 id="moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site-deleting-spam-comments-via-the-disqus-web-interface-and-spookfox">Deleting spam comments via the Disqus web interface and Spookfox</h3>
<div class="outline-text-2" id="text-moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site-deleting-spam-comments-via-the-disqus-web-interface-and-spookfox">
<p>
8000+ comments are a lot to read, but it should be
pretty straightforward to review the comments at
least until 2016 or so, and then just clean out
spam as I come across it after that. I used the
Disqus web interface to delete spam comments since
the <code>isSpam</code> attribute didn't seem to be reliable.
The web interface pages through comments 25 items
at a time and doesn't seem to let you select all
of them, so I started tinkering around with using
<a href="https://github.com/bitspook/spookfox">Spookfox</a> to automate this. Spookfox lets me
control Mozilla Firefox from Emacs Lisp.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">progn</span>
  <span class="org-comment-delimiter">;; </span><span class="org-comment">select all</span>
  (spookfox-eval-js-in-active-tab <span class="org-string">"document.querySelector('.mod-bar__check input').click()"</span>)
  (wait-for 1)
  <span class="org-comment-delimiter">;; </span><span class="org-comment">delete</span>
  (spookfox-eval-js-in-active-tab <span class="org-string">"document.querySelectorAll('</span><span class="org-string"><span class="org-constant">.mod-bar__button</span></span><span class="org-string">')[2].click()"</span>)
  (wait-for 2)
  <span class="org-comment-delimiter">;; </span><span class="org-comment">click OK, which should make the list refresh</span>
  (spookfox-eval-js-in-active-tab <span class="org-string">"btn = document.querySelectorAll('</span><span class="org-string"><span class="org-constant">.mod-bar__button</span></span><span class="org-string">')[1]; if (btn.textContent.match('</span><span class="org-string"><span class="org-constant">OK</span></span><span class="org-string">')) btn.click();"</span>)
  (wait-for 4)
  <span class="org-comment-delimiter">;; </span><span class="org-comment">backup: (spookfox-eval-js-in-active-tab "window.location.href = '</span><span class="org-comment"><span class="org-constant">https://sachac.disqus.com/admin/moderate/spam</span></span><span class="org-comment">'")</span>
  )
</pre>
</div>


<p>
I got to the end of the spam comments after maybe
10 or 20 pages, though, so maybe Disqus had
auto-deleted most of the spam comments.
</p>

<p>
It's almost amusing, paging through all these
spammy attempts at link-building and product
promotion. I didn't want to click on any of the
links since there might be malware, so sometimes I
used <code>curl</code> to check the site. Most of the old
spam links I checked don't even have working
domains any more. Anything that needed spam didn't
really have lasting power. It was all very "My
name is Ozymandias, king of kings: / Look on my
works, ye Mighty, and despair!"&#x2026; and then gone.
</p>
</div>
</div>
<div id="outline-container-moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site-modifying-eleventy-import-disqus-for-my-site" class="outline-2">
<h3 id="moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site-modifying-eleventy-import-disqus-for-my-site">Modifying eleventy-import-disqus for my site</h3>
<div class="outline-text-2" id="text-moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site-modifying-eleventy-import-disqus-for-my-site">
<p>
Back to eleventy-import-disqus. I followed the
directions to make a contentMap.json and removed
the trailing <code>,</code> from the last entry so that the
JSON could be parsed.
</p>

<p>
<a href="https://github.com/sachac/eleventy-import-disqus-sachachua.com/tree/sachachua.com">Modifications to eleventy-import-disqus:</a>
</p>
<ul class="org-ul">
<li>The original code created all the files in the same directory, so I changed it to create the same kind of nested structure I use (generally <code>./blog/yyyy/mm/post-slug/index.html</code> and <code>./blog/yyyy/mm/post-slug/index.11tydata.json</code>). I decided to store the Disqus comments in <code>index.json</code>, which is lower-priority than <code>.11tydata.json.</code> <a href="https://stackoverflow.com/questions/13542667/create-directory-when-writing-to-file-in-node-js">fs-extra</a> made this easier by creating all the parent directories.</li>
<li>Ignored deleted messages</li>
<li>Discarded avatars</li>
<li>Did some reporting to help me review potential spam</li>
<li>Reparented messages if I deleted their parent posts</li>
<li>Indent the thread JSON nicely in case I want to add or remove comments by hand</li>
</ul>

<p>
With the thread JSON files, my blog takes 143
seconds to generate, versus 133 seconds without
the comments. +10 seconds isn't too bad. I was
worried that it would be longer, since I added
2,088 data JSON files to the build process, but I
guess 11ty is pretty efficient.
</p>
</div>
</div>
<div id="outline-container-moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site-next-steps" class="outline-2">
<h3 id="moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site-next-steps">Next steps</h3>
<div class="outline-text-2" id="text-moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site-next-steps">
<p>
It had been nice to have a comment form that
people could fill in from anywhere and which
shared their comments without needing my (often
delayed) intervention. I learned lots of things
from what people shared. Sometimes people even had
discussions with each other, which was extra cool.
Still, I think it might be a good time to
experiment with alternatives. <a href="mailto:sacha@sachachua.com">Plain e-mail</a> for
now, I guess, maybe with a nudge asking people if
I could share their comments. <a href="https://sachachua.com/blog/2025/03/tweaking-my-11ty-blog-to-link-to-the-mastodon-post-defined-in-an-org-mode-property/">Mastodon</a>, too -
could be fun to make it easy to add a toot to the
static comments from <a href="https://codeberg.org/martianh/mastodon.el">mastodon.el</a> or from my Org
Mode inbox. (<mark>Update 2025-03-30:</mark> <a href="https://sachachua.com/dotemacs#mastodon-adding-mastodon-toots-as-comments-in-my-11ty-static-blog">Adding Mastodon
toots as comments in my 11ty static blog</a>) Might be
good to figure out <a href="https://webmention.io/">Webmentions</a>, too. (But then
<a href="https://brainbaking.com/post/2023/05/why-i-retired-my-webmention-server/">other people have been dealing with spam
Webmentions, of course</a>.)
</p>

<p>
Comment counts can be useful social signals for
interesting posts. I haven't added comment counts
to the lists of blog posts yet.
eleventy-import-disqus created a
commentsCounts.json, which I could use in my
templates. However, I might change the comments in
the per-post .json file if I figure out how to
include Mastodon comments, so I may need to update
that file or recalculate it from the posts.
</p>

<p>
Many of the blogs I read have shifted away from
commenting systems, and the ones who still have
comments on seem to be bracing for <a href="https://www.zylstra.org/blog/2025/03/incoming-ai-generated-comment-spam/">AI-generated
comment spam</a>. I'm not sure I like the way the
Internet is moving, but maybe in this little
corner, we can still have conversations across
time. Comments are such a wonderful part of
learning out loud. I wonder how we can keep
learning together.</p>
</div>
</div>
<div><a href="https://sachachua.com/blog/2025/03/moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site/index.org">View org source for this post</a></div>
<p>You can <a href="https://social.sachachua.com/@sacha/statuses/01JQM0XSA0W90C9Y4WKA808MTY" target="_blank" rel="noopener noreferrer">comment on Mastodon</a>, <a href="https://sachachua.com/blog/2025/03/moving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site/#comment">view 8 comments</a>, or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2025%2F03%2Fmoving-18-years-of-comments-out-of-disqus-and-into-my-11ty-static-site%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">Tweaking my 11ty blog to link to the Mastodon post defined in an Org Mode property</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2025/03/tweaking-my-11ty-blog-to-link-to-the-mastodon-post-defined-in-an-org-mode-property/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2025-03-27T17:20:15Z</updated>
    <published>2025-03-27T17:20:15Z</published>
    <category term="11ty" />
<category term="mastodon" />
<category term="org" />
		<id>https://sachachua.com/blog/2025/03/tweaking-my-11ty-blog-to-link-to-the-mastodon-post-defined-in-an-org-mode-property/</id>
		<content type="html"><![CDATA[<p>
One of the things I like about blogging from Org
Mode in Emacs is that it's easy to add properties
to the section that I'm working on and then use
those property values elsewhere. For example, I've
modified Emacs to simplify <a href="https://sachachua.com/blog/2025/03/mastodon-el-copy-toot-url-after-posting-also-copying-just-this-post-with-11ty/#:~:text=If%20it%20has%20and%20I%20don't%20have%20a%20Mastodon%20toot%20field%20yet%2C%20maybe%20I%20can%20automatically%20set%20that%20property%2C%20assuming%20I%20end%20up%20back%20in%20the%20Org%20Mode%20file%20I%20started%20it%20from.">tooting a link to my
blog post</a> and <a href="https://sachachua.com/blog/2025/03/mastodon-el-copy-toot-url-after-posting-also-copying-just-this-post-with-11ty/#:~:text=If%20it%20has%20and%20I%20don't%20have%20a%20Mastodon%20toot%20field%20yet%2C%20maybe%20I%20can%20automatically%20set%20that%20property%2C%20assuming%20I%20end%20up%20back%20in%20the%20Org%20Mode%20file%20I%20started%20it%20from.">saving the Mastodon status URL</a> in
the <code>EXPORT_MASTODON</code> property. Then I can use
that in my <a href="https://www.11ty.dev/">11ty</a> static site generation process to
include a link to the Mastodon thread as a comment
option.
</p>

<p>
First, I need to export the property and include
it in the front matter. I use <a href="https://www.11ty.dev/docs/data-template-dir/">.11tydata.json</a> files
to store the details for each blog post. I
modified <a href="https://github.com/sachac/ox-11ty/blob/master/ox-11ty.el">ox-11ty.el</a> so that I could specify
functions to change the front matter
(<code>org-11ty-front-matter-functions</code>,
<code>org-11ty&#45;&#45;front-matter</code>):
</p>


<div class="org-src-container">
<pre class="src src-elisp">(<span class="org-keyword">defvar</span> <span class="org-variable-name">org-11ty-front-matter-functions</span> nil
  <span class="org-doc">"Functions to call with the current front matter plist and info."</span>)
(<span class="org-keyword">defun</span> <span class="org-function-name">org-11ty&#45;&#45;front-matter</span> (info)
  <span class="org-doc">"Return front matter for INFO."</span>
  (<span class="org-keyword">let*</span> ((date (plist-get info <span class="org-builtin">:date</span>))
         (title (plist-get info <span class="org-builtin">:title</span>))
         (modified (plist-get info <span class="org-builtin">:modified</span>))
         (permalink (plist-get info <span class="org-builtin">:permalink</span>))
         (categories (plist-get info <span class="org-builtin">:categories</span>))
         (collections (plist-get info <span class="org-builtin">:collections</span>))
         (extra (<span class="org-keyword">if</span> (plist-get info <span class="org-builtin">:extra</span>) (json-parse-string
                                             (plist-get info <span class="org-builtin">:extra</span>)
                                             <span class="org-builtin">:object-type</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">plist</span>))))
    (seq-reduce
     (<span class="org-keyword">lambda</span> (prev val)
       (funcall val prev info))
     org-11ty-front-matter-functions
     (append
      extra
      (list <span class="org-builtin">:permalink</span> permalink
            <span class="org-builtin">:date</span> (<span class="org-keyword">if</span> (listp date) (car date) date)
            <span class="org-builtin">:modified</span> (<span class="org-keyword">if</span> (listp modified) (car modified) modified)
            <span class="org-builtin">:title</span> (<span class="org-keyword">if</span> (listp title) (car title) title)
            <span class="org-builtin">:categories</span> (<span class="org-keyword">if</span> (stringp categories) (split-string categories) categories)
            <span class="org-builtin">:tags</span> (<span class="org-keyword">if</span> (stringp collections) (split-string collections) collections))))))
</pre>
</div>


<p>
Then I added <a href="https://sachachua.com/dotemacs#org-mode-publishing-11ty-static-site-generation-include-mastodon-field-in-front-matter">the <code>EXPORT_MASTODON</code> Org property</a> as
part of the front matter. This took a little
figuring out because I needed to pass it as one of
<code>org-export-backend-options</code>, where the parameter
is defined as <code>MASTODON</code> but the actual property
needs to be called <code>EXPORT_MASTODON</code>.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-org-11ty-add-mastodon-to-front-matter</span> (front-matter info)
  (plist-put front-matter <span class="org-builtin">:mastodon</span> (plist-get info <span class="org-builtin">:mastodon</span>)))
(<span class="org-keyword">with-eval-after-load</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">ox-11ty</span>
  (<span class="org-keyword">cl-pushnew</span>
   <span class="org-highlight-quoted-quote">'</span>(<span class="org-builtin">:mastodon</span> <span class="org-string">"MASTODON"</span> nil nil)
   (org-export-backend-options (org-export-get-backend <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">11ty</span>)))
  (add-hook <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">org-11ty-front-matter-functions</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">my-org-11ty-add-mastodon-to-front-matter</span>))
</pre>
</div>


<p>
Then I added the Mastodon field as an option to my
<a href="https://github.com/sachac/eleventy-blog-setup/blob/master/_includes/shortcodes/comments.cjs">comments.cjs</a> shortcode. This was a little tricky
because I'm not sure I'm passing the data
correctly to the shortcode (sometimes it ends up
as item.data, sometimes it's item.data.data,
&#x2026;?), but with <code>?.</code>, I can just throw all the
possibilities in there and it'll eventually find
the right one.
</p>


<div class="org-src-container">
<pre class="src src-js"><span class="org-keyword">const</span> <span class="org-variable-name">pluginRss</span> = require(<span class="org-string">'@11ty/eleventy-plugin-rss'</span>);
module.exports = <span class="org-keyword">function</span>(<span class="org-variable-name">eleventyConfig</span>) {
  <span class="org-keyword">function</span> <span class="org-function-name">getCommentChoices</span>(<span class="org-variable-name">data</span>, <span class="org-variable-name">ref</span>) {
    <span class="org-keyword">const</span> <span class="org-variable-name">mastodonUrl</span> = data.mastodon || data.page?.mastodon || data.data?.mastodon;
    <span class="org-keyword">const</span> <span class="org-variable-name">mastodon</span> = mastodonUrl &amp;&amp; <span class="org-string">`&lt;a href="${mastodonUrl}" target="_blank" rel="noopener noreferrer"&gt;comment on Mastodon&lt;/a&gt;`</span>;
    <span class="org-keyword">const</span> <span class="org-variable-name">url</span> = ref.absoluteUrl(data.url || data.permalink || data.data?.url || data.data?.permalink, data.metadata?.url || data.data?.metadata?.url);
    <span class="org-keyword">const</span> <span class="org-variable-name">subject</span> = encodeURIComponent(<span class="org-string">'Comment on '</span> + url);
    <span class="org-keyword">const</span> <span class="org-variable-name">body</span> = encodeURIComponent(<span class="org-string">"Name you want to be credited by (if any): \nMessage: \nCan I share your comment so other people can learn from it? Yes/No\n"</span>);
    <span class="org-keyword">const</span> <span class="org-variable-name">email</span> = <span class="org-string">`&lt;a href="mailto:sacha@sachachua.com?subject=${subject}&amp;body=${body}"&gt;e-mail me at sacha@sachachua.com&lt;/a&gt;`</span>;
    <span class="org-keyword">const</span> <span class="org-variable-name">disqusLink</span> = url + <span class="org-string">'#comment'</span>;
    <span class="org-keyword">const</span> <span class="org-variable-name">disqusForm</span> = data.metadata?.disqusShortname &amp;&amp; <span class="org-string">`&lt;div id="disqus_thread"&gt;&lt;/div&gt;</span>
<span class="org-string">&lt;script&gt;</span>
<span class="org-string"> var disqus_config = function () {</span>
<span class="org-string">   this.page.url = "${url}";</span>
<span class="org-string">   this.page.identifier = "${data.id || ''} ${data.metadata?.url || ''}?p=${ data.id || data.permalink || this.page?.url}";</span>
<span class="org-string">   this.page.disqusTitle = "${ data.title }"</span>
<span class="org-string">   this.page.postId = "${ data.id || data.permalink || this.page?.url }"</span>
<span class="org-string"> };</span>
<span class="org-string"> (function() { // DON'T EDIT BELOW THIS LINE</span>
<span class="org-string">   var d = document, s = d.createElement('script');</span>
<span class="org-string">   s.src = 'https://${ data.metadata?.disqusShortname }.disqus.com/embed.js';</span>
<span class="org-string">   s.setAttribute('data-timestamp', +new Date());</span>
<span class="org-string">   (d.head || d.body).appendChild(s);</span>
<span class="org-string"> })();</span>
<span class="org-string">&lt;/script&gt;</span>
<span class="org-string">&lt;noscript&gt;Disqus requires Javascript, but you can still e-mail me if you want!&lt;/noscript&gt;`</span>;
    <span class="org-keyword">return</span> { mastodon, disqusLink, disqusForm, email };
  }
  eleventyConfig.addShortcode(<span class="org-string">'comments'</span>, <span class="org-keyword">function</span>(<span class="org-variable-name">data</span>, <span class="org-variable-name">linksOnly</span>=<span class="org-constant">false</span>) {
    <span class="org-keyword">const</span> { mastodon, disqusForm, disqusLink, email } = getCommentChoices(data, <span class="org-constant">this</span>);
    <span class="org-keyword">if</span> (linksOnly) {
      <span class="org-keyword">return</span> <span class="org-string">`You can ${mastodon ? mastodon + ', ' : ''}&lt;a href="${disqusLink}"&gt;comment with Disqus (JS required)&lt;/a&gt;${mastodon ? ',' : ''} or ${email}.`</span>;
    } <span class="org-keyword">else</span> {
      <span class="org-keyword">return</span> <span class="org-string">`&lt;div id="comment"&gt;&lt;/div&gt;</span>
<span class="org-string">You can ${mastodon ? mastodon + ', ' : ''}comment with Disqus (JS required)${mastodon ? ', ' : ''} or you can ${email}.</span>
<span class="org-string">${disqusForm || ''}`</span>;}
  });
}
</pre>
</div>


<p>
I included it in my <a href="https://github.com/sachac/eleventy-blog-setup/blob/master/_includes/shortcodes/post.cjs">post.cjs</a> shortcode:
</p>


<div class="org-src-container">
<pre class="src src-js">module.exports = eleventyConfig =&gt;
eleventyConfig.addShortcode(<span class="org-string">'post'</span>, <span class="org-keyword">async</span> <span class="org-keyword">function</span>(<span class="org-variable-name">item</span>, <span class="org-variable-name">index</span>, <span class="org-variable-name">includeComments</span>) {
  <span class="org-keyword">let</span> <span class="org-variable-name">comments</span> = <span class="org-string">'&lt;div class="comments"&gt;'</span> + (includeComments ? <span class="org-constant">this</span>.comments(item) : <span class="org-constant">this</span>.comments(item, <span class="org-constant">true</span>)) + <span class="org-string">'&lt;/div&gt;'</span>;
  <span class="org-keyword">let</span> <span class="org-variable-name">categoryList</span> = item.categories || item.data &amp;&amp; item.data.categories;
  <span class="org-keyword">let</span> <span class="org-variable-name">categoriesFooter</span> = <span class="org-string">''</span>, <span class="org-variable-name">categories</span> = <span class="org-string">''</span>;
  <span class="org-keyword">if</span> (categoryList &amp;&amp; categoryList.length &gt; 0) {
    categoriesFooter = <span class="org-string">`&lt;div class="footer-categories"&gt;More posts about ${this.categoryList(categoryList)}&lt;/div&gt;`</span>;
    categories = <span class="org-string">`| &lt;span class="categories"&gt;${this.categoryList(categoryList)}&lt;/span&gt;`</span>;
  }

  <span class="org-keyword">return</span>  <span class="org-string">`&lt;article class="post" id="index${index}" data-url="${item.url || item.permalink || ''}"&gt;</span>
<span class="org-string">&lt;header&gt;&lt;h2 data-pagefind-meta="title"&gt;&lt;a href="${item.url || item.permalink || ''}"&gt;${item.title || item.data &amp;&amp; item.data.title}&lt;/a&gt;&lt;/h2&gt;</span>
<span class="org-string">${this.timeInfo(item)}${categories}</span>
<span class="org-string">&lt;/header&gt;</span>
<span class="org-string">&lt;div class="entry"&gt;</span>
<span class="org-string">${await (item.templateContent || item.layoutContent || item.data?.content || item.content || item.inputContent)}</span>
<span class="org-string">&lt;/div&gt;</span>
<span class="org-string">${comments}</span>
<span class="org-string">${categoriesFooter}</span>
<span class="org-string">&lt;/article&gt;`</span>;
});
</pre>
</div>


<p>
I also included it in my <a href="https://github.com/sachac/eleventy-blog-setup/blob/master/_includes/shortcodes/rssItem.cjs">RSS item</a> template to make
it easier for people to send me comments without
having to dig through my website for contact info.
</p>


<div class="org-src-container">
<pre class="src src-js"><span class="org-keyword">const</span> <span class="org-variable-name">posthtml</span> = require(<span class="org-string">"posthtml"</span>);
<span class="org-keyword">const</span> <span class="org-variable-name">urls</span> = require(<span class="org-string">"posthtml-urls"</span>);

module.exports = (eleventyConfig) =&gt; {
  eleventyConfig.addAsyncShortcode(<span class="org-string">'rssItem'</span>, <span class="org-keyword">async</span> <span class="org-keyword">function</span>(<span class="org-variable-name">item</span>) {
    <span class="org-keyword">let</span> <span class="org-variable-name">content</span> = item.templateContent.replace(<span class="org-string">/&#45;&#45;/</span>g, <span class="org-string">'&amp;#45;&amp;#45;'</span>);
    <span class="org-keyword">if</span> (<span class="org-constant">this</span>.transformWithHtmlBase) {
      content = <span class="org-keyword">await</span> <span class="org-constant">this</span>.transformWithHtmlBase(content);
    }
    <span class="org-keyword">return</span> <span class="org-string">`&lt;item&gt;</span>
<span class="org-string">    &lt;title&gt;${item.data.title}&lt;/title&gt;</span>
<span class="org-string">    &lt;link&gt;${this.absoluteUrl(item.url, item.data.metadata.url)}&lt;/link&gt;</span>
<span class="org-string">    &lt;dc:creator&gt;&lt;![CDATA[${item.data.metadata.author.name}]]&gt;&lt;/dc:creator&gt;</span>
<span class="org-string">    &lt;pubDate&gt;${item.date.toUTCString()}&lt;/pubDate&gt;</span>
<span class="org-string">    ${item.data.categories?.map((cat) =&gt; `</span>&lt;category&gt;${cat}&lt;/category&gt;<span class="org-string">`).join("\n") || ''}</span>
<span class="org-string">    &lt;guid isPermaLink="false"&gt;${this.guid(item)}&lt;/guid&gt;</span>
<span class="org-string">    &lt;description&gt;&lt;![CDATA[${content}</span>
<span class="org-string">&lt;p&gt;${this.comments(item, true)}&lt;/p&gt;]]&gt;&lt;/description&gt;</span>
<span class="org-string">    &lt;/item&gt;`</span>;
  });
};
</pre>
</div>


<p>
The new workflow I'm trying out seems to be working:
</p>

<ol class="org-ol">
<li>Keep <code>npx eleventy &#45;&#45;serve</code> running in the
background, using <code>.eleventyignore</code> to make
rebuilds reasonably fast.</li>
<li>Export the subtree with <code>C-c e s 1 1</code>, which uses <code>org-export-dispatch</code> to call <code>my-org-11ty-export</code> with the subtree.</li>
<li>After about 10 seconds, use <code>my-org-11ty-copy-just-this-post</code> and verify.</li>
<li>Use <code>my-mastodon-11ty-toot-post</code> to compose a toot. Edit the toot and post it.</li>
<li>Check that the <code>EXPORT_MASTODON</code> property has been set.</li>
<li>Export the subtree again, this time with the front matter.</li>
<li>Publish my whole blog.</li>
</ol>

<p>
Next, I'm thinking of modifying
<code>my-mastodon-11ty-toot-post</code> so that it includes a
list of links to blog posts I might be building on
or responding to, and possibly the handles of
people related to those blog posts or topics.
Hmm&#x2026;
</p>
<div><a href="https://sachachua.com/blog/2025/03/tweaking-my-11ty-blog-to-link-to-the-mastodon-post-defined-in-an-org-mode-property/index.org">View org source for this post</a></div><p>You can <a href="https://social.sachachua.com/@sacha/statuses/01JQCB0QP0S3XPJ81EP4CK8XD9" target="_blank" rel="noopener noreferrer">comment on Mastodon</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2025%2F03%2Ftweaking-my-11ty-blog-to-link-to-the-mastodon-post-defined-in-an-org-mode-property%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">mastodon.el: Copy toot URL after posting; also, copying just this post with 11ty</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2025/03/mastodon-el-copy-toot-url-after-posting-also-copying-just-this-post-with-11ty/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2025-03-24T00:41:53Z</updated>
    <published>2025-03-24T00:41:53Z</published>
    <category term="mastodon" />
<category term="emacs" />
<category term="11ty" />
		<id>https://sachachua.com/blog/2025/03/mastodon-el-copy-toot-url-after-posting-also-copying-just-this-post-with-11ty/</id>
		<content type="html"><![CDATA[<p>
I often want to copy the toot URL after posting a
new toot about a blog post so that I can update
the blog post with it. Since I post from Emacs
using <a href="https://codeberg.org/martianh/mastodon.el">mastodon.el</a>, I can probably figure out how
to get the URL after tooting. A quick-and-dirty
way is to retrieve the latest status.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defvar</span> <span class="org-variable-name">my-mastodon-toot-posted-hook</span> nil <span class="org-doc">"Called with the item."</span>)

(<span class="org-keyword">defun</span> <span class="org-function-name">my-mastodon-copy-toot-url</span> (toot)
  (<span class="org-keyword">interactive</span> (list (my-mastodon-latest-toot)))
  (kill-new (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">url</span> toot)))
(add-hook <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">my-mastodon-toot-posted-hook</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">my-mastodon-copy-toot-url</span>)

(<span class="org-keyword">defun</span> <span class="org-function-name">my-mastodon-latest-toot</span> ()
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">require</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">mastodon-http</span>)
  (<span class="org-keyword">let*</span> ((json-array-type <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">list</span>)
         (json-object-type <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">alist</span>))
    (car
     (mastodon-http&#45;&#45;get-json
      (mastodon-http&#45;&#45;api
       (format <span class="org-string">"accounts/%s/statuses?count=1&amp;limit=1&amp;exclude_reblogs=t"</span>
               (mastodon-auth&#45;&#45;get-account-id)))
      nil <span class="org-builtin">:silent</span>))))

(<span class="org-keyword">with-eval-after-load</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">mastodon-toot</span>
  (<span class="org-keyword">when</span> (functionp <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">mastodon-toot-send</span>)
    (advice-add
     <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">mastodon-toot-send</span>
     <span class="org-builtin">:after</span>
     (<span class="org-keyword">lambda</span> (<span class="org-type">&amp;rest</span> _)
       (run-hook-with-args <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">my-mastodon-toot-posted-hook</span> (my-mastodon-latest-toot)))))
  (<span class="org-keyword">when</span> (functionp <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">mastodon-toot&#45;&#45;send</span>)
    (advice-add
     <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">mastodon-toot&#45;&#45;send</span>
     <span class="org-builtin">:after</span>
     (<span class="org-keyword">lambda</span> (<span class="org-type">&amp;rest</span> _)
       (run-hook-with-args <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">my-mastodon-toot-posted-hook</span> (my-mastodon-latest-toot))))))
</pre>
</div>


<p>
I considered overriding the keybinding in
<code>mastodon-toot-mode-map</code>, but I figured using
advice would mean I can copy things even after
automated toots.
</p>

<p>
A more elegant way to do this might be to modify
<code>mastodon-toot-send</code> to <code>run-hook-with-args</code> a
variable with the <code>response</code> as an argument, but
this will do for now.
</p>

<p>
I used a hook in my advice so that I can change
the behaviour from other functions. For example, I
have some code to compose a toot with <a href="https://sachachua.com/dotemacs#mastodon-tooting-a-link-to-the-current-post">a link to
the current post</a>. After I send a toot, I want to
check if the toot contains the current entry's
permalink. If it has and I don't have a Mastodon
toot field yet, maybe I can automatically set that
property, assuming I end up back in the Org Mode
file I started it from.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-mastodon-org-maybe-set-toot-url</span> (toot)
  (<span class="org-keyword">when</span> (derived-mode-p <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">org-mode</span>)
    (<span class="org-keyword">let</span> ((permalink (org-entry-get-with-inheritance <span class="org-string">"EXPORT_ELEVENTY_PERMALINK"</span>)))
      (<span class="org-keyword">when</span> (<span class="org-keyword">and</span> permalink
                 (string-match (regexp-quote permalink) (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">content</span> toot))
                 (not (org-entry-get-with-inheritance <span class="org-string">"MASTODON"</span>)))
        (<span class="org-keyword">save-excursion</span>
          (goto-char (org-find-property <span class="org-string">"EXPORT_ELEVENTY_PERMALINK"</span>
                                        permalink))
          (org-entry-put
           (point)
           <span class="org-string">"EXPORT_MASTODON"</span>
           (alist-get <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">url</span> toot))
          (message <span class="org-string">"Toot URL set: %s, republish if needed"</span> toot))))))
(add-hook <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">my-mastodon-toot-posted-hook</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">my-mastodon-org-maybe-set-toot-url</span>)
</pre>
</div>


<p>
If I combine that with a development copy of my
blog that ignores most of my posts so it compiles
faster and a function that copies just the current
post's files over, I can quickly make a post
available at its permalink (which means the link
in the toot will work) before I recompile the rest
of the blog, which takes a number of minutes.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-org-11ty-copy-just-this-post</span> ()
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">when</span> (derived-mode-p <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">org-mode</span>)
    (<span class="org-keyword">let</span> ((file (org-entry-get-with-inheritance <span class="org-string">"EXPORT_ELEVENTY_FILE_NAME"</span>))
          (path my-11ty-base-dir))
      (call-process <span class="org-string">"chmod"</span> nil nil nil <span class="org-string">"ugo+rwX"</span> <span class="org-string">"-R"</span> (expand-file-name file (expand-file-name <span class="org-string">"_local"</span> path)))
      (call-process <span class="org-string">"rsync"</span> nil (get-buffer-create <span class="org-string">"*rsync*"</span>) nil <span class="org-string">"-avze"</span> <span class="org-string">"ssh"</span>
                    (expand-file-name file (expand-file-name <span class="org-string">"_local"</span> path))
                    (concat <span class="org-string">"web:/var/www/static-blog/"</span> file))
      (browse-url (concat (replace-regexp-in-string <span class="org-string">"/$"</span> <span class="org-string">""</span> my-blog-base-url)
                          (org-entry-get-with-inheritance <span class="org-string">"EXPORT_ELEVENTY_PERMALINK"</span>))))))
</pre>
</div>


<p>
The proper blog updates (index page, RSS/ATOM
feeds, category pages, prev/next links, etc.) can happen when the
publishing is finished.
</p>

<p>
So my draft workflow is:
</p>

<ol class="org-ol">
<li>Write the post.</li>
<li>Export it to the local <code>NODE_ENV=dev npx eleventy &#45;&#45;serve &#45;&#45;quiet</code> with <a href="https://github.com/sachac/ox-11ty">ox-11ty</a>.</li>
<li>Check that it looks okay locally.</li>
<li>Use <code>my-org-11ty-copy-just-this-post</code> and confirm that it looks fine.</li>
<li>Compose a toot with <a href="https://sachachua.com/dotemacs#mastodon-tooting-a-link-to-the-current-post">my-mastodon-11ty-toot-post</a>
and check if sending it updates the Mastodon
toot.</li>
<li>Re-export the post.</li>
<li>Run my blog publishing process. <code>NODE_ENV=production npx eleventy &#45;&#45;quiet</code> and then rsync.</li>
</ol>

<p>
Let's see if this works&#x2026;
</p>

<div class="note">This is part of my <a href="https://sachachua.com/dotemacs#mastodon">Emacs configuration.</a></div><div><a href="https://sachachua.com/blog/2025/03/mastodon-el-copy-toot-url-after-posting-also-copying-just-this-post-with-11ty/index.org">View org source for this post</a></div><p>You can <a href="https://social.sachachua.com/@sacha/statuses/01JQ2TC154Z4YXG35A52NQPKG8" target="_blank" rel="noopener noreferrer">comment on Mastodon</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2025%2F03%2Fmastodon-el-copy-toot-url-after-posting-also-copying-just-this-post-with-11ty%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">On this day</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2025/03/on-this-day/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2025-03-23T16:02:57Z</updated>
    <published>2025-03-23T16:02:57Z</published>
    <category term="11ty" />
<category term="js" />
		<id>https://sachachua.com/blog/2025/03/on-this-day/</id>
		<content type="html"><![CDATA[<p>
Nudged by <a href="https://github.com/emacsomancer/org-daily-reflection">org-daily-reflection</a> (<a href="https://types.pl/@emacsomancer/114096616526351883">@emacsomancer's
toot</a>) and <a href="https://adactio.com/journal/21811">Jeremy Keith's post</a> where he mentions
his <a href="https://adactio.com/archive/onthisday">on this day</a> page, I finally got around to
making my own <a href="https://sachachua.com/blog/on-this-day">on this day</a> page again. I use the
11ty static site generator, so it's static unless
you have Javascript enabled. It might be good for
<a href="https://sachachua.com/blog/2024/11/how-do-i-want-to-get-better-at-learning-out-loud-part-1-of-4-starting/#org1a9a0b0">bumping into things</a>. I used to have an <a href="https://sachachua.com/blog/2014/03/notes-managing-large-blog-archive/#:~:text=I%20have%20an%20%E2%80%9COn%20this%20day%E2%80%9D%20widget">"On this
day" widget</a> back when I used Wordpress, which was
fun to look at occasionally.
</p>

<p>
The code might be a little adamant about
converting all the dates to America/Toronto:
</p>

<details><summary>11ty code for posts on this day</summary>
<div class="org-src-container">
<pre class="src src-js"><span class="org-keyword">export</span> <span class="org-keyword">default</span> <span class="org-keyword">class</span> OnThisDay {
  data() {
    <span class="org-keyword">return</span> {
      layout: <span class="org-string">'layouts/base'</span>,
      permalink: <span class="org-string">'/blog/on-this-day/'</span>,
      title: <span class="org-string">'On this day'</span>
    };
  }

  <span class="org-keyword">async</span> render(data) {
    <span class="org-keyword">const</span> <span class="org-variable-name">today</span> = <span class="org-keyword">new</span> <span class="org-type">Date</span>(<span class="org-keyword">new</span> <span class="org-type">Date</span>().toLocaleString(<span class="org-string">'en-US'</span>, { timeZone: <span class="org-string">'America/Toronto'</span> }));
    <span class="org-keyword">const</span> <span class="org-variable-name">options</span> = { month: <span class="org-string">'long'</span>, day: <span class="org-string">'numeric'</span> };
    <span class="org-keyword">const</span> <span class="org-variable-name">date</span> = today.toLocaleDateString(<span class="org-string">'en-US'</span>, options);
    <span class="org-keyword">const</span> <span class="org-variable-name">currentMonthDay</span> = today.toISOString().substring(5, 10);
    <span class="org-keyword">let</span> <span class="org-variable-name">list</span> = data.collections._posts
        .filter(post =&gt; {
          <span class="org-keyword">const</span> <span class="org-variable-name">postDateTime</span> = <span class="org-keyword">new</span> <span class="org-type">Date</span>(post.date).toLocaleString(<span class="org-string">'en-US'</span>, { timeZone: <span class="org-string">'America/Toronto'</span> });
          <span class="org-keyword">const</span> <span class="org-variable-name">postMonthDay</span> = (<span class="org-keyword">new</span> <span class="org-type">Date</span>(postDateTime)).toISOString().substring(5, 10);
          <span class="org-keyword">return</span> postMonthDay === currentMonthDay;
        })
        .sort((a, b) =&gt; {
          <span class="org-keyword">if</span> (a.date &lt; b.date) <span class="org-keyword">return</span> 1;
          <span class="org-keyword">if</span> (a.date &gt; b.date) <span class="org-keyword">return</span> -1;
          <span class="org-keyword">return</span> 0;
        })
        .map(post =&gt; {
          <span class="org-keyword">const</span> <span class="org-variable-name">postDateTime</span> = <span class="org-keyword">new</span> <span class="org-type">Date</span>(post.date).toLocaleString(<span class="org-string">'en-US'</span>, { timeZone: <span class="org-string">'America/Toronto'</span> });
          <span class="org-keyword">const</span> <span class="org-variable-name">postDate</span> = <span class="org-keyword">new</span> <span class="org-type">Date</span>(postDateTime);
          <span class="org-keyword">const</span> <span class="org-variable-name">postYear</span> = postDate.getFullYear();
          <span class="org-keyword">return</span> <span class="org-string">`&lt;li&gt;${postYear}: &lt;a href="${post.url}"&gt;${post.data.title}&lt;/a&gt;&lt;/li&gt;`</span>;
        })
        .join(<span class="org-string">'\n'</span>);
    list = list.length &gt; 0
      ? <span class="org-string">`&lt;ul&gt;${list}&lt;/ul&gt;`</span>
      : <span class="org-string">`&lt;p&gt;No posts were written on ${date} in previous years.&lt;/p&gt;`</span>;

    <span class="org-keyword">return</span> <span class="org-string">`&lt;section&gt;&lt;h2&gt;On this day&lt;/h2&gt;</span>
<span class="org-string">&lt;p&gt;This page lists posts written on this day throughout the years. If you've enabled Javascript, it will show the current day. If you don't, it'll show the posts from the day I last updated this blog. You might also like to explore &lt;a href="/blog/all"&gt;all posts&lt;/a&gt;, &lt;a href="/topic"&gt;a topic-based outline&lt;/a&gt; or &lt;a href="/blog/category"&gt;categories&lt;/a&gt;.&lt;/p&gt;</span>
<span class="org-string">&lt;h3 class="date"&gt;${date}&lt;/h3&gt;</span>
<span class="org-string">&lt;div id="posts-container"&gt;${list}&lt;/div&gt;</span>

<span class="org-string">&lt;script&gt;</span>
<span class="org-string">  $(document).ready(function() { onThisDay(); });</span>
<span class="org-string">&lt;/script&gt;</span>
<span class="org-string">&lt;/section&gt;`</span>;
  }
};
</pre>
</div>

</details>

<details><summary>Client-side Javascript for the dynamic list</summary>
<div class="org-src-container">
<pre class="src src-js2">function onThisDay() {
  const tz = 'America/Toronto';
  function getEffectiveDate() {
    const urlParams = new URLSearchParams(window.location.search);
    const dateParam = urlParams.get('date');
    if (dateParam &amp;&amp; /^\d{2}-\d{2}$/.test(dateParam)) {
      const currentYear = new Date().getFullYear();
      const dateObj = new Date(`${currentYear}-${dateParam}T12:00:00Z`);
      if (dateObj.getTime()) {
        return {
          monthDay: dateParam,
          formatted: dateObj.toLocaleDateString('en-US', { month: 'long', day: 'numeric' })
        };
      }
    }
    const today = new Date(new Date().toLocaleString('en-US', { timeZone: tz }));
    return {
      monthDay: today.toISOString().substring(5, 10), // MM-DD
      formatted: today.toLocaleDateString('en-US', { month: 'long', day: 'numeric' })
    };
  }
  // Fetch and process the posts
  fetch('/blog/all/index.json')
    .then(response =&gt; response.json())
    .then(posts =&gt; {
      const dateInfo = getEffectiveDate();
      const dateElement = document.querySelector('h3.date');
      if (dateElement) {
        dateElement.textContent = dateInfo.formatted;
      }
      const matchingPosts = posts.filter(post =&gt; {
        const postDate = new Date(post.date).toLocaleString('en-US', { timeZone: tz });
        const postMonthDay = (new Date(postDate)).toISOString().substring(5, 10);
        return postMonthDay === dateInfo.monthDay;
      });

      matchingPosts.sort((a, b) =&gt; {
        const dateA = new Date(a.date);
        const dateB = new Date(b.date);
        return dateB - dateA;
      });

      const elem = document.getElementById('posts-container');
      if (matchingPosts.length &gt; 0) {
        const postsHTML = matchingPosts.map(post =&gt; {
          const postDate = new Date(post.date).toLocaleString('en-US', { timeZone: tz });
          const postYear = new Date(postDate).getFullYear();
          return `&lt;li&gt;${postYear}: &lt;a href="${post.permalink}"&gt;${post.title}&lt;/a&gt;&lt;/li&gt;`;
        }).join('\n');
        elem.innerHTML = `&lt;ul&gt;${postsHTML}&lt;/ul&gt;`;
      } else {
        elem.innerHTML = `&lt;p&gt;No posts were written on ${dateInfo.formatted}.&lt;/p&gt;`;
      }
    })
    .catch(error =&gt; {
      console.error('Error fetching posts:', error);
    });
}
</pre>
</div>

</details>

<p>
I used to include the day's posts as a footer on
the individual blog post page. That might be
something to consider again.
</p>
<div><a href="https://sachachua.com/blog/2025/03/on-this-day/index.org">View org source for this post</a></div><p>You can <a href="https://social.sachachua.com/@sacha/statuses/01JQ1VKG7TBQXXZVSZS9X3JT3X" target="_blank" rel="noopener noreferrer">comment on Mastodon</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2025%2F03%2Fon-this-day%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry><entry>
		<title type="html">Organizing my visual book notes by topic</title>
		<link rel="alternate" type="text/html" href="https://sachachua.com/blog/2024/10/organizing-my-visual-book-notes-by-topic/"/>
		<author><name><![CDATA[Sacha Chua]]></name></author>
		<updated>2024-10-25T18:02:36Z</updated>
    <published>2024-10-25T18:02:36Z</published>
    <category term="blogging" />
<category term="11ty" />
		<id>https://sachachua.com/blog/2024/10/organizing-my-visual-book-notes-by-topic/</id>
		<content type="html"><![CDATA[<p>
I want to start building up more thoughts as
chunks and relating them more logically instead of
just chronologically. I've been using categories
to organize my posts into buckets, but within a
category, it's still chronological. I also have a
<a href="https://sachachua.com/blog/outline/">large outline</a> that includes posts from 2017
to 2024. I'd like to break it up into smaller
topic pages so that they're easier to link to,
although it's a little more challenging to search.
</p>

<p>
Now that I have a nice <a href="https://sachachua.com/blog/category/visual-book-notes/">gallery view for my visual
book notes</a>, I wanted to organize the book notes by
topic. I made an <a href="https://github.com/sachac/eleventy-blog-setup/blob/main/_includes/shortcodes/archive.js">async Eleventy paired shortcode called <code>gallerylist</code></a>
that lets me turn a list of links into into thumbnails
and links.
</p>

<p>
I also modified <code>org-html-toc</code> to not include the
Table of Contents header and to tweak the HTML
attributes assigned to it.
</p>

<details><summary>New table of contents code</summary>
<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-org-html-toc</span> (depth info <span class="org-type">&amp;optional</span> scope)
  <span class="org-doc">"Build a table of contents.</span>
<span class="org-doc">DEPTH is an integer specifying the depth of the table.  INFO is</span>
<span class="org-doc">a plist used as a communication channel.  Optional argument SCOPE</span>
<span class="org-doc">is an element defining the scope of the table.  Return the table</span>
<span class="org-doc">of contents as a string, or nil if it is empty."</span>
  (<span class="org-keyword">let</span> ((toc-entries
   (mapcar (<span class="org-keyword">lambda</span> (headline)
       (cons (org-html&#45;&#45;format-toc-headline headline info)
       (org-export-get-relative-level headline info)))
     (org-export-collect-headlines info depth scope))))
    (<span class="org-keyword">when</span> toc-entries
      (<span class="org-keyword">let*</span> ((toc-id-counter (plist-get info <span class="org-builtin">:org-html&#45;&#45;toc-counter</span>))
             (toc (concat (format <span class="org-string">"&lt;div class=\"text-table-of-contents toc-id%s\" role=\"doc-toc\"&gt;"</span>
                                  (<span class="org-keyword">if</span> toc-id-counter (format <span class="org-string">"-%d"</span> toc-id-counter) <span class="org-string">""</span>))
        (org-html&#45;&#45;toc-text toc-entries)
        <span class="org-string">"&lt;/div&gt;\n"</span>)))
        (plist-put info <span class="org-builtin">:org-html&#45;&#45;toc-counter</span> (1+ (<span class="org-keyword">or</span> toc-id-counter 0)))
  (<span class="org-keyword">if</span> scope toc
    (<span class="org-keyword">let</span> ((outer-tag (<span class="org-keyword">if</span> (org-html&#45;&#45;html5-fancy-p info)
             <span class="org-string">"nav"</span>
           <span class="org-string">"div"</span>)))
      (concat (format <span class="org-string">"&lt;%s class=\"table-of-contents toc-id%s\" role=\"doc-toc\"&gt;\n"</span>
                            outer-tag
                            (<span class="org-keyword">if</span> toc-id-counter (format <span class="org-string">"-%d"</span> toc-id-counter) <span class="org-string">""</span>))
              <span class="org-comment-delimiter">;; </span><span class="org-comment">(let ((top-level (plist-get info :html-toplevel-hlevel)))</span>
              <span class="org-comment-delimiter">;; </span><span class="org-comment">(format "&lt;h%d&gt;%s&lt;/h%d&gt;\n"</span>
              <span class="org-comment-delimiter">;;   </span><span class="org-comment">top-level</span>
              <span class="org-comment-delimiter">;;   </span><span class="org-comment">(org-html&#45;&#45;translate "Table of Contents" info)</span>
              <span class="org-comment-delimiter">;;   </span><span class="org-comment">top-level))</span>
        toc
        (format <span class="org-string">"&lt;/%s&gt;\n"</span> outer-tag))))))))

(<span class="org-keyword">with-eval-after-load</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">org</span>
  (<span class="org-keyword">defalias</span> <span class="org-highlight-quoted-quote">'</span><span class="org-function-name">org-html-toc</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">my-org-html-toc</span>))
</pre>
</div>

</details>

<p>
This is what my <a href="https://sachachua.com/topic/visual-book-notes/">visual book notes topic page</a> looks like now:
</p>


<figure id="org005a8be">
<a href="https://sachachua.com/topic/visual-book-notes/" class="box-shadow"><img src="https://sachachua.com/blog/2024/10/organizing-my-visual-book-notes-by-topic/2024-10-25_09-23-26.png" alt="2024-10-25_09-23-26.png" class="box-shadow"></a>

<figcaption><span class="figure-number">Figure 1: </span>Screenshot of visual book notes</figcaption>
</figure>

<p>
I can improve on this by using the topic maps to determine next/previous links for the posts. Someday!
</p>
<div><a href="https://sachachua.com/blog/2024/10/organizing-my-visual-book-notes-by-topic/index.org">View org source for this post</a></div><p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2024%2F10%2Forganizing-my-visual-book-notes-by-topic%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></content>
		</entry>
</feed>