<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.9.5">Jekyll</generator><link href="https://josephtesfaye.com/blog/feed.xml" rel="self" type="application/atom+xml" /><link href="https://josephtesfaye.com/blog/" rel="alternate" type="text/html" /><updated>2026-03-06T02:41:14+00:00</updated><id>https://josephtesfaye.com/blog/feed.xml</id><title type="html">Orcharinth</title><subtitle>A curated sanctuary where the complexity of code and science meets the organic rhythm of art, health, and thought.</subtitle><author><name>Joseph Huang</name></author><entry><title type="html">Interview</title><link href="https://josephtesfaye.com/blog/web/interview/" rel="alternate" type="text/html" title="Interview" /><published>2026-03-04T00:00:00+00:00</published><updated>2026-03-04T00:00:00+00:00</updated><id>https://josephtesfaye.com/blog/web/interview</id><content type="html" xml:base="https://josephtesfaye.com/blog/web/interview/"><![CDATA[<h1 id="introduce-yourself">Introduce yourself</h1>
<p>Thank you for having me. My name is Huang Jicheng, and you can call me Joseph. I graduated with a 4-year bachelor’s degree from Macao Polytechnic University in Macao SAR, and since then I’ve been working as a software engineer for more than eight years, mainly focusing on backend development using Java and cloud platforms like AWS. Most of my experience comes from building large-scale systems for E-Commerce, ERP, and mobility services, etc. I’m fluent in English, a native Chinese speaker, and my Japanese is at an intermediate level, with which I’m stronger in reading and writing, weaker in conversation, which I’m continuously improving. I’m especially excited about joining [Company Name] to contribute my experience in Java backend development and distributed systems.</p>
<p><ruby>本日<rt>ほんじつ</rt></ruby>はお<ruby>時間<rt>じかん</rt></ruby>をいただき、ありがとうございます。黄級誠と<ruby>申<rt>もう</rt></ruby>します。コウとお<ruby>呼<rt>よ</rt></ruby>びください。私はマカオにあるマカオ<ruby>理<rt>り</rt>工<rt>こう</rt>大<rt>だい</rt>学<rt>がく</rt></ruby>を4<ruby>年<rt>ねん</rt>制<rt>せい</rt></ruby>の<ruby>学士<rt>がくし</rt></ruby><ruby>課程<rt>かてい</rt></ruby>で<ruby>卒業<rt>そつぎょう</rt></ruby>しました。それ<ruby>以<rt>い</rt>降<rt>こう</rt></ruby>、ソフトウェアエンジニアとして８<ruby>年<rt>ねん</rt></ruby><ruby>以上<rt>いじょう</rt></ruby>の<ruby>実<rt>じつ</rt>務<rt>む</rt></ruby><ruby>経<rt>けい</rt>験<rt>けん</rt></ruby>があり、<ruby>主<rt>おも</rt></ruby>にジャバ（Java）とエーダブリューエス（AWS）などのクラウドプラットフォーム（cloud platform）を<ruby>使用<rt>しよう</rt></ruby>したバックエンド<ruby>開<rt>かい</rt>発<rt>はつ</rt></ruby>に<ruby>注<rt>ちゅう</rt>力<rt>りょく</rt></ruby>してきました。<ruby>経験<rt>けいけん</rt></ruby>の<ruby>多<rt>おお</rt></ruby>くは、Eコマース（ecommerce）やイーアールピー（ERP）、モビリティサービス（mobility service）などの<ruby>大<rt>だい</rt></ruby><ruby>規<rt>き</rt></ruby> <ruby>模<rt>ぼ</rt></ruby>システムの<ruby>構築<rt>こうちく</rt></ruby>によるものです。<ruby>語<rt>ご</rt>学<rt>がく</rt></ruby>については、<ruby>中国<rt>ちゅうごく</rt>語<rt>ご</rt></ruby>が<ruby>母<rt>ぼ</rt>語<rt>ご</rt></ruby>で、英語はビジネスレベルで<ruby>流<rt>りゅう</rt>暢<rt>ちょう</rt></ruby>に<ruby>話<rt>はな</rt></ruby>せます。日本語は<ruby>中<rt>ちゅう</rt>級<rt>きゅう</rt></ruby>レベルで、<ruby>読<rt>よ</rt></ruby>み<ruby>書<rt>か</rt></ruby> きは<ruby>得意<rt>とくい</rt></ruby>ですが<ruby>会話<rt>かいわ</rt></ruby>はまだ<ruby>苦手<rt>にて</rt></ruby>な<ruby>部分<rt>ぶぶん</rt></ruby>があります。これまでのジャババックエンド（Java backend）<ruby>開<rt>かい</rt>発<rt>はつ</rt></ruby>や<ruby>分<rt>ぶん</rt>散<rt>さん</rt></ruby>システムの<ruby>経<rt>けい</rt>験<rt>けん</rt></ruby>を<ruby>活<rt>い</rt></ruby>かして、<ruby>貴<rt>き</rt>社<rt>しゃ</rt></ruby>に<ruby>貢<rt>こう</rt>献<rt>けん</rt></ruby>できることを<ruby>非<rt>ひ</rt>常<rt>じょう</rt></ruby>に<ruby>楽<rt>たの</rt></ruby>しみにしています。どうぞよろしくお<ruby>願<rt>ねが</rt></ruby>いいたします。</p>
<h1 id="why-do-you-come-to-japan">Why do you come to Japan?</h1>
<p>I came to Japan because I’m genuinely interested in Japanese society and culture. Living here allows me to experience the culture firsthand and communicate more effectively with local teams. I see Japan as a place where I can grow professionally and personally, while contributing meaningful value to the products I work on.</p>
<p>日本の社会や文化に<ruby>心<rt>こころ</rt></ruby>から<ruby>興<rt>きょう</rt>味<rt>み</rt></ruby>があり、<ruby>来<rt>らい</rt>日<rt>にち</rt></ruby>いたしました。<ruby>実<rt>じっ</rt>際<rt>さい</rt></ruby>にこちらで<ruby>生<rt>せい</rt>活<rt>かつ</rt></ruby>することで、文化を<ruby>肌<rt>はだ</rt></ruby>で<ruby>感<rt>かん</rt></ruby>じながら、<ruby>現<rt>げん</rt>地<rt>ち</rt></ruby>のチームともより<ruby>円<rt>えん</rt>滑<rt>かつ</rt></ruby> にコミュニケーションをとることが<ruby>可能<rt>かのう</rt></ruby>になると<ruby>考<rt>かん</rt></ruby>えています。私にとって、日本はエンジニアとして、また<ruby>一人<rt>ひとり</rt></ruby>の<ruby>人間<rt>にんげん</rt></ruby>としても<ruby>成<rt>せい</rt>長<rt>ちょう</rt></ruby>できる<ruby>場<rt>ば</rt></ruby>であり、<ruby>携<rt>たずさ</rt></ruby>わる<ruby>製<rt>せい</rt>品<rt>ひん</rt></ruby>を<ruby>通<rt>とお</rt></ruby>じて<ruby>有<rt>ゆう</rt>意<rt>い</rt>義<rt>ぎ</rt></ruby>な<ruby>価<rt>か</rt>値<rt>ち</rt></ruby>を<ruby>提<rt>てい</rt>供<rt>きょう</rt></ruby>していきたいと<ruby>考<rt>かん</rt></ruby>えております。</p>]]></content><author><name>Joseph Huang</name></author><category term="Web" /><category term="Minimal Mistakes" /><summary type="html"><![CDATA[Introduce yourself]]></summary></entry><entry><title type="html">Enabling Sticky TOC for Mobile Display in Minimal Mistakes</title><link href="https://josephtesfaye.com/blog/web/mobile-sticky-toc/" rel="alternate" type="text/html" title="Enabling Sticky TOC for Mobile Display in Minimal Mistakes" /><published>2026-03-03T00:00:00+00:00</published><updated>2026-03-03T00:00:00+00:00</updated><id>https://josephtesfaye.com/blog/web/mobile-sticky-toc</id><content type="html" xml:base="https://josephtesfaye.com/blog/web/mobile-sticky-toc/"><![CDATA[<p>Currently Minimal Mistakes doesn’t support sticky Table of Contents for mobile display, even if you have set <code>toc_sticky: true</code> (see <a href="https://mmistakes.github.io/minimal-mistakes/docs/layouts/#table-of-contents">Table of contents</a>). Now I want to enable it for mobile with minimal changes so that there is a sticky TOC icon on the screen and when clicked it shows/hides the TOC. This should not affect the desktop display and should apply regardless of the drafting format in use (Org or Markdown).</p>
<p>The fastest, most efficient approach to implement this is to rip the <code>.toc</code> out of the standard mobile document flow, apply fixed positioning to turn it into a popup, and inject a floating toggle button using JavaScript.</p>
<p>Because Minimal Mistakes builds the <code>toc_sticky</code> sidebar via Liquid layouts, this DOM-level manipulation works perfectly regardless of whether the post was drafted in Org-mode or Markdown.</p>
<p>Here are the specific updates you need to make to your files.</p>
<ol> <li>
<p>Update your Stylesheet </p>
<p>Append this CSS to the bottom of <code>assets/css/main.scss</code>. This code uses Minimal Mistakes’ standard <code>$large</code> breakpoint (64em or 1024px) to transform the TOC into a floating modal, while inheriting the native light/dark theme colors already defined by the <code>.toc</code> class.</p>  <div class="language-scss highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="c1">// Mobile Sticky TOC Toggle</span>
<span class="nc">.toc-mobile-btn</span> <span class="p">{</span>
  <span class="nl">display</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">@media</span> <span class="p">(</span><span class="n">max-width</span><span class="o">:</span> <span class="m">64em</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// 1. The floating button</span>
  <span class="nc">.has-toc</span> <span class="nc">.toc-mobile-btn</span> <span class="p">{</span>
    <span class="nl">display</span><span class="p">:</span> <span class="n">flex</span><span class="p">;</span>
    <span class="nl">position</span><span class="p">:</span> <span class="nb">fixed</span><span class="p">;</span>
    <span class="nl">bottom</span><span class="p">:</span> <span class="m">25px</span><span class="p">;</span>
    <span class="nl">right</span><span class="p">:</span> <span class="m">25px</span><span class="p">;</span>
    <span class="nl">z-index</span><span class="p">:</span> <span class="m">9999</span><span class="p">;</span>
    <span class="nl">background</span><span class="p">:</span> <span class="nf">rgba</span><span class="p">(</span><span class="m">128</span><span class="o">,</span> <span class="m">128</span><span class="o">,</span> <span class="m">128</span><span class="o">,</span> <span class="m">0</span><span class="mi">.2</span><span class="p">);</span> <span class="c1">// Light/Dark mode agnostic</span>
    <span class="na">backdrop-filter</span><span class="p">:</span> <span class="nf">blur</span><span class="p">(</span><span class="m">5px</span><span class="p">);</span>
    <span class="na">-webkit-backdrop-filter</span><span class="p">:</span> <span class="nf">blur</span><span class="p">(</span><span class="m">5px</span><span class="p">);</span>
    <span class="nl">color</span><span class="p">:</span> <span class="nb">inherit</span><span class="p">;</span>
    <span class="nl">border</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="nf">rgba</span><span class="p">(</span><span class="m">128</span><span class="o">,</span> <span class="m">128</span><span class="o">,</span> <span class="m">128</span><span class="o">,</span> <span class="m">0</span><span class="mi">.3</span><span class="p">);</span>
    <span class="nl">border-radius</span><span class="p">:</span> <span class="m">50%</span><span class="p">;</span>
    <span class="nl">width</span><span class="p">:</span> <span class="m">50px</span><span class="p">;</span>
    <span class="nl">height</span><span class="p">:</span> <span class="m">50px</span><span class="p">;</span>
    <span class="nl">font-size</span><span class="p">:</span> <span class="m">1</span><span class="mi">.2rem</span><span class="p">;</span>
    <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">4px</span> <span class="m">10px</span> <span class="nf">rgba</span><span class="p">(</span><span class="m">0</span><span class="o">,</span> <span class="m">0</span><span class="o">,</span> <span class="m">0</span><span class="o">,</span> <span class="m">0</span><span class="mi">.15</span><span class="p">);</span>
    <span class="nl">cursor</span><span class="p">:</span> <span class="nb">pointer</span><span class="p">;</span>
    <span class="nl">align-items</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
    <span class="nl">justify-content</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
    <span class="nl">transition</span><span class="p">:</span> <span class="n">transform</span> <span class="m">0</span><span class="mi">.2s</span> <span class="n">ease</span><span class="p">;</span>

    <span class="k">&amp;</span><span class="nd">:active</span> <span class="p">{</span>
      <span class="nl">transform</span><span class="p">:</span> <span class="nf">scale</span><span class="p">(</span><span class="m">0</span><span class="mi">.95</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="c1">// 2. The TOC Popup Panel</span>
  <span class="nc">.sidebar__right.sticky</span> <span class="nc">.toc</span> <span class="p">{</span>
    <span class="nl">position</span><span class="p">:</span> <span class="nb">fixed</span><span class="p">;</span>
    <span class="nl">bottom</span><span class="p">:</span> <span class="m">90px</span><span class="p">;</span>
    <span class="nl">right</span><span class="p">:</span> <span class="m">25px</span><span class="p">;</span>
    <span class="nl">width</span><span class="p">:</span> <span class="m">320px</span><span class="p">;</span>
    <span class="nl">max-width</span><span class="p">:</span> <span class="nf">calc</span><span class="p">(</span><span class="m">100vw</span> <span class="o">-</span> <span class="m">50px</span><span class="p">);</span>
    <span class="nl">max-height</span><span class="p">:</span> <span class="m">60vh</span><span class="p">;</span>
    <span class="nl">overflow-y</span><span class="p">:</span> <span class="nb">auto</span><span class="p">;</span>
    <span class="nl">z-index</span><span class="p">:</span> <span class="m">9998</span><span class="p">;</span>
    <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">5px</span> <span class="m">25px</span> <span class="nf">rgba</span><span class="p">(</span><span class="m">0</span><span class="o">,</span> <span class="m">0</span><span class="o">,</span> <span class="m">0</span><span class="o">,</span> <span class="m">0</span><span class="mi">.2</span><span class="p">);</span>
    <span class="nl">margin</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>

    <span class="c1">// Animation states</span>
    <span class="nl">opacity</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
    <span class="nl">visibility</span><span class="p">:</span> <span class="nb">hidden</span><span class="p">;</span>
    <span class="nl">transform</span><span class="p">:</span> <span class="nf">translateY</span><span class="p">(</span><span class="m">20px</span><span class="p">);</span>
    <span class="nl">transition</span><span class="p">:</span> <span class="n">all</span> <span class="m">0</span><span class="mi">.3s</span> <span class="nf">cubic-bezier</span><span class="p">(</span><span class="m">0</span><span class="mi">.25</span><span class="o">,</span> <span class="m">0</span><span class="mi">.8</span><span class="o">,</span> <span class="m">0</span><span class="mi">.25</span><span class="o">,</span> <span class="m">1</span><span class="p">);</span>

    <span class="k">&amp;</span><span class="nc">.is-active</span> <span class="p">{</span>
      <span class="nl">opacity</span><span class="p">:</span> <span class="m">1</span><span class="p">;</span>
      <span class="nl">visibility</span><span class="p">:</span> <span class="nb">visible</span><span class="p">;</span>
      <span class="nl">transform</span><span class="p">:</span> <span class="nf">translateY</span><span class="p">(</span><span class="m">0</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span></code></pre></div> </div>  </li> <li>
<p>Inject the Button via JavaScript </p>
<p>To ensure the button doesn’t clutter pages that don’t have a TOC, we dynamically inject it. Append this snippet to the very end of your <code>_includes/head/custom.html</code> file:</p>  <div class="language-html highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script&gt;</span>
  <span class="kd">function</span> <span class="nx">initMobileTOC</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">toc</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">.sidebar__right.sticky .toc</span><span class="dl">'</span><span class="p">);</span>
    <span class="c1">// Only initialize if a sticky TOC exists and the button hasn't been added yet</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">toc</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">.toc-mobile-btn</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
      <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="dl">'</span><span class="s1">has-toc</span><span class="dl">'</span><span class="p">);</span>

      <span class="kd">var</span> <span class="nx">btn</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">button</span><span class="dl">'</span><span class="p">);</span>
      <span class="nx">btn</span><span class="p">.</span><span class="nx">className</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">toc-mobile-btn</span><span class="dl">'</span><span class="p">;</span>
      <span class="nx">btn</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">&lt;i class="fas fa-list-ul"&gt;&lt;/i&gt;</span><span class="dl">'</span><span class="p">;</span>
      <span class="nx">btn</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">aria-label</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Toggle Table of Contents</span><span class="dl">'</span><span class="p">);</span>
      <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">btn</span><span class="p">);</span>

      <span class="c1">// Toggle TOC visibility on button click</span>
      <span class="nx">btn</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">click</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">e</span><span class="p">.</span><span class="nx">stopPropagation</span><span class="p">();</span>
        <span class="nx">toc</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">toggle</span><span class="p">(</span><span class="dl">'</span><span class="s1">is-active</span><span class="dl">'</span><span class="p">);</span>
      <span class="p">});</span>

      <span class="c1">// Close TOC when clicking anywhere outside of it</span>
      <span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">click</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">toc</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">contains</span><span class="p">(</span><span class="dl">'</span><span class="s1">is-active</span><span class="dl">'</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nx">toc</span><span class="p">.</span><span class="nx">contains</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span> <span class="o">!==</span> <span class="nx">btn</span><span class="p">)</span> <span class="p">{</span>
          <span class="nx">toc</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">remove</span><span class="p">(</span><span class="dl">'</span><span class="s1">is-active</span><span class="dl">'</span><span class="p">);</span>
        <span class="p">}</span>
      <span class="p">});</span>

      <span class="c1">// Automatically close the TOC popup after tapping a link inside it</span>
      <span class="c1">// var tocLinks = toc.querySelectorAll('a');</span>
      <span class="c1">// tocLinks.forEach(function(link) {</span>
      <span class="c1">//   link.addEventListener('click', function() {</span>
      <span class="c1">//     if (window.innerWidth </span><span class="o">&lt;=</span> <span class="mi">1024</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// Matches the 64em media query</span>
      <span class="c1">//       toc.classList.remove('is-active');</span>
      <span class="c1">//     }</span>
      <span class="c1">//   });</span>
      <span class="c1">// });</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">DOMContentLoaded</span><span class="dl">'</span><span class="p">,</span> <span class="nx">initMobileTOC</span><span class="p">);</span>
<span class="nt">&lt;/script&gt;</span></code></pre></div> </div>  </li> <li>
<p>Handle Encrypted Posts (Optional Safety Net) </p>
<p>Just in case you ever decide to disable <code>toc_sticky</code> on an encrypted post (which moves the TOC completely inside the encrypted <code>section.page__content</code> container), it is best practice to wake this script up after decryption.</p> <p>Open <code>_includes/secure_ui.html</code> and drop this block right next to your Gumshoe initialization:</p>  <div class="language-js highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="c1">// Re-initialize Mobile TOC Toggle</span>
<span class="k">try</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="k">typeof</span> <span class="nx">initMobileTOC</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">function</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">initMobileTOC</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">warn</span><span class="p">(</span><span class="dl">"</span><span class="s2">Mobile TOC failed to load</span><span class="dl">"</span><span class="p">,</span> <span class="nx">e</span><span class="p">);</span>
<span class="p">}</span></code></pre></div> </div>  </li> </ol>]]></content><author><name>Joseph Huang</name></author><category term="Web" /><category term="Minimal Mistakes" /><summary type="html"><![CDATA[Currently Minimal Mistakes doesn’t support sticky Table of Contents for mobile display, even if you have set toc_sticky: true (see Table of contents). Now I want to enable it for mobile with minimal changes so that there is a sticky TOC icon on the screen and when clicked it shows/hides the TOC. This should not affect the desktop display and should apply regardless of the drafting format in use (Org or Markdown).]]></summary></entry><entry><title type="html">Enabling drafting in Org Mode in Minimal Mistakes</title><link href="https://josephtesfaye.com/blog/web/enable-org-mode/" rel="alternate" type="text/html" title="Enabling drafting in Org Mode in Minimal Mistakes" /><published>2026-02-23T00:00:00+00:00</published><updated>2026-03-01T00:00:00+00:00</updated><id>https://josephtesfaye.com/blog/web/enable-org-mode</id><content type="html" xml:base="https://josephtesfaye.com/blog/web/enable-org-mode/"><![CDATA[<p>From now on I will be able to write posts in Org Mode for this blog, besides the existing Markdown method. Here’s how I configure it. This article is written in an Org file and also serves as a demonstration of this new feature.</p>
<p>First, you can do everything in the original Markdown way, which is kept intact. Now you can write everything in Org Mode with the extra benefit of using front matter, Liquid syntax, figures, galleries, etc. exactly the same as in Markdown.</p>
<p>Here are the basic elements demonstrated:</p>
<h1 id="org-elements-examples">Org Elements Examples</h1>
<h2 id="org-elements-examples-headings">Headings</h2>
<h3 id="20260301173459">Custom ID</h3>
<p>In Markdown you can write the attribute syntax <code>{: id="your-custom-id"}</code> to assign a custom ID to a heading. In Org you can achieve the same by using the conventional <code>CUSTOM_ID</code> in a property drawer.</p>
<h3 id="org-elements-examples-headings-topdown-ids-and-id-deduplication">Topdown IDs and ID Deduplication</h3>
<p>By default, heading <code>id</code>-s are generated automatically by transforming the heading to a hyphenated string. By adding <code>heading_ids: topdown</code> in the front matter you can enable topdown IDs, which are generated by concatenating for each heading all the <code>id</code>-s of its parent headings, if any. For example,</p>

            <div class="language-org highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>* Heading 1
# id: heading-1
** Foo
# id: heading-1-foo
*** Bar
# id: heading-1-foo-bar
* Heading 2
# id: heading-2
** Foo
# id: heading-2-foo
*** Bar
# id: heading-2-foo-bar</code></pre></div> </div>
          
<p>At any rate, there is a duplication check during the generation. If a duplicate ID is found, a number will be appended incrementally.</p>
<p>However, <code>CUSTOM_ID</code> is never affected.</p>
<h3 id="org-elements-examples-headings-heading-numbering">Heading Numbering</h3>
<p>Headings can be numbered automatically. You can give numbers to all headings of a post or only the headings of a subtree:</p>
<ul> <li>For all headings: Add <code>ordered: true</code> to the front matter of the post.</li> <li>
<p>For all headings under a subtree: Add the attribute <code>{: .ordered}</code> right under a heading. All of its sub-headings will be numbered. For example: </p>
<div class="language-org highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>*** Headings A
{: .ordered}
**** Heading 1
***** Heading 1.1
****** Heading 1.1.1
***** Heading 1.2
**** Heading 2
***** Heading 2.1

*** Headings B
**** Heading B1
{: .ordered}
***** Heading B1.1
****** Heading B1.1.1
***** Heading B1.2
**** Heading B2
***** Heading B2.1</code></pre></div> </div>  <p>Output:</p>  <div class="language-text highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>* Headings
** Headings A
1. Heading 1
1.1. Heading 1.1
1.1.1. Heading 1.1.1
1.2. Heading 1.2
2. Heading 2
2.1. Heading 2.1
** Headings B
*** Heading 1
1. Heading 1.1
1.1. Heading 1.1.1
2. Heading 1.2
*** Heading 2
**** Heading 2.1</code></pre></div> </div>  <p>If a heading is marked with the <code>{: .ordered}</code> attribute the numbering for its sub-headings should always start from <code>1</code> regardless of its level in the document or its position relative its siblings.</p> </li> </ul>
<p>If both <code>ordered: true</code> and <code>{: .ordered}</code> are present for a heading, the rules of the latter should override the former.</p>
<h4 id="org-elements-examples-headings-heading-numbering-heading-a" class="ordered">Heading A</h4>

<h5 id="org-elements-examples-headings-heading-numbering-heading-a-heading-1">Heading 1</h5>
<h6 id="org-elements-examples-headings-heading-numbering-heading-a-heading-1-heading-11">Heading 1.1</h6>Heading 1.1.1
<h6 id="org-elements-examples-headings-heading-numbering-heading-a-heading-1-heading-12">Heading 1.2</h6>
<h5 id="org-elements-examples-headings-heading-numbering-heading-a-heading-2">Heading 2</h5>
<h6 id="org-elements-examples-headings-heading-numbering-heading-a-heading-2-heading-21">Heading 2.1</h6>
<h4 id="org-elements-examples-headings-heading-numbering-heading-b">Heading B</h4>
<h5 id="org-elements-examples-headings-heading-numbering-heading-b-heading-b1" class="ordered">Heading B1</h5>

<h6 id="org-elements-examples-headings-heading-numbering-heading-b-heading-b1-heading-b11">Heading B1.1</h6>Heading B1.1.1
<h6 id="org-elements-examples-headings-heading-numbering-heading-b-heading-b1-heading-b12">Heading B1.2</h6>
<h5 id="org-elements-examples-headings-heading-numbering-heading-b-heading-b2">Heading B2</h5>
<h6 id="org-elements-examples-headings-heading-numbering-heading-b-heading-b2-heading-b21">Heading B2.1</h6>
<h2 id="org-elements-examples-tables">Tables</h2>
<p>Here is the morphological breakdown:</p>
<table>     <thead><tr>
<th>Component</th>
<th>Origin</th>
<th>Meaning</th>
</tr></thead>
<tbody>
<tr>
<td>Erythro-</td>
<td>Greek (erythros)</td>
<td>Red</td>
</tr>
<tr>
<td>-xyl-</td>
<td>Greek (xylon)</td>
<td>Wood</td>
</tr>
<tr>
<td>-aceae</td>
<td>Latin (Suffix)</td>
<td>Belonging to the family of</td>
</tr>
</tbody>
</table>
<h2 id="org-elements-examples-lists">Lists</h2>
<p>An ordered list with multiple levels:</p>
<ol> <li>
<p>First item </p>
<div class="language-text highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>Hello world!</code></pre></div> </div>  </li> <li>
<p>Second item </p>
<ol> <li>
<p>Sub-item A (Indented 3 spaces) </p>
<p>Some text</p> <ol> <li>Sub-item A (Indented 3 spaces)</li> <li>
<p>Sub-item A (Indented 3 spaces) </p>
<ol> <li>Sub-item A (Indented 3 spaces)</li> <li>Sub-item A (Indented 3 spaces)</li> </ol> </li> <li>Sub-item A (Indented 3 spaces)</li> </ol> </li> <li>
<p>Sub-item B </p>
<ul> <li>Deeply nested bullet (Indented another 3 spaces)</li> <li>Deeply nested bullet (Indented another 3 spaces)</li> </ul> </li> </ol> </li> <li>Third item</li> </ol>
<h2 id="org-elements-examples-links">Links</h2>
<h3 id="org-elements-examples-links-link-abbreviations">Link Abbreviations</h3>
<p>In Markdown you can use <a href="/blog/web/link-abbr/">link abbreviations</a> to expand links throughout the document. Now you can do the same in an Org document, heeding Org Mode’s own built-in link abbreviation method, i.e., <code>#+LINK</code>.</p>
<p>For example:</p>

            <div class="language-org highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>---
title: "Org Links"
link_abbrs:
  - link_abbr: foo https://i.postimg.cc/Vv8jFw8D/
gallery:
  - url: foo:unsplash-gallery-image-1.jpg
    image_path: foo:unsplash-gallery-image-1.jpg
    title: Check this [[https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg][image]]
  - image_path: foo:unsplash-gallery-image-1.jpg
  - foo:unsplash-gallery-image-1.jpg
  - image_path: [[https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg]]
  - [[https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg]]
  - [[https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg]]
---

#+LINK: foo ~/Downloads/temp/archive/image/foo/

#+CAPTION: This is an image
[[https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg]]

{% include figure image_path="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" popup=true
alt="" caption="A figure" %}

{% include gallery columns=3 caption="This is a gallery" %}</code></pre></div> </div>
          
<p>Here <code>#+LINK</code> is used in Org Mode, also for Jekyll build. It acts as a fallback to <code>link_abbrs</code> in the front matter, i.e., if an abbreviation isn’t found in <code>link_abbrs</code> but found in <code>#+LINK</code>, also use it to expand links in the built site. <code>#+LINK</code> can appear anywhere in the document except the front matter.</p>
<p><img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg"></p>






<figure class="custom-grid " style="grid-template-columns: repeat(6, 1fr);"> <figure> <a href="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" title="&lt;p&gt;Check this [[https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg][image]]&lt;/p&gt;"> <img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt=""> </a> </figure> <figure> <a href="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg"> <img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt=""> </a> </figure> <figure> <a href="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg"> <img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt=""> </a> </figure> <figure> <a href="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" title="&lt;p&gt;An image referenced through an org link&lt;/p&gt;"> <img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt=""> </a> </figure> <figure> <a href="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg"> <img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt=""> </a> </figure> <figure> <a href="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg"> <img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt=""> </a> </figure> <figcaption>A gallery </figcaption> </figure>



<h2 id="org-elements-examples-images">Images</h2>
<p>A simple image:</p>
<p><img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg"></p>
<p>A figure:</p>
<figure class=""><a href="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" class="image-popup" title="This is a figure caption
"><img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt="This is a figure alt text"></a><figcaption> This is a figure caption </figcaption></figure>

<p>A gallery (with <a href="/blog/web/custom-gallery/">customization</a>):</p>






<figure class="custom-grid " style="grid-template-columns: repeat(6, 1fr);"> <figure> <a href="/blog/assets/archive/image/foo/unsplash-gallery-image-1.jpg" title='&lt;p&gt;An image referenced through a &lt;a href="/blog/web/local-images/#method-2-using-symlinks"&gt;symlink&lt;/a&gt;&lt;/p&gt;'> <img src="/blog/assets/archive/image/foo/unsplash-gallery-image-1.jpg" alt=""> </a> </figure> <figure> <a href="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" title='&lt;p&gt;An image referenced through a &lt;a href="/blog/web/link-abbr/"&gt;link abbreviation&lt;/a&gt;&lt;/p&gt;'> <img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt=""> </a> </figure> <figure> <a href="/blog/assets/archive/image/foo/unsplash-gallery-image-1.jpg" title="&lt;p&gt;An image referenced through both a link abbreviation and a symlink&lt;/p&gt;"> <img src="/blog/assets/archive/image/foo/unsplash-gallery-image-1.jpg" alt=""> </a> </figure> <figcaption>This is a gallery caption </figcaption> </figure>



<h2 id="org-elements-examples-code-highlighting">Code highlighting</h2>
<p>Inline codes: <code>print</code>, <code>hello</code></p>
<p>Using <code>:</code> to start a line of code:</p>
<pre class="example">
descriptor + subject + taxonomic rank
</pre>
<p>Using structure template (code block):</p>

            <div class="language-ruby highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">print_hi</span><span class="p">(</span><span class="nb">name</span><span class="p">)</span>
  <span class="nb">puts</span> <span class="s2">"Hi, </span><span class="si">#{</span><span class="nb">name</span><span class="si">}</span><span class="s2">"</span>
<span class="k">end</span>
<span class="n">print_hi</span><span class="p">(</span><span class="s1">'Tom'</span><span class="p">)</span>

<span class="c1">#=&gt; prints 'Hi, Tom' to STDOUT.</span></code></pre></div> </div>
          
<p>Text block:</p>

            <div class="language-text highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>Hello
{% include gallery columns=6 caption="My custom gallery" %}
{% include gallery columns=6 caption="My custom gallery" %}</code></pre></div> </div>
          
<h2 id="org-elements-examples-notices">Notices</h2>

            <div class="language-markdown highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>{: .notice--warning}
<span class="ge">*Watch out!*</span> This is a warning notice inside an Org file.</code></pre></div> </div>
          
<p class="notice"> <b>Watch out!</b> This is a warning notice inside an Org file.</p>
<p class="notice--primary"> <b>Watch out!</b> This is a warning notice inside an Org file.</p>
<p class="notice--info"> <b>Watch out!</b> This is a warning notice inside an Org file.</p>
<p class="notice--warning"> <b>Watch out!</b> This is a warning notice inside an Org file.</p>
<p class="notice--success"> <b>Watch out!</b> This is a warning notice inside an Org file.</p>
<p class="notice--danger"> <b>Watch out!</b> This is a warning notice inside an Org file.</p>
<h2 id="org-elements-examples-furigana">Furigana</h2>
<p>You can write furigana for Japanese Kanji words like the following:</p>

            <div class="language-text highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>本|日（ほん|じつ）はお時|間（じ|かん）をいただき、ありがとうございます。私はマカ
オにあるマカオ理|工|大|学（り|こう|だい|がく）を4年|制（ねん|せい）の学|士（が
く|し）課|程（か|てい）で卒業（そつ|ぎょう）しました。</code></pre></div> </div>
          
<p><ruby>本<rt>ほん</rt>日<rt>じつ</rt></ruby>はお<ruby>時<rt>じ</rt>間<rt>かん</rt></ruby>をいただき、ありがとうございます。私はマカオにあるマカオ<ruby>理<rt>り</rt>工<rt>こう</rt>大<rt>だい</rt>学<rt>がく</rt></ruby>を4<ruby>年<rt>ねん</rt>制<rt>せい</rt></ruby>の<ruby>学<rt>がく</rt>士<rt>し</rt></ruby><ruby>課<rt>か</rt>程<rt>てい</rt></ruby>で<ruby>卒業<rt>そつぎょう</rt></ruby>しました。</p>
<h1 id="configurations">Configurations</h1>
<h2 id="configurations-basic-converter">Basic Converter</h2>
<p>Here’s how I did the configurations.</p>
<ol> <li>
<p>Add <code>org-ruby</code> to your <code>Gemfile</code>: </p>
<div class="language-ruby highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="n">group</span> <span class="ss">:jekyll_plugins</span> <span class="k">do</span>
  <span class="o">...</span>
  <span class="n">gem</span> <span class="s2">"org-ruby"</span>
<span class="k">end</span></code></pre></div> </div>  </li> <li>
<p>Write a new plugin: <code>_plugins/org_converter.rb</code> </p>
<div class="language-ruby highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'nokogiri'</span>
<span class="nb">require</span> <span class="s1">'rouge'</span>

<span class="no">Jekyll</span><span class="o">::</span><span class="no">Hooks</span><span class="p">.</span><span class="nf">register</span> <span class="p">[</span><span class="ss">:pages</span><span class="p">,</span> <span class="ss">:documents</span><span class="p">],</span> <span class="ss">:pre_render</span> <span class="k">do</span> <span class="o">|</span><span class="n">doc</span><span class="o">|</span>
  <span class="c1"># Enable Liquid syntax</span>
  <span class="k">if</span> <span class="n">doc</span><span class="p">.</span><span class="nf">extname</span><span class="p">.</span><span class="nf">downcase</span> <span class="o">==</span> <span class="s1">'.org'</span>
    <span class="c1"># Enable {% raw %}...{% endraw %}</span>
    <span class="n">doc</span><span class="p">.</span><span class="nf">content</span> <span class="o">=</span> <span class="n">doc</span><span class="p">.</span><span class="nf">content</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/^[ \t]*(\{%[ \t]*raw[ \t]*%\})[ \t]*\n?/</span><span class="p">,</span> <span class="s1">'\1'</span><span class="p">)</span>
    <span class="n">doc</span><span class="p">.</span><span class="nf">content</span> <span class="o">=</span> <span class="n">doc</span><span class="p">.</span><span class="nf">content</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/^[ \t]*(\{%[ \t]*endraw[ \t]*%\})[ \t]*\n?/</span><span class="p">,</span> <span class="s1">'\1'</span><span class="p">)</span>

    <span class="n">block_regex</span> <span class="o">=</span> <span class="sr">/(?mi:^[ \t]*#\+(?:begin_src|BEGIN_HTML).*?^[ \t]*#\+(?:end_src|END_HTML))/</span>
    <span class="n">raw_regex</span> <span class="o">=</span> <span class="sr">/(?mi:\{%[ \t]*raw[ \t]*%\}.*?\{%[ \t]*endraw[ \t]*%\})/</span>
    <span class="n">include_regex</span> <span class="o">=</span> <span class="sr">/^[ \t]*(\{%[ \t]*include[ \t]+(?:figure|gallery)(?:(?!%\}).|\{%.*?%\})*%\})[ \t]*$/m</span>

    <span class="c1"># Enable figures, galleries, and links to posts (skip inside code or raw blocks)</span>
    <span class="n">doc</span><span class="p">.</span><span class="nf">content</span> <span class="o">=</span> <span class="n">doc</span><span class="p">.</span><span class="nf">content</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/</span><span class="si">#{</span><span class="n">block_regex</span><span class="si">}</span><span class="sr">|</span><span class="si">#{</span><span class="n">raw_regex</span><span class="si">}</span><span class="sr">|</span><span class="si">#{</span><span class="n">include_regex</span><span class="si">}</span><span class="sr">/</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">match</span><span class="o">|</span>
      <span class="vg">$1</span> <span class="p">?</span> <span class="s2">"</span><span class="se">\n</span><span class="s2">#+BEGIN_HTML</span><span class="se">\n</span><span class="si">#{</span><span class="vg">$1</span><span class="si">}</span><span class="se">\n</span><span class="s2">#+END_HTML</span><span class="se">\n</span><span class="s2">"</span> <span class="p">:</span> <span class="n">match</span>
    <span class="k">end</span>

    <span class="n">inline_code</span> <span class="o">=</span> <span class="sr">/[=~][^=~\n]+[=~]/</span>
    <span class="n">markdown_link</span> <span class="o">=</span> <span class="sr">/(?&lt;!\!)\[([^\]]+)\]\(([^)]+)\)/</span>

    <span class="n">doc</span><span class="p">.</span><span class="nf">content</span> <span class="o">=</span> <span class="n">doc</span><span class="p">.</span><span class="nf">content</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/</span><span class="si">#{</span><span class="n">block_regex</span><span class="si">}</span><span class="sr">|^[ \t]*:[^\n]*$|</span><span class="si">#{</span><span class="n">inline_code</span><span class="si">}</span><span class="sr">|</span><span class="si">#{</span><span class="n">markdown_link</span><span class="si">}</span><span class="sr">/</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">match</span><span class="o">|</span>
      <span class="n">match</span><span class="p">.</span><span class="nf">start_with?</span><span class="p">(</span><span class="s1">'['</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">match</span><span class="p">.</span><span class="nf">start_with?</span><span class="p">(</span><span class="s1">'[['</span><span class="p">)</span> <span class="p">?</span> <span class="s2">"[[</span><span class="si">#{</span><span class="vg">$2</span><span class="si">}</span><span class="s2">][</span><span class="si">#{</span><span class="vg">$1</span><span class="si">}</span><span class="s2">]]"</span> <span class="p">:</span> <span class="n">match</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">module</span> <span class="nn">Jekyll</span>
  <span class="k">class</span> <span class="nc">OrgConverter</span> <span class="o">&lt;</span> <span class="no">Converter</span>
    <span class="n">safe</span> <span class="kp">true</span>
    <span class="n">priority</span> <span class="ss">:low</span>

    <span class="k">def</span> <span class="nf">matches</span><span class="p">(</span><span class="n">ext</span><span class="p">)</span>
      <span class="n">ext</span> <span class="o">=~</span> <span class="sr">/^\.org$/i</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">output_ext</span><span class="p">(</span><span class="n">ext</span><span class="p">)</span>
      <span class="s2">".html"</span>
    <span class="k">end</span>

    <span class="k">def</span> <span class="nf">convert</span><span class="p">(</span><span class="n">content</span><span class="p">)</span>
      <span class="nb">require</span> <span class="s1">'org-ruby'</span>
      <span class="n">html</span> <span class="o">=</span> <span class="no">Orgmode</span><span class="o">::</span><span class="no">Parser</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">content</span><span class="p">).</span><span class="nf">to_html</span>
      <span class="n">doc</span> <span class="o">=</span> <span class="no">Nokogiri</span><span class="o">::</span><span class="no">HTML</span><span class="p">.</span><span class="nf">fragment</span><span class="p">(</span><span class="n">html</span><span class="p">)</span>

      <span class="n">doc</span><span class="p">.</span><span class="nf">css</span><span class="p">(</span><span class="s1">'h1, h2, h3, h4, h5, h6'</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">node</span><span class="o">|</span>
        <span class="n">node</span><span class="p">[</span><span class="s1">'id'</span><span class="p">]</span> <span class="o">=</span> <span class="n">node</span><span class="p">.</span><span class="nf">text</span><span class="p">.</span><span class="nf">downcase</span><span class="p">.</span><span class="nf">strip</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/[^a-z0-9\s-]/</span><span class="p">,</span> <span class="s1">''</span><span class="p">).</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/\s+/</span><span class="p">,</span> <span class="s1">'-'</span><span class="p">)</span>
      <span class="k">end</span>

      <span class="c1"># Add &lt;thead&gt; to tables</span>
      <span class="n">doc</span><span class="p">.</span><span class="nf">css</span><span class="p">(</span><span class="s1">'table'</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">table</span><span class="o">|</span>
        <span class="n">trs</span> <span class="o">=</span> <span class="n">table</span><span class="p">.</span><span class="nf">xpath</span><span class="p">(</span><span class="s1">'./tr'</span><span class="p">)</span>
        <span class="k">next</span> <span class="k">if</span> <span class="n">trs</span><span class="p">.</span><span class="nf">empty?</span>

        <span class="k">if</span> <span class="n">trs</span><span class="p">.</span><span class="nf">first</span><span class="p">.</span><span class="nf">at_xpath</span><span class="p">(</span><span class="s1">'./th'</span><span class="p">)</span>
          <span class="n">thead</span> <span class="o">=</span> <span class="no">Nokogiri</span><span class="o">::</span><span class="no">XML</span><span class="o">::</span><span class="no">Node</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s1">'thead'</span><span class="p">,</span> <span class="n">doc</span><span class="p">)</span>
          <span class="n">thead</span><span class="p">.</span><span class="nf">add_child</span><span class="p">(</span><span class="n">trs</span><span class="p">.</span><span class="nf">first</span><span class="p">)</span>
          <span class="n">tbody</span> <span class="o">=</span> <span class="no">Nokogiri</span><span class="o">::</span><span class="no">XML</span><span class="o">::</span><span class="no">Node</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s1">'tbody'</span><span class="p">,</span> <span class="n">doc</span><span class="p">)</span>
          <span class="n">trs</span><span class="p">[</span><span class="mi">1</span><span class="o">..-</span><span class="mi">1</span><span class="p">].</span><span class="nf">each</span> <span class="p">{</span> <span class="o">|</span><span class="n">tr</span><span class="o">|</span> <span class="n">tbody</span><span class="p">.</span><span class="nf">add_child</span><span class="p">(</span><span class="n">tr</span><span class="p">)</span> <span class="p">}</span>
          <span class="n">table</span><span class="p">.</span><span class="nf">add_child</span><span class="p">(</span><span class="n">thead</span><span class="p">)</span>
          <span class="n">table</span><span class="p">.</span><span class="nf">add_child</span><span class="p">(</span><span class="n">tbody</span><span class="p">)</span>
        <span class="k">else</span>
          <span class="n">tbody</span> <span class="o">=</span> <span class="no">Nokogiri</span><span class="o">::</span><span class="no">XML</span><span class="o">::</span><span class="no">Node</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s1">'tbody'</span><span class="p">,</span> <span class="n">doc</span><span class="p">)</span>
          <span class="n">trs</span><span class="p">.</span><span class="nf">each</span> <span class="p">{</span> <span class="o">|</span><span class="n">tr</span><span class="o">|</span> <span class="n">tbody</span><span class="p">.</span><span class="nf">add_child</span><span class="p">(</span><span class="n">tr</span><span class="p">)</span> <span class="p">}</span>
          <span class="n">table</span><span class="p">.</span><span class="nf">add_child</span><span class="p">(</span><span class="n">tbody</span><span class="p">)</span>
        <span class="k">end</span>
      <span class="k">end</span>

      <span class="c1"># Process code blocks to enable code highlighting</span>
      <span class="n">doc</span><span class="p">.</span><span class="nf">css</span><span class="p">(</span><span class="s1">'pre.src[lang]'</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">pre</span><span class="o">|</span>
        <span class="n">lang</span> <span class="o">=</span> <span class="n">pre</span><span class="p">[</span><span class="s1">'lang'</span><span class="p">]</span>
        <span class="n">lexer</span> <span class="o">=</span> <span class="no">Rouge</span><span class="o">::</span><span class="no">Lexer</span><span class="p">.</span><span class="nf">find_fancy</span><span class="p">(</span><span class="n">lang</span><span class="p">)</span> <span class="o">||</span> <span class="no">Rouge</span><span class="o">::</span><span class="no">Lexers</span><span class="o">::</span><span class="no">PlainText</span>
        <span class="n">formatter</span> <span class="o">=</span> <span class="no">Rouge</span><span class="o">::</span><span class="no">Formatters</span><span class="o">::</span><span class="no">HTML</span><span class="p">.</span><span class="nf">new</span>

        <span class="c1"># .sub(/\A[\r\n]+/, '') targets the absolute beginning of the string</span>
        <span class="c1"># (\A) and removes any leading line breaks or carriage returns.</span>
        <span class="c1"># .sub(/\s+\z/, '') targets the absolute end of the string (\z) and</span>
        <span class="c1"># removes any trailing whitespace, including empty lines and spaces.</span>
        <span class="n">code_text</span> <span class="o">=</span> <span class="n">pre</span><span class="p">.</span><span class="nf">text</span><span class="p">.</span><span class="nf">sub</span><span class="p">(</span><span class="sr">/\A[\r\n]+/</span><span class="p">,</span> <span class="s1">''</span><span class="p">).</span><span class="nf">sub</span><span class="p">(</span><span class="sr">/\s+\z/</span><span class="p">,</span> <span class="s1">''</span><span class="p">)</span>

        <span class="n">highlighted</span> <span class="o">=</span> <span class="n">formatter</span><span class="p">.</span><span class="nf">format</span><span class="p">(</span><span class="n">lexer</span><span class="p">.</span><span class="nf">lex</span><span class="p">(</span><span class="n">code_text</span><span class="p">))</span>
        <span class="n">new_node</span> <span class="o">=</span> <span class="no">Nokogiri</span><span class="o">::</span><span class="no">HTML</span><span class="p">.</span><span class="nf">fragment</span><span class="p">(</span><span class="sx">%Q{
            &lt;div class="language-</span><span class="si">#{</span><span class="n">lang</span><span class="si">}</span><span class="sx"> highlighter-rouge"&gt;
              &lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;</span><span class="si">#{</span><span class="n">highlighted</span><span class="si">}</span><span class="sx">&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
            &lt;/div&gt;
          }</span><span class="p">)</span>
        <span class="n">pre</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="n">new_node</span><span class="p">)</span>
      <span class="k">end</span>

      <span class="n">doc</span><span class="p">.</span><span class="nf">to_html</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span></code></pre></div> </div>  </li> <li>
<p>Install the gems with bundler and run: </p>
<div class="language-sh highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>bundle <span class="nb">install
</span>bundle <span class="nb">exec </span>jekyll serve</code></pre></div> </div>  </li> </ol>
<h2 id="configurations-headings">Headings</h2>
<h3 id="configurations-headings-custom-id">Custom ID</h3>
<p>In markdown you can write <code>{: id="20260116203517"}</code> to assign a custom ID to a heading. In Org there is the <code>CUSTOM_ID</code> property in a drawer. By default, <code>org-ruby</code> completely drops <code>:PROPERTIES:</code> drawers when converting to HTML, which is why your <code>CUSTOM_ID</code> values are disappearing before Nokogiri even has a chance to see them. Additionally, your current <code>org_converter.rb</code> script forcefully overwrites every heading’s ID with a hyphenated slug.</p>
<p>The fastest, most efficient way to make this work is to pre-process the Org document line-by-line right before passing it to org-ruby. We can detect the <code>:CUSTOM_ID:</code> inside the drawer, temporarily “smuggle” it directly into the heading’s text as a special marker <code>(%%CUSTOM_ID:...%%)</code>, and then have your Nokogiri loop extract it and safely assign it as the real HTML ID.</p>
<p>Here are the lines to change in <code>_plugins/org_converter.rb</code>:</p>

            <div class="language-diff highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="p">modified   _plugins/org_converter.rb
@@ -36,12 +36,42 @@</span> module Jekyll
     end

     def convert(content)
<span class="gi">+      # Parse CUSTOM_ID properties and inject a temporary marker
+      lines = content.lines
+      lines.each_with_index do |line, index|
+        if line =~ /^\*+[ \t]+/
+          j = index + 1
+          if j &lt; lines.length &amp;&amp; lines[j] =~ /^[ \t]*:PROPERTIES:[ \t]*$/i
+            k = j + 1
+            custom_id = nil
+            while k &lt; lines.length &amp;&amp; lines[k] !~ /^[ \t]*:END:[ \t]*$/i &amp;&amp; lines[k] !~ /^\*+[ \t]+/
+              if lines[k] =~ /^[ \t]*:CUSTOM_ID:[ \t]+(\S+)/i
+                custom_id = $1
+              end
+              k += 1
+            end
+            if custom_id &amp;&amp; lines[k] =~ /^[ \t]*:END:[ \t]*$/i
+              lines[index] = lines[index].chomp + " %%CUSTOM_ID:#{custom_id}%%\n"
+            end
+          end
+        end
+      end
+      content = lines.join
+
</span>       require 'org-ruby'
       html = Orgmode::Parser.new(content).to_html
       doc = Nokogiri::HTML.fragment(html)

       doc.css('h1, h2, h3, h4, h5, h6').each do |node|
<span class="gd">-        node['id'] = node.text.downcase.strip.gsub(/[^a-z0-9\s-]/, '').gsub(/\s+/, '-')
</span><span class="gi">+        if node.content =~ /%%CUSTOM_ID:(\S+)%%/
+          node['id'] = $1
+          # Clean the marker out of all text nodes inside this heading
+          node.xpath('.//text()').each do |t|
+            t.content = t.content.gsub(/\s*%%CUSTOM_ID:\S+%%/, '')
+          end
+        else
+          node['id'] = node.text.downcase.strip.gsub(/[^a-z0-9\s-]/, '').gsub(/\s+/, '-')
+        end
</span>       end

       # Add &lt;thead&gt; to tables</code></pre></div> </div>
          
<h3 id="configurations-headings-topdown-ids-and-id-deduplication">Topdown IDs and ID Deduplication</h3>
<p>To achieve this, we need to extract the <code>heading_ids: topdown</code> preference from the front matter and pass it into the <code>org_converter.rb</code> pipeline. Because the convert(content) method strictly receives the raw string and does not have native access to the front matter variables, the most efficient approach is to inject a temporary marker (<code>%%HEADING_IDS:topdown%%</code>) into the document during the <code>:pre_render</code> hook, and then seamlessly extract it inside the converter before passing the content to <code>org-ruby</code>.</p>
<p>We can simultaneously implement a stack-based hierarchy tracking array and an ID tracking hash to guarantee uniqueness and concatenation.</p>
<p>Here are the exact diffs to update the <code>_plugins/org_converter.rb</code> file.</p>
<ol> <li>
<p>Inject the configuration marker </p>
<p>Add the injection logic to your <code>:pre_render</code> hook:</p>  <div class="language-diff highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="gd">--- _plugins/org_converter.rb
</span><span class="gi">+++ _plugins/org_converter.rb
</span><span class="p">@@ -4,6 +4,10 @@</span>
 Jekyll::Hooks.register [:pages, :documents], :pre_render do |doc|
   # Enable Liquid syntax
   if doc.extname.downcase == '.org'
<span class="gi">+    if doc.data['heading_ids'] == 'topdown'
+      doc.content = "%%HEADING_IDS:topdown%%\n" + doc.content
+    end
+
</span>     # Enable {{ "{%" }} raw %}...{{ "{%" }} endraw %}
     doc.content = doc.content.gsub(/^[ \t]*(\{%[ \t]*raw[ \t]*%\})[ \t]*\n?/, '\1')
     doc.content = doc.content.gsub(/^[ \t]*(\{%[ \t]*endraw[ \t]*%\})[ \t]*\n?/, '\1')</code></pre></div> </div>  </li> <li>
<p>Implement Topdown and Duplicate Logic </p>
<p>Update the convert function to intercept the marker and apply the new logic dynamically:</p>  <div class="language-diff highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="p">modified   _plugins/org_converter.rb
@@ -40,6 +44,11 @@</span> module Jekyll
     end

     def convert(content)
<span class="gi">+      topdown = false
+      if content.sub!(/\A%%HEADING_IDS:topdown%%\r?\n/, '')
+        topdown = true
+      end
+
</span>       # Parse CUSTOM_ID properties and inject a temporary marker
       lines = content.lines
       lines.each_with_index do |line, index|
<span class="p">@@ -66,16 +75,44 @@</span> module Jekyll
       html = Orgmode::Parser.new(content).to_html
       doc = Nokogiri::HTML.fragment(html)

+      id_stack = Array.new(6)
<span class="gi">+      seen_ids = {}
+
</span>       doc.css('h1, h2, h3, h4, h5, h6').each do |node|
<span class="gi">+        level = node.name[1].to_i
+        custom_id = nil
+
</span>         if node.content =~ /%%CUSTOM_ID:(\S+)%%/
<span class="gd">-          node['id'] = $1
</span><span class="gi">+          custom_id = $1
</span>           # Clean the marker out of all text nodes inside this heading
           node.xpath('.//text()').each do |t|
             t.content = t.content.gsub(/\s*%%CUSTOM_ID:\S+%%/, '')
           end
<span class="gi">+        end
+
+        slug = node.text.downcase.strip.gsub(/[^a-z0-9\s-]/, '').gsub(/\s+/, '-')
+        slug = 'section' if slug.empty?
+
+        if custom_id
+          base_id = custom_id
+        elsif topdown
+          parent_id = id_stack[0...level-1].compact.last
+          base_id = parent_id ? "#{parent_id}-#{slug}" : slug
</span>         else
<span class="gd">-          node['id'] = node.text.downcase.strip.gsub(/[^a-z0-9\s-]/, '').gsub(/\s+/, '-')
</span><span class="gi">+          base_id = slug
</span>         end
<span class="gi">+
+        final_id = base_id
+        count = 1
+        while seen_ids.key?(final_id)
+          final_id = "#{base_id}-#{count}"
+          count += 1
+        end
+        seen_ids[final_id] = true
+
+        node['id'] = final_id
+        id_stack[level - 1] = final_id
+        (level..5).each { |i| id_stack[i] = nil }
</span>       end

       # Add &lt;thead&gt; to tables</code></pre></div> </div>  </li> </ol>
<h3 id="configurations-headings-heading-numbering">Heading Numbering</h3>
<p>To make this work seamlessly, we must append a <code>:post_render</code> hook to the bottom of <code>org_converter.rb</code>. This is structurally necessary for two reasons:</p>
<ol> <li>Front Matter Access: Jekyll strips front matter (<code>ordered: true</code>) before passing the text to the convert function, so <code>org_converter</code> cannot natively see it.</li> <li>TOC Availability: Minimal Mistakes injects the Table of Contents via Liquid templates during the layout phase. If we try to inject numbers inside the convert step, the TOC doesn’t exist in the HTML yet, meaning it would remain unnumbered.</li> </ol>
<p>By running this logic in <code>:post_render</code>, we have full access to both the front matter and the fully generated layout (including the TOC).</p>
<p>Append these lines to the very end of the <code>_plugins/org_converter.rb</code> file:</p>

            <div class="language-diff highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="p">modified   _plugins/org_converter.rb
@@ -151,3 +151,53 @@</span> module Jekyll
     end
   end
 end
<span class="gi">+
+Jekyll::Hooks.register [:pages, :documents], :post_render do |doc|
+  # Only process Org Mode files
+  next unless doc.extname.downcase == '.org'
+
+  is_ordered_post = doc.data['ordered'] == true || doc.data['ordered'] == 'true'
+  fragment = Nokogiri::HTML(doc.output)
+  modified = false
+
+  counters = Array.new(7, 0)
+  anchor_stack = is_ordered_post ? [0] : []
+
+  main_content = fragment.at_css('section.page__content') || fragment.at_css('#main') || fragment
+
+  main_content.css('h1, h2, h3, h4, h5, h6').each do |heading|
+    # Skip structural layout headings
+    next if heading.ancestors('.toc, .page__comments, .page__related, .sidebar').any?
+
+    level = heading.name[1].to_i
+    # Exit any ordered scopes that are at the same or higher level than the current heading
+    anchor_stack.reject! { |a| a &gt;= level }
+
+    counters[level] += 1
+    ((level + 1)..6).each { |i| counters[i] = 0 }
+
+    if anchor_stack.any?
+      active_anchor = anchor_stack.last
+      visible_counters = counters[(active_anchor + 1)..level]
+
+      if visible_counters.any?
+        number_prefix = visible_counters.join('.') + '. '
+        heading.inner_html = "&lt;span class=\"heading-number\"&gt;#{number_prefix}&lt;/span&gt;" + heading.inner_html
+
+        if heading['id']
+          toc_link = fragment.at_css(".toc__menu a[href='##{heading['id']}']")
+          if toc_link
+            toc_link.inner_html = "&lt;span class=\"toc-number\"&gt;#{number_prefix}&lt;/span&gt;" + toc_link.inner_html
+          end
+        end
+        modified = true
+      end
+    end
+
+    if heading['class'] &amp;&amp; heading['class'].split.include?('ordered')
+      anchor_stack &lt;&lt; level
+    end
+  end
+
+  doc.output = fragment.to_html if modified
+end</span></code></pre></div> </div>
          
<h2 id="configurations-lists">Lists</h2>
<h3 id="configurations-lists-normalizing-loose-lists">Normalizing Loose Lists</h3>
<p>The outputs of the same list structure produced from Org and Markdown are slightly different as indicated in the below examples by the “Diff” comments. It seems that in Markdown when an item has sub-contents like a literal block, a sub-list, or a paragraph, its first content would be enclosed in <code>&lt;p&gt;</code>, while in Org this doesn’t happen. How to make Org list also follow this pattern in the output?</p>
<ul> <li>
<p>Org list: </p>
<div class="language-org highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>1. First item

   #+begin_src text
   Hello world!
   #+end_src

2. Second item
   1. Sub-item A (Indented 3 spaces)

      Some text

      1. Sub-item A (Indented 3 spaces)
      2. Sub-item A (Indented 3 spaces)
         1. Sub-item A (Indented 3 spaces)
         2. Sub-item A (Indented 3 spaces)
      3. Sub-item A (Indented 3 spaces)
   2. Sub-item B
      * Deeply nested bullet (Indented another 3 spaces)
3. Third item</code></pre></div> </div>  </li> <li>
<p>Org list output: </p>
<div class="language-html highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="nt">&lt;ol&gt;</span>
  <span class="nt">&lt;li&gt;</span>First item <span class="c">&lt;!-- Diff 1: No &lt;p&gt; around the content --&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"language-text highlighter-rouge"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"highlight"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;pre</span> <span class="na">class=</span><span class="s">"highlight"</span><span class="nt">&gt;</span>
          <span class="nt">&lt;code&gt;</span>Hello world!<span class="nt">&lt;/code&gt;</span>
        <span class="nt">&lt;/pre&gt;</span>
      <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
  <span class="nt">&lt;/li&gt;</span>
  <span class="nt">&lt;li&gt;</span>Second item
    <span class="nt">&lt;ol&gt;</span>
      <span class="nt">&lt;li&gt;</span>Sub-item A (Indented 3 spaces) <span class="c">&lt;!-- Diff 2: No &lt;p&gt; around the content --&gt;</span>
        <span class="nt">&lt;p&gt;</span>Some text<span class="nt">&lt;/p&gt;</span>
        <span class="nt">&lt;ol&gt;</span>
          <span class="nt">&lt;li&gt;</span>Sub-item A (Indented 3 spaces)<span class="nt">&lt;/li&gt;</span>
          <span class="nt">&lt;li&gt;</span>Sub-item A (Indented 3 spaces)
            <span class="nt">&lt;ol&gt;</span>
              <span class="nt">&lt;li&gt;</span>Sub-item A (Indented 3 spaces)<span class="nt">&lt;/li&gt;</span>
              <span class="nt">&lt;li&gt;</span>Sub-item A (Indented 3 spaces)<span class="nt">&lt;/li&gt;</span>
            <span class="nt">&lt;/ol&gt;</span>
          <span class="nt">&lt;/li&gt;</span>
          <span class="nt">&lt;li&gt;</span>Sub-item A (Indented 3 spaces)<span class="nt">&lt;/li&gt;</span>
        <span class="nt">&lt;/ol&gt;</span>
      <span class="nt">&lt;/li&gt;</span>
      <span class="nt">&lt;li&gt;</span>Sub-item B <span class="c">&lt;!-- Diff 3: No &lt;p&gt; around the content --&gt;</span>
        <span class="nt">&lt;ul&gt;</span>
          <span class="nt">&lt;li&gt;</span>Deeply nested bullet (Indented another 3 spaces)<span class="nt">&lt;/li&gt;</span>
        <span class="nt">&lt;/ul&gt;</span>
      <span class="nt">&lt;/li&gt;</span>
    <span class="nt">&lt;/ol&gt;</span>
  <span class="nt">&lt;/li&gt;</span>
  <span class="nt">&lt;li&gt;</span>Third item<span class="nt">&lt;/li&gt;</span>
<span class="nt">&lt;/ol&gt;</span></code></pre></div> </div>  </li> <li>
<p>Markdown list: </p>
<div class="language-markdown highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="p">1.</span> First item

   <span class="p">```</span><span class="nl"> text
</span>   Hello world!
   <span class="p">```</span>
<span class="p">
2.</span> Second item
<span class="p">   1.</span> Sub-item A (Indented 3 spaces)<span class="sb">

      Some text

      1. Sub-item A (Indented 3 spaces)
      1. Sub-item A (Indented 3 spaces)
         1. Sub-item A (Indented 3 spaces)
         1. Sub-item A (Indented 3 spaces)
      1. Sub-item A (Indented 3 spaces)
</span><span class="p">   2.</span> Sub-item B
<span class="p">      *</span> Deeply nested bullet (Indented another 3 spaces)
<span class="p">3.</span> Third item</code></pre></div> </div>  </li> <li>
<p>Markdown list output: </p>
<div class="language-html highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="nt">&lt;ol&gt;</span>
  <span class="nt">&lt;li&gt;</span>
    <span class="nt">&lt;p&gt;</span>First item<span class="nt">&lt;/p&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"language-text highlighter-rouge"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"highlight"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;pre</span> <span class="na">class=</span><span class="s">"highlight"</span><span class="nt">&gt;</span>
          <span class="nt">&lt;code&gt;</span>Hello world!<span class="nt">&lt;/code&gt;</span>
        <span class="nt">&lt;/pre&gt;</span>
      <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
  <span class="nt">&lt;/li&gt;</span>
  <span class="nt">&lt;li&gt;</span>Second item
    <span class="nt">&lt;ol&gt;</span>
      <span class="nt">&lt;li&gt;</span>
        <span class="nt">&lt;p&gt;</span>Sub-item A (Indented 3 spaces)<span class="nt">&lt;/p&gt;</span>
        <span class="nt">&lt;p&gt;</span>Some text<span class="nt">&lt;/p&gt;</span>
        <span class="nt">&lt;ol&gt;</span>
          <span class="nt">&lt;li&gt;</span>Sub-item A (Indented 3 spaces)<span class="nt">&lt;/li&gt;</span>
          <span class="nt">&lt;li&gt;</span>Sub-item A (Indented 3 spaces)
            <span class="nt">&lt;ol&gt;</span>
              <span class="nt">&lt;li&gt;</span>Sub-item A (Indented 3 spaces)<span class="nt">&lt;/li&gt;</span>
              <span class="nt">&lt;li&gt;</span>Sub-item A (Indented 3 spaces)<span class="nt">&lt;/li&gt;</span>
            <span class="nt">&lt;/ol&gt;</span>
          <span class="nt">&lt;/li&gt;</span>
          <span class="nt">&lt;li&gt;</span>Sub-item A (Indented 3 spaces)<span class="nt">&lt;/li&gt;</span>
        <span class="nt">&lt;/ol&gt;</span>
      <span class="nt">&lt;/li&gt;</span>
      <span class="nt">&lt;li&gt;</span>
        <span class="nt">&lt;p&gt;</span>Sub-item B<span class="nt">&lt;/p&gt;</span>
        <span class="nt">&lt;ul&gt;</span>
          <span class="nt">&lt;li&gt;</span>Deeply nested bullet (Indented another 3 spaces)<span class="nt">&lt;/li&gt;</span>
        <span class="nt">&lt;/ul&gt;</span>
      <span class="nt">&lt;/li&gt;</span>
    <span class="nt">&lt;/ol&gt;</span>
  <span class="nt">&lt;/li&gt;</span>
  <span class="nt">&lt;li&gt;</span>Third item<span class="nt">&lt;/li&gt;</span>
<span class="nt">&lt;/ol&gt;</span></code></pre></div> </div>  </li> </ul>
<p>The differences originate from how Kramdown (Markdown) and org-ruby process “loose” lists.</p>
<p>In Markdown, when a list item contains block-level elements (like a nested list, a code block, or a paragraph) or is separated by blank lines, Kramdown considers it a “loose” list item and actively wraps its leading, raw text into a <code>&lt;p&gt;</code> tag. <code>org-ruby</code>, however, natively dumps the raw text directly into the <code>&lt;li&gt;</code> element as an inline node before appending the block-level children.</p>
<p>To force your Org files to mimic Kramdown’s exact wrapping behavior, we can add a logic step right at the end of your HTML processing pipeline. Nokogiri will scan every <code>&lt;li&gt;</code>, check if it contains any block-level child elements, and if it does, it will gather all the leading text up to that block and neatly wrap it in a <code>&lt;p&gt;</code> tag.</p>

            <div class="language-diff highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="gd">--- _plugins/org_converter.rb
</span><span class="gi">+++ _plugins/org_converter.rb
</span><span class="p">@@ -134,6 +134,28 @@</span>
           target['class'] = (existing_classes + class_names).uniq.join(' ')
         end
       end

+      # Process complex list items to wrap leading text in &lt;p&gt;
<span class="gi">+      block_tags = %w[p div ul ol blockquote pre table dl figure h1 h2 h3 h4 h5 h6 hr]
+      doc.css('li').each do |li|
+        first_block = li.children.find { |c| c.element? &amp;&amp; block_tags.include?(c.name.downcase) }
+
+        if first_block
+          leading_nodes = []
+          li.children.each do |child|
+            break if child == first_block
+            leading_nodes &lt;&lt; child
+          end
+
+          # Check if there is actual inline content/text to wrap
+          has_content = leading_nodes.any? do |n|
+            (n.text? &amp;&amp; !n.text.strip.empty?) || (n.element? &amp;&amp; n.name.downcase != 'br')
+          end
+
+          if has_content
+            p_node = Nokogiri::XML::Node.new('p', doc)
+            first_block.add_previous_sibling(p_node)
+            leading_nodes.each { |n| p_node.add_child(n) }
+          end
+        end
+      end
+
</span>       doc.to_html
     end
   end</code></pre></div> </div>
          
<h2 id="configurations-links">Links</h2>
<h3 id="configurations-links-link-abbreviations">Link Abbreviations</h3>
<ol> <li>
<p>Resolve the (potential) conflict between <code>link_abbrs</code> and <code>#+LINK</code>? </p>
<p>There will be absolutely no conflict. Because your custom <code>_plugins/link_abbr.rb</code> uses a <code>:pre_render</code> hook, it modifies the raw text document before <code>org-ruby</code> even touches the file. By the time <code>org-ruby</code> wakes up to compile the HTML, your plugin has already replaced <code>[[https://i.postimg.cc/Vv8jFw8D/image.jpg]]</code> with <code>[[/assets/.../image.jpg]]</code>. <code>org-ruby</code> will simply see a standard file link and successfully render it, completely ignoring the now-unused <code>#+LINK: foo</code> directive at the top of the file.</p> </li> <li>
<p>Add Org Syntax Support to Your Plugin </p>
<p>Adding support for <code>[[https://i.postimg.cc/Vv8jFw8D/filename]]</code> is highly achievable, but introduces a major edge-case in the Front Matter: <b>Unquoted brackets break YAML</b>. If you type <code>- [[https://i.postimg.cc/Vv8jFw8D/image.jpg]]</code> in YAML without quotes, the YAML parser natively interprets <code>[</code> as an array constructor. It will process your string into a nested Ruby array <code>[["foo:image.jpg"]]</code>, which causes standard string comparisons to crash.</p> <p>The updated code below intercepts Org-style <code>[[...]]</code> links in the body content, title fields, and implements a safety net to deeply extract the string from the front matter even if the YAML parser incorrectly converted the unquoted brackets into an array.</p> <p>Apply this diff to <code>_plugins/link_abbr.rb</code>:</p> </li> </ol>
<h2 id="configurations-notices">Notices</h2>
<p>In Markdown files you can write <a href="https://mmistakes.github.io/minimal-mistakes/markup/markup-html-tags-and-formatting/#notices">notices</a> such as:</p>

            <div class="language-markdown highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="gs">**Watch out!**</span> This paragraph of text has been <span class="p">[</span><span class="nv">emphasized</span><span class="p">](</span><span class="sx">#</span><span class="p">)</span> with the <span class="sb">`{: .notice--warning}`</span> class.
{: .notice--warning}</code></pre></div> </div>
          
<p>Because Jekyll processes Org files into HTML using <code>org-ruby</code> before Kramdown can parse them, Kramdown’s block attribute syntax like <code>{: .notice--warning}</code> will not work.</p>
<p>To achieve the exact same result in <code>org-ruby</code> while still being able to use standard Org formatting (like bold, lists, and links) inside the notice, you must inject the raw HTML wrapper directly using the <code>#+html:</code> directive.</p>

            <div class="language-org highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>#+html: &lt;div class="notice--warning"&gt;
*Watch out!*
This is a warning notice inside an Org file.
#+html: &lt;/div&gt;</code></pre></div> </div>
          
<p><code>org-ruby</code> passes the lines starting with <code>#+html:</code> straight to the output without modification. Because you leave empty lines between the HTML tags and your text, org-ruby will still evaluate the text in the middle as standard Org syntax, converting <b>Watch out!</b> into bold tags and wrapping the lines in paragraph tags.</p>
<p>However, this is tedious to write compared with the Markdown syntax. It would be much better if we can mimic Kramdown’s attribute syntax in Org files.</p>
<p>Because you are already utilizing Nokogiri in your Jekyll pipeline, we can create a lightweight parser that finds the exact <code>{: .classname }</code> text, strips it out, and natively injects the CSS classes into the correct HTML tags.</p>

            <div class="language-diff highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="p">modified   _plugins/org_converter.rb
@@ -84,6 +84,39 @@</span> module Jekyll
         pre.replace(new_node)
       end

+      # Process Kramdown-style attribute lists {: .class1 .class2 }
<span class="gi">+      doc.xpath('.//text()[contains(., "{:")]').each do |node|
+        # Skip if the syntax is inside a literal code block
+        next if node.ancestors('pre, code').any?
+
+        if node.content =~ /\{:\s*((?:\.[a-zA-Z0-9_\-–—]+\s*)+)\}/
+          raw_classes = $1
+          normalized_classes = raw_classes.gsub(/[–—]/, '--')
+          class_names = normalized_classes.scan(/\.([a-zA-Z0-9_-]+)/).flatten
+
+          # Strip the syntax from the text node
+          node.content = node.content.sub(/\{:\s*(?:\.[a-zA-Z0-9_\-–—]+\s*)+\}/, '')
+
+          parent = node.parent
+          target = parent
+
+          # Clean up trailing &lt;br&gt; if the syntax was on a new line
+          if node.content.strip.empty? &amp;&amp; node.previous_sibling &amp;&amp; node.previous_sibling.name == 'br'
+            node.previous_sibling.remove
+          end
+
+          # If removing the syntax leaves the block entirely empty, it targets the previous element
+          if parent.name == 'p' &amp;&amp; parent.text.strip.empty? &amp;&amp; parent.children.all? { |c| c.name == 'text' || c.name == 'br' }
+            target = parent.previous_element || parent
+            parent.remove if target != parent
+          end
+
+          # Apply the classes safely without overwriting existing ones
+          existing_classes = target['class'] ? target['class'].split(' ') : []
+          target['class'] = (existing_classes + class_names).uniq.join(' ')
+        end
+      end
+
</span>       doc.to_html
     end
   end</code></pre></div> </div>
          
<h2 id="configurations-newlines-in-body-text">Newlines in Body Text</h2>
<p><code>org-ruby</code> and standard Markdown engines preserve newlines during HTML generation for a specific reason: to maintain the readability of the generated HTML source code. By HTML design, browsers interpret any newline in the source code as a single space. While this works perfectly for languages like English that use spaces to separate words, it creates unwanted, unnatural gaps in CJK (Chinese, Japanese, Korean) texts where words are not separated by spaces.</p>
<p>Here are some examples:</p>
<p>English:</p>
<p>Watch out! This is a warning notice inside an Org file. Watch out! This is a warning notice inside an Org file. Watch out! This is a warning notice inside an Org file.</p>

            <div class="language-org highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>Source:

Watch out! This is a warning notice inside an Org
file. Watch out! This is a warning notice inside
an Org file. Watch out! This is a warning notice
inside an Org file.

Output:

&lt;p&gt;Watch out! This is a warning notice inside an Org
  file. Watch out! This is a warning notice inside
  an Org file. Watch out! This is a warning notice
  inside an Org file.&lt;/p&gt;</code></pre></div> </div>
          
<p>Japanese:</p>
<p>本日はお時間をいただき、ありがとうございます。私はマカオにあるマカオ理工大学を4年制の学士課程で卒業しました。</p>

            <div class="language-org highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>Source:

本日はお時間をいただき、ありがとうございます。私は
マカオにあるマカオ理工大学を4年制の学士課程で卒業
しました。

Output:

&lt;p&gt;本日はお時間をいただき、ありがとうございます。私は
  マカオにあるマカオ理工大学を4年制の学士課程で卒業
  しました。&lt;/p&gt;</code></pre></div> </div>
          
<p>The most efficient and safe approach to fix this is to clean up the text using Nokogiri right before outputting the final HTML. We can implement logic to completely remove newlines (and any surrounding whitespace) only when they are sandwiched between CJK characters, while safely converting all other newlines into a single space so English words don’t get squashed together.</p>
<p>Add the following code to <code>_plugins/org_converter.rb</code> right before the <code>doc.to_html</code> call:</p>

            <div class="language-diff highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="p">modified   _plugins/org_converter.rb
@@ -242,6 +242,21 @@</span> module Jekyll
         end
       end

+      # Newline cleanup (Remove extra spaces between CJK characters)
<span class="gi">+      cjk = "\\p{Han}\\p{Hiragana}\\p{Katakana}ー、。！？「」『』（）【】，．：；"
+      doc.xpath('.//text()[not(ancestor::pre or ancestor::code)]').each do |node|
+        content = node.content
+        next unless content.match?(/[\r\n]/)
+
+        # 1. Completely remove newlines and spaces between CJK characters (uses
+        # lookahead to safely handle overlapping lines)
+        content = content.gsub(/([#{cjk}])\s*[\r\n]+\s*(?=[#{cjk}])/o, '\1')
+        # 2. Safely convert remaining newlines to a single space
+        content = content.gsub(/\s*[\r\n]+\s*/, ' ')
+
+        node.content = content if content != node.content
+      end
+
</span>       doc.to_html
     end
   end</code></pre></div> </div>
          
<p>Why this implementation excels:</p>
<ul> <li>Bypassing XPath Blindspots: By filtering <code>[not(ancestor::pre or
    ancestor::code)]</code> via XPath and moving the newline check <code>next unless
    content.match?(/[\r\n]/)</code> into Ruby, we bypass libxml2’s strict string literal parsing. Ruby will now correctly evaluate and catch every single node that contains a newline.</li> <li>Positive Lookahead <code>(?=...)</code>: In a sentence spanning 3 lines, the lookahead <code>(?=[#{cjk}])</code> asserts that a CJK character exists after the newline without “consuming” it, allowing the engine to successfully match and delete consecutive newlines.</li> <li>Cross-Platform Newlines: Using <code>[\r\n]+</code> ensures it works flawlessly regardless of whether your files were saved with Windows (<code>\r\n</code>) or Unix (<code>\n</code>) line endings.</li> </ul>
<h2 id="configurations-furigana">Furigana</h2>
<p>Here’s an edge case for the furigana handling code. The 3rd and 5th “理工大学” aren’t parsed or matched correctly. One thing in common is that they are all broken by a newline with the <code>|</code> character leading the next line. This may cause some confusion between the Org table syntax and the furigana syntax.</p>

            <div class="language-org highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>本日（ほんじつ）
本|日（ほん|じつ）
理工大学（りこうだいがく）
理|工|
大|学（り|こう|だい|がく）
理|工|大
|学（り|こう|だい|がく）
理|工|大|学（り|
こう|だい|がく）
理|工|大|学（り
|こう|だい|がく）</code></pre></div> </div>
          
<p><ruby>本日<rt>ほんじつ</rt></ruby> <ruby>本<rt>ほん</rt>日<rt>じつ</rt></ruby> <ruby>理工大学<rt>りこうだいがく</rt></ruby> <ruby>理<rt>り</rt>工<rt>こう</rt>大<rt>だい</rt>学<rt>がく</rt></ruby> <ruby>理<rt>り</rt>工<rt>こう</rt>大<rt>だい</rt>学<rt>がく</rt></ruby> <ruby>理<rt>り</rt>工<rt>こう</rt>大<rt>だい</rt>学<rt>がく</rt></ruby> <ruby>理<rt>り</rt>工<rt>こう</rt>大<rt>だい</rt>学<rt>がく</rt></ruby></p>
<p>Changes:</p>

            <div class="language-sh highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>git diff 1cb2b19..204e959 <span class="nt">--</span> _plugins/org_converter.rb</code></pre></div> </div>
          

            <div class="language-diff highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="p">modified   _plugins/org_converter.rb
@@ -1,6 +1,10 @@</span>
 require 'nokogiri'
 require 'rouge'
 
<span class="gi">+module Jekyll
+  FURIGANA_REGEX = /((?:\p{Han}|々|\|)(?:(?:\p{Han}|々|\||\s)*(?:\p{Han}|々|\|))?)\s*[[[ぁ-んァ-ヶー|\s]+][（(]][）)]/
+end
+
</span> Jekyll::Hooks.register [:pages, :documents], :pre_render do |doc|
   # Enable Liquid syntax
   if doc.extname.downcase == '.org'
<span class="p">@@ -21,6 +25,15 @@</span> Jekyll::Hooks.register [:pages, :documents], :pre_render do |doc|
       $1 ? "\n#+BEGIN_HTML\n#{$1}\n#+END_HTML\n" : match
     end
 
<span class="gi">+    # Pre-process furigana to strip newlines and prevent org-ruby table corruption
+    doc.content = doc.content.gsub(/#{block_regex}|#{raw_regex}|#{Jekyll::FURIGANA_REGEX}/) do |match|
+      if match.match?(/\A[ \t]*(?:#\+|\{%)/)
+        match
+      else
+        match.gsub(/[\r\n]+/, '')
+      end
+    end
+
</span>     inline_code = /[=~][^=~\n]+[=~]/
     markdown_link = /(?&lt;!\!)\[([^\]]+)\]\(([^)]+)\)/
 
<span class="p">@@ -213,6 +226,49 @@</span> module Jekyll
         end
       end
 
<span class="gi">+      # Furigana handling
+      # 1. Use XPath to filter out pre/code ancestors at the C-level (libxml2) for maximum speed.
+      # 2. Fast pre-filter using 'contains' so Ruby only processes nodes that actually have parentheses.
+      target_nodes = doc.xpath('.//text()[not(ancestor::pre or ancestor::code) and (contains(., "（") or contains(., "("))]')
+      target_nodes.each do |node|
+        content = node.content
+        if content.match?(Jekyll::FURIGANA_REGEX)
+          new_html = content.gsub(Jekyll::FURIGANA_REGEX) do |match|
+            raw_base = $1
+            raw_ruby = $2
+            clean_base = raw_base.gsub(/\s+/, '')
+            clean_ruby = raw_ruby.gsub(/\s+/, '')
+            bases = clean_base.split('|')
+            rubies = clean_ruby.split('|')
+
+            if bases.length == rubies.length
+              ruby_content = bases.zip(rubies).map { |b, r| "#{b}&lt;rt&gt;#{r}&lt;/rt&gt;" }.join('')
+              "&lt;ruby&gt;#{ruby_content}&lt;/ruby&gt;"
+            else
+              base = clean_base.delete('|')
+              rb = clean_ruby.delete('|')
+              "&lt;ruby&gt;#{base}&lt;rt&gt;#{rb}&lt;/rt&gt;&lt;/ruby&gt;"
+            end
+          end
+          node.replace(Nokogiri::HTML::DocumentFragment.parse(new_html)) if new_html != content
+        end
+      end</span></code></pre></div> </div>]]></content><author><name>Joseph Huang</name></author><category term="Web" /><category term="Minimal Mistakes" /><summary type="html"><![CDATA[From now on I will be able to write posts in Org Mode for this blog, besides the existing Markdown method. Here’s how I configure it. This article is written in an Org file and also serves as a demonstration of this new feature.]]></summary></entry><entry><title type="html">🔒 Protected Content</title><link href="https://josephtesfaye.com/blog/web/private-posts/" rel="alternate" type="text/html" title="🔒 Protected Content" /><published>2026-02-23T00:00:00+00:00</published><updated>2026-03-01T00:00:00+00:00</updated><id>https://josephtesfaye.com/blog/web/private-posts</id><content type="html" xml:base="https://josephtesfaye.com/blog/web/private-posts/"><![CDATA[<p>I want to be able to make a post private, that is, it can only be seen by me, or someone with a password, and not by any other person, any search engine, any other means.</p>
<h1 id="the-ideas">The Ideas</h1>
<p>Because Jekyll generates a completely static site, there is no backend server to check a user’s session or validate a password before sending the HTML to the browser. If a page exists on the server, anyone who guesses the URL can technically download it.</p>
<p>To achieve true privacy on a static site, you have to shift the protection mechanism. Here are the two most effective architectural ideas for implementing this:</p>
<h2 id="idea-1-client-side-content-encryption-the-best-static-approach">Idea 1: Client-Side Content Encryption (The Best Static Approach)</h2>
<p>Instead of hiding the file, you scramble its contents. You would use a custom Jekyll plugin (or an existing gem like <code>jekyll-crypt</code>) that hooks into the build process.</p>
<ul> <li>How it works: In the front matter of your private post, you define a <code>password:
    "mysecret"~ and a ~published: true</code> flag. During <code>jekyll build</code>, the plugin intercepts the post’s rendered HTML and encrypts it using an algorithm like AES-256, using the password as the key.</li> <li>What gets published: The output HTML file contains zero actual content. It only contains a JavaScript password prompt, the decryption logic, and a giant block of ciphertext.</li> <li>The User Experience: When you visit the URL, the page loads a password box. Entering the correct password triggers the JavaScript to decrypt the ciphertext locally in the browser and inject the readable HTML into the DOM.</li> <li>Security: Search engines and scrapers only see the JavaScript and ciphertext, making it completely impossible for them to index the real content. Anyone poking around your GitHub repository (if public) or the site’s source code will only see gibberish.</li> </ul>
<p>If you want to keep everything self-contained within Jekyll and easily hostable anywhere (like basic GitHub Pages), this is the definitive way to go.</p>
<h2 id="idea-2-edge-level-access-control-the-infrastructure-approach">Idea 2: Edge-Level Access Control (The Infrastructure Approach)</h2>
<p>If you want to protect the files at the network level rather than encrypting the contents, you must rely on the hosting provider rather than Jekyll.</p>
<ul> <li>How it works: Instead of relying on a backend server, you use a modern CDN or hosting provider that supports “Edge Functions” or zero-trust access (like Cloudflare Pages, Netlify, or Vercel).</li> <li>The Architecture: You set up a routing rule at the CDN level stating that any URL matching <code>/private/*</code> requires authentication.</li> <li>The User Experience: When a user navigates to the private post, the CDN intercepts the request before it even reaches the static HTML files. It serves a login page. If the password is correct, the CDN sets a secure cookie and forwards the static file.</li> <li>Security: Highly secure. The static files are never transmitted over the network until authentication is verified at the edge node.</li> </ul>
<h2 id="what-about-the-the-source-files">What about the the source files?</h2>
<p>What about the the source markdown or org files? How do you also hide them from the public, since currently I have the site hosted on Github through Github Actions, which requires the repository to be public? How can you make the repository private to still be able to serve the site?</p>
<p>Yes, you can hide your source files by making the repository private, but how you achieve this depends on your GitHub account tier and where you want to host the site.</p>
<p>Here is the breakdown of your options:</p>
<ol> <li>
<p>The Native GitHub Way (Requires Paid Account) </p>
<p>If you have a GitHub Pro, Team, or Enterprise account, you can simply go to your repository settings and change the visibility to Private. GitHub Pages natively supports publishing from private repositories on these paid tiers. You can even set access controls so the site itself is only visible to specific people in your organization.</p> </li> <li>
<p>The Alternative Hosting Way (Free &amp; Easiest) </p>
<p>If you are on a GitHub Free account, GitHub Pages requires the repository to be public to serve the site.</p> <p>The easiest way to keep your source code private for free is to move your hosting to Cloudflare Pages, Netlify, or Vercel.</p> <ol> <li>Make your GitHub repository Private. This instantly breaks GitHub Pages.</li> <li>Link your private GitHub repository to a free Cloudflare Pages or Netlify account.</li> <li>They will automatically run your Jekyll build commands (just like GitHub Actions) and serve the compiled HTML globally. Your source files remain completely hidden in your private GitHub repo.</li> </ol> </li> <li>
<p>The Two-Repository Strategy (Free, GitHub-Only) </p>
<p>If you are on a free account but absolutely want to keep using GitHub Pages, you can separate your raw source files from your generated HTML using two repositories and GitHub Actions.</p> <ol> <li>Create Repo A (Private): This holds your Jekyll source files (Markdown, Org files, _config.yml, etc.).</li> <li>Create Repo B (Public): This will only hold the generated, static HTML site.</li> <li>Configure GitHub Actions in Repo A: You update your existing <code>.github/workflows/jekyll.yml</code> file to build the site, but instead of deploying to the local repository, it pushes the compiled <code>_site</code> folder directly to Repo B.</li> </ol> <p>To do this, you would use an action like <code>peaceiris/actions-gh-pages</code> in your workflow. It allows deploying to an external repository by using a Personal Access Token (PAT):</p>  <div class="language-yaml highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Deploy to external public repository</span>
  <span class="na">uses</span><span class="pi">:</span> <span class="s">peaceiris/actions-gh-pages@v3</span>
  <span class="na">with</span><span class="pi">:</span>
    <span class="na">personal_token</span><span class="pi">:</span> <span class="s">$</span>
    <span class="na">external_repository</span><span class="pi">:</span> <span class="s">your-username/your-public-repo-B</span>
    <span class="na">publish_branch</span><span class="pi">:</span> <span class="s">main</span>
    <span class="na">publish_dir</span><span class="pi">:</span> <span class="s">./_site</span></code></pre></div> </div>  <p>This way, anyone looking at Repo B only sees the final compiled web pages, while your Org and Markdown source files safely remain in your private Repo A.</p> </li> </ol>
<h2 id="20260225232548">The Jekyll Build Process</h2>
<p>By default, Jekyll completely empties the destination directory (<code>_site/</code>) every time you run a build or start the local server. It does this to ensure no stale, orphaned files from previous builds are left behind.</p>
<p>Because of this cleaning process, if you initialize a Git repository directly inside the <code>_site/</code> folder, the <code>.git/</code> directory is at the risk of being instantly deleted the next time Jekyll builds the site.</p>
<p>I say “risk” because there is a built-in safety feature of Jekyll. In general, to prevent Jekyll from deleting the specific files/folders inside <code>_site/</code>, you can use the <code>keep_files</code> array in your <code>_config.yml</code> file, like this:</p>

            <div class="language-yaml highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="na">keep_files</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s">.git</span></code></pre></div> </div>
          
<p>However, even if you do not explicitly define <code>keep_files</code>, Jekyll has underlying default settings designed specifically to prevent catastrophic data loss. By default, Jekyll’s internal configuration automatically sets <code>keep_files: [".git",
  ".svn"]</code>.</p>
<p>Here is exactly what happens to the <code>_site</code> directory when you trigger a build:</p>
<ol> <li>
<p>Configuration Merge </p>
<p>When you run <code>jekyll build</code> or <code>jekyll serve</code>, Jekyll first loads its system-wide default configuration. It then layers your <code>_config.yml</code> on top of it. Unless you explicitly override <code>keep_files</code> with an empty array or tell Jekyll to overwrite the defaults entirely, <code>.git</code> remains in the internal “safe list.”</p> </li> <li>
<p>The Cleanup Phase (<code>Jekyll::Cleaner</code>) </p>
<p>Before reading any of your new source files, Jekyll instantiates a Cleaner class. This class scans the entire destination folder (<code>_site/</code>). It iterates through every file and directory currently sitting in <code>_site/</code> and checks them against the merged <code>keep_files</code> array.</p> <ul> <li>If a file or folder is not in the array, it is permanently deleted.</li> <li>Because <code>.git</code> is in the default array, the cleaner skips it, leaving your version control history untouched.</li> </ul> </li> <li>
<p>The Read and Render Phase </p>
<p>Jekyll scans your root directory (ignoring files that start with <code>_</code>, <code>.</code>, or are explicitly excluded), processes Liquid tags, converts Markdown and Org files into HTML, and compiles your SCSS.</p> </li> <li>
<p>The Write Phase </p>
<p>Jekyll writes the newly generated HTML files and copies static assets (like images and JS files) directly into the clean <code>_site/</code> folder, effectively surrounding the preserved <code>.git</code> directory with the fresh build.</p> </li> </ol>
<p>The risk is that when you run <code>bundle exec jekyll clean</code> the entire <code>_site</code> directory is deleted, including the <code>.git/</code> directory in it, even if you explicitly specify it in <code>keep_files</code> in your <code>_config.yml</code>.</p>
<p>The reason this happens is that jekyll clean is a brute-force command. Its explicit purpose is to completely obliterate the generated site and all cache folders to ensure a perfectly clean slate. It intentionally bypasses the <code>keep_files</code> array, which is only evaluated by the internal cleaner during <code>jekyll
  build</code> or <code>jekyll serve</code>.</p>
<p>Here are the best ways to handle this:</p>
<ol> <li>
<p>Stop using jekyll clean (Recommended) </p>
<p>You rarely actually need to run <code>jekyll clean</code>. Because <code>jekyll build</code> and <code>jekyll
      serve</code> automatically invoke their own safe cleanup phase before generating new files (the phase that does respect <code>keep_files</code>), stale files are already removed automatically. You can safely rely entirely on the build/serve commands.</p> </li> <li>
<p>Clear only the caches manually </p>
<p>If you are running <code>jekyll clean</code> because you are experiencing caching issues (like SCSS not updating or plugin changes not registering), you can safely delete the cache folders without touching the <code>_site/</code> directory at all.</p> <p>Instead of bundle exec jekyll clean, run:</p> <pre class="example">
rm -rf .jekyll-cache .jekyll-metadata
    </pre> </li> <li>
<p>Create a custom clean command </p>
<p>If you want a command that mimics <code>jekyll clean</code> but preserves your <code>.git</code> folder inside <code>_site/</code>, you can add an alias to your shell profile (like <code>~/.bashrc</code> or <code>~/.zshrc</code>) or create a simple bash script:</p>  <div class="language-sh highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="nb">alias </span><span class="nv">jclean</span><span class="o">=</span><span class="s2">"rm -rf .jekyll-cache .jekyll-metadata &amp;&amp; find _site -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} +"</span></code></pre></div> </div>  <p>This instantly deletes the Jekyll caches and wipes everything inside <code>_site/</code> except the .git/ directory.</p> </li> </ol>
<h1 id="the-procedures" class="ordered">The Procedures</h1>

<h2 id="use-the-two-repository-strategy">Use the Two-Repository Strategy</h2>
<p>The most efficient and robust way to manage the Two-Repository Strategy is to completely decouple your local testing environment from your deployment process.</p>
<p>Attempting to turn the local <code>_site</code> folder into a Git repository is a constant battle against Jekyll’s internal cleaning mechanisms (see <a href="#20260225232548">Jekyll Build Process</a>). The best approach is to treat your local <code>_site</code> folder as a purely temporary, disposable directory, and let GitHub Actions act as the bridge between Repo A (Private Source) and Repo B (Public HTML).</p>
<h3 id="the-local-workflow-repo-a">The Local Workflow (Repo A)</h3>
<p>Make sure you stop tracking the built site entirely in your source repository.</p>
<ol> <li>Add <code>_site/</code> to the <code>.gitignore</code> file in Repo A.</li> <li>When writing posts or testing configurations, simply run <code>bundle exec jekyll
    serve</code> on your Mac.</li> <li>You never run <code>git commit</code> inside the <code>_site</code> folder. You only commit your source Markdown, Org files, and configurations to Repo A.</li> </ol>
<h3 id="create-and-serving-repo-b">Create and serving Repo B</h3>
<p>Now you can create a new repository (<a href="https://github.com/josephtesfaye/blog">Repo B</a>) on GitHub to host the static <code>_site</code> contents.</p>
<p>To serve your public site from Repo B using GitHub Pages, you need to configure it to act purely as a static file host. Since Repo A has already done the heavy lifting of building the Jekyll site, Repo B just needs to serve the finished HTML.</p>
<ol> <li>
<p>Ensure Repo B is Public </p>
<p>If you are on a free GitHub account, Repo B must be set to Public. Go to Settings &gt; General in Repo B, scroll down to the “Danger Zone,” and ensure the repository visibility is public.</p> </li> <li>
<p>Configure GitHub Pages </p>
<ol> <li>In Repo B, go to the Settings &gt; Pages.</li> <li>Under the Build and deployment section, set the Source dropdown to Deploy from a branch.</li> <li>Under the Branch section, select the branch that your GitHub Action from Repo A is pushing to (usually main, master, or gh-pages).</li> <li>Select the <code>/ (root)</code> folder.</li> <li>Click Save.</li> </ol> </li> <li>
<p>Bypass GitHub’s Default Jekyll Build (Crucial) </p>
<p>By default, GitHub Pages assumes any repository is a raw Jekyll site and will try to build it again. Since your site is already compiled by Repo A, running it through Jekyll a second time can break your CSS, strip out folders that start with an underscore (like <code>_page</code> or <code>_site</code>), or cause build failures.</p> <p>To tell GitHub Pages to serve the files exactly as they are without processing them, you need a file named <code>.nojekyll</code> (with no extension) in the root of Repo B.</p> <p>If you are using the <code>peaceiris/actions-gh-pages</code> action in Repo A (which we are using in the following step), the action automatically generates and includes the file for you by default, so you don’t need to do anything manually.</p> <p>If you are using a different deployment method, make sure your workflow in Repo A creates an empty <code>.nojekyll</code> file in the <code>_site</code> directory before pushing it to Repo B. You can add a quick step in your workflow right before the deployment step:</p>  <div class="language-yaml highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Add .nojekyll file</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">touch ./_site/.nojekyll</span></code></pre></div> </div>  </li> <li>
<p>Wait for the Deployment </p>
<p>Once you save the Pages settings, GitHub will trigger a very fast deployment (since it’s just copying static files). Within a minute or two, your site will be live at https://your-username.github.io/your-public-repo-B/ (or your custom domain, if you add one in the Pages settings).</p> </li> </ol>
<h3 id="the-deployment-bridge-github-actions">The Deployment Bridge (GitHub Actions)</h3>
<p>When you push your source code to the private Repo A, a GitHub Action spins up, builds the site in a clean cloud environment, and pushes the resulting HTML directly to Repo B.</p>
<p>To set this up:</p>
<ol> <li>
<p>Generate a Personal Access Token (PAT) in your GitHub Developer Settings with the repo scope checked. </p>
<p>I highly recommend creating a Fine-grained personal access token.</p> <p>While the “Tokens (classic)” will absolutely work, they grant sweeping access to every repository in your account. If a classic token with the repo scope is ever compromised, the attacker has full control over all your code.</p> <p>A fine-grained token allows you to restrict the token’s access strictly to Repo B (your public HTML repository) and limit it to only the specific permissions needed to push files.</p> <p>Here is how to generate the fine-grained token for this exact deployment setup:</p> <ol> <li>Go to your GitHub Settings &gt; Developer settings (at the very bottom) &gt; Personal access tokens &gt; Fine-grained tokens.</li> <li>Click the <code>Generate new token</code> button.</li> <li>Give your token a name (e.g., “Jekyll Deploy to Repo B”) and set an expiration date.</li> <li>Under Repository access, select <code>Only select repositories</code>. In the dropdown that appears, find and select Repo B (your destination repository).</li> <li>Scroll down to Permissions and click on <code>Add permissions</code> to expand it. Find <code>Contents</code> and change the access level to Read and write. (This is all the action needs to push your built <code>_site</code> files).</li> <li>Click <code>Generate token</code> at the bottom of the page.</li> <li>Copy the generated token immediately, as GitHub will only show it to you this one time.</li> </ol> </li> <li>Go to the Settings of Repo A, navigate to Secrets and variables &gt; Actions, and add a new repository secret. Name it <code>DEPLOY_PAT</code> and paste your token.</li> <li>
<p>In Repo A, update your <code>.github/workflows/jekyll.yml</code> file to look like this: </p>
<div class="language-yaml highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Deploy Jekyll site to Repo B</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">master</span>  <span class="c1"># Or main, depending on your default branch in Repo A</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build_and_deploy</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checkout Source Code</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Setup Ruby</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">ruby/setup-ruby@v1</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">ruby-version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3.1'</span>
          <span class="na">bundler-cache</span><span class="pi">:</span> <span class="no">true</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build Site</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">bundle exec jekyll build</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Deploy to External Repository</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">peaceiris/actions-gh-pages@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">personal_token</span><span class="pi">:</span> <span class="s">$</span>
          <span class="c1"># Replace with actual Repo B name</span>
          <span class="na">external_repository</span><span class="pi">:</span> <span class="s">your-username/Repo-B</span>
          <span class="c1"># Branch to push to in Repo B</span>
          <span class="na">publish_branch</span><span class="pi">:</span> <span class="s">master</span>
          <span class="na">publish_dir</span><span class="pi">:</span> <span class="s">./_site</span>
          <span class="c1"># Ensures stale files in Repo B are deleted</span>
          <span class="na">keep_files</span><span class="pi">:</span> <span class="no">false</span></code></pre></div> </div>  </li> </ol>
<p>The <code>peaceiris/actions-gh-pages</code> action automatically creates the <code>.nojekyll</code> file in Repo B, preventing GitHub Pages from trying to rebuild your already-built files.</p>
<p>Repo B’s history will strictly be clean, automated deployment commits, while Repo A retains your actual human commit history and draft iterations.</p>
<h2 id="enable-client-side-content-encryption">Enable Client-Side Content Encryption</h2>
<p>To make a post “private” you have several natively supported keys to add to the post’s front matter:</p>
<ul> <li>
<code>published: false</code> —handled by Jekyll natively during its read phase—if a post has this set, Jekyll completely ignores the file, meaning no HTML is ever generated.</li> <li>
<code>hidden: true</code> —handled by Minimal Mistakes natively to exclude the post from pagination loops and recent post lists. You can still visit the post via its link.</li> <li>
<code>search: false</code> —handled by Minimal Mistakes to exclude a post from the search index. It only works when the search engine is Lunr.</li> </ul>
<p>Now we will enable true private posts by implementing client-side content encryption. We will add an <code>encrypted</code> key to control the encryption of a post. It has the following values and their corresponding meanings:</p>
<ul> <li>
<code>true</code>: Encrypt everything of the post, including the title, TOC, tags, etc.</li> <li>
<code>false</code>: No encryption, the default when unset.</li> <li>
<code>buttitle</code>: Encrypt everything but the title.</li> </ul>
<p>Then we use a single, secure master password to decrypt the contents.</p>
<p>Here is an efficient, dependency-free approach to implement this. By utilizing Ruby’s standard <code>openssl</code> library during the build phase, you can encrypt the post content before it’s injected into the layout. On the client side, we will use the standard <code>CryptoJS</code> library via CDN to prompt for the password and decrypt the content directly in the browser.</p>
<h3 id="create-the-encryption-plugin">Create the Encryption Plugin</h3>
<p>Why write custom encryption instead of using a gem like <code>jekyll-crypt</code>?</p>
<ul> <li>Dependency Rot: Most Jekyll encryption gems (like <code>jekyll-crypt</code>) haven’t been updated in over 5 to 8 years. They often rely on outdated versions of CryptoJS or deprecated Ruby OpenSSL methods, which can pose security risks or cause build failures on modern Ruby 3.x environments.</li> <li>UI Integration: Gems force their own rigid HTML/CSS structures for the password prompt. A custom plugin allows you to seamlessly integrate the prompt into your Minimal Mistakes theme so it matches your site’s exact aesthetic.</li> <li>Zero Bloat: You avoid adding third-party dependencies to your <code>Gemfile</code>. Ruby’s native <code>openssl</code> library is already present, making the custom script the fastest, most lightweight solution.</li> </ul>
<p>Create a new file at <code>_plugins/encrypt_post.rb</code> and add the following code. This hooks into Jekyll’s build process right after the markdown/org file is converted to HTML, but before it gets wrapped in your site’s layout.</p>

            <div class="language-ruby highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'openssl'</span>
<span class="nb">require</span> <span class="s1">'base64'</span>
<span class="nb">require</span> <span class="s1">'nokogiri'</span>
<span class="nb">require</span> <span class="s1">'erb'</span>

<span class="no">Jekyll</span><span class="o">::</span><span class="no">Hooks</span><span class="p">.</span><span class="nf">register</span> <span class="p">[</span><span class="ss">:pages</span><span class="p">,</span> <span class="ss">:documents</span><span class="p">],</span> <span class="ss">:post_render</span> <span class="k">do</span> <span class="o">|</span><span class="n">doc</span><span class="o">|</span>
  <span class="n">enc</span> <span class="o">=</span> <span class="n">doc</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'encrypted'</span><span class="p">]</span>
  <span class="k">if</span> <span class="n">enc</span> <span class="o">==</span> <span class="kp">true</span> <span class="o">||</span> <span class="n">enc</span> <span class="o">==</span> <span class="s1">'true'</span> <span class="o">||</span> <span class="n">enc</span> <span class="o">==</span> <span class="s1">'buttitle'</span>
    <span class="n">doc</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'excerpt'</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"This post is password protected."</span>

    <span class="k">if</span> <span class="n">enc</span> <span class="o">==</span> <span class="kp">true</span> <span class="o">||</span> <span class="n">enc</span> <span class="o">==</span> <span class="s1">'true'</span>
      <span class="n">doc</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'title'</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"🔒 Protected Content"</span>
      <span class="n">doc</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'read_time'</span><span class="p">]</span> <span class="o">=</span> <span class="kp">false</span>
    <span class="k">end</span>

    <span class="n">password</span> <span class="o">=</span> <span class="no">ENV</span><span class="p">[</span><span class="s1">'BLOG_PASSWORD'</span><span class="p">]</span>

    <span class="k">if</span> <span class="n">password</span><span class="p">.</span><span class="nf">nil?</span> <span class="o">||</span> <span class="n">password</span><span class="p">.</span><span class="nf">empty?</span>
      <span class="n">doc</span><span class="p">.</span><span class="nf">output</span><span class="p">.</span><span class="nf">gsub!</span><span class="p">(</span><span class="sr">/&lt;body&gt;.*&lt;\/body&gt;/m</span><span class="p">,</span> <span class="s2">"&lt;body&gt;&lt;p style='color:red;'&gt;Configuration error: Environment variable BLOG_PASSWORD is missing.&lt;/p&gt;&lt;/body&gt;"</span><span class="p">)</span>
      <span class="k">next</span>
    <span class="k">end</span>

    <span class="c1"># Parse the fully generated HTML page</span>
    <span class="n">html</span> <span class="o">=</span> <span class="no">Nokogiri</span><span class="o">::</span><span class="no">HTML</span><span class="p">(</span><span class="n">doc</span><span class="p">.</span><span class="nf">output</span><span class="p">)</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">enc</span> <span class="o">==</span> <span class="kp">true</span> <span class="o">||</span> <span class="n">enc</span> <span class="o">==</span> <span class="s1">'true'</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">doc</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'real_title'</span><span class="p">]</span>
      <span class="n">title_node</span> <span class="o">=</span> <span class="n">html</span><span class="p">.</span><span class="nf">at_css</span><span class="p">(</span><span class="s1">'h1.page__title'</span><span class="p">)</span>
      <span class="n">title_node</span><span class="p">.</span><span class="nf">content</span> <span class="o">=</span> <span class="n">doc</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'real_title'</span><span class="p">]</span> <span class="k">if</span> <span class="n">title_node</span>
    <span class="k">end</span>

    <span class="c1"># Target the specific Minimal Mistakes layout blocks</span>
    <span class="n">selectors</span> <span class="o">=</span> <span class="p">[</span>
      <span class="s1">'section.page__content'</span><span class="p">,</span>  <span class="c1"># The main text + TOC</span>
      <span class="s1">'footer.page__meta'</span><span class="p">,</span>      <span class="c1"># Tags and categories</span>
      <span class="s1">'section.page__share'</span><span class="p">,</span>    <span class="c1"># Share block</span>
      <span class="s1">'div.page__comments'</span><span class="p">,</span>     <span class="c1"># Comments block</span>
      <span class="s1">'div.page__related'</span>       <span class="c1"># Related posts grid</span>
    <span class="p">]</span>

    <span class="c1"># If fully encrypted, we also strip and encrypt the title header</span>
    <span class="k">if</span> <span class="n">enc</span> <span class="o">==</span> <span class="kp">true</span> <span class="o">||</span> <span class="n">enc</span> <span class="o">==</span> <span class="s1">'true'</span>
      <span class="n">selectors</span><span class="p">.</span><span class="nf">unshift</span><span class="p">(</span><span class="s1">'nav.breadcrumbs'</span><span class="p">,</span> <span class="s1">'header'</span><span class="p">)</span>
    <span class="k">end</span>

    <span class="n">payload</span> <span class="o">=</span> <span class="p">{}</span>

    <span class="c1"># Extract the HTML of the targets and replace them with placeholders</span>
    <span class="n">selectors</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">selector</span><span class="o">|</span>
      <span class="n">node</span> <span class="o">=</span> <span class="n">html</span><span class="p">.</span><span class="nf">at_css</span><span class="p">(</span><span class="n">selector</span><span class="p">)</span>
      <span class="k">if</span> <span class="n">node</span>
        <span class="n">payload</span><span class="p">[</span><span class="n">selector</span><span class="p">]</span> <span class="o">=</span> <span class="n">node</span><span class="p">.</span><span class="nf">to_html</span>
        <span class="n">safe_id</span> <span class="o">=</span> <span class="s2">"secure-placeholder-</span><span class="si">#{</span><span class="n">selector</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/[^a-zA-Z0-9]/</span><span class="p">,</span> <span class="s1">'-'</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span>
        <span class="n">node</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="s2">"&lt;div id='</span><span class="si">#{</span><span class="n">safe_id</span><span class="si">}</span><span class="s2">'&gt;&lt;/div&gt;"</span><span class="p">)</span>
      <span class="k">end</span>
    <span class="k">end</span>

    <span class="c1"># Skip if none of the elements are found (e.g., custom layouts)</span>
    <span class="k">next</span> <span class="k">if</span> <span class="n">payload</span><span class="p">.</span><span class="nf">empty?</span>

    <span class="c1"># Encrypt the extracted HTML bundle</span>
    <span class="n">cipher</span> <span class="o">=</span> <span class="no">OpenSSL</span><span class="o">::</span><span class="no">Cipher</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s1">'aes-256-cbc'</span><span class="p">)</span>
    <span class="n">cipher</span><span class="p">.</span><span class="nf">encrypt</span>
    <span class="n">iv</span> <span class="o">=</span> <span class="n">cipher</span><span class="p">.</span><span class="nf">random_iv</span>
    <span class="n">salt</span> <span class="o">=</span> <span class="no">OpenSSL</span><span class="o">::</span><span class="no">Random</span><span class="p">.</span><span class="nf">random_bytes</span><span class="p">(</span><span class="mi">16</span><span class="p">)</span>
    <span class="n">key</span> <span class="o">=</span> <span class="no">OpenSSL</span><span class="o">::</span><span class="no">PKCS5</span><span class="p">.</span><span class="nf">pbkdf2_hmac</span><span class="p">(</span><span class="n">password</span><span class="p">.</span><span class="nf">to_s</span><span class="p">,</span> <span class="n">salt</span><span class="p">,</span> <span class="mi">10000</span><span class="p">,</span> <span class="n">cipher</span><span class="p">.</span><span class="nf">key_len</span><span class="p">,</span> <span class="s1">'sha256'</span><span class="p">)</span>
    <span class="n">cipher</span><span class="p">.</span><span class="nf">key</span> <span class="o">=</span> <span class="n">key</span>

    <span class="n">encrypted_data</span> <span class="o">=</span> <span class="n">cipher</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="n">payload</span><span class="p">.</span><span class="nf">to_json</span><span class="p">)</span> <span class="o">+</span> <span class="n">cipher</span><span class="p">.</span><span class="nf">final</span>

    <span class="n">b64_iv</span> <span class="o">=</span> <span class="no">Base64</span><span class="p">.</span><span class="nf">strict_encode64</span><span class="p">(</span><span class="n">iv</span><span class="p">)</span>
    <span class="n">b64_salt</span> <span class="o">=</span> <span class="no">Base64</span><span class="p">.</span><span class="nf">strict_encode64</span><span class="p">(</span><span class="n">salt</span><span class="p">)</span>
    <span class="n">b64_ciphertext</span> <span class="o">=</span> <span class="no">Base64</span><span class="p">.</span><span class="nf">strict_encode64</span><span class="p">(</span><span class="n">encrypted_data</span><span class="p">)</span>

    <span class="c1"># Build the injection UI</span>
    <span class="n">template_path</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">doc</span><span class="p">.</span><span class="nf">site</span><span class="p">.</span><span class="nf">source</span><span class="p">,</span> <span class="s1">'_includes'</span><span class="p">,</span> <span class="s1">'secure_ui.html'</span><span class="p">)</span>
    <span class="n">secure_ui</span> <span class="o">=</span> <span class="no">ERB</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="no">File</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="n">template_path</span><span class="p">)).</span><span class="nf">result</span><span class="p">(</span><span class="nb">binding</span><span class="p">)</span>

    <span class="c1"># Inject the UI strictly inside div#main by targeting an inner placeholder</span>
    <span class="n">target_selector</span> <span class="o">=</span> <span class="p">(</span><span class="n">enc</span> <span class="o">==</span> <span class="kp">true</span> <span class="o">||</span> <span class="n">enc</span> <span class="o">==</span> <span class="s1">'true'</span><span class="p">)</span> <span class="p">?</span> <span class="s1">'header'</span> <span class="p">:</span> <span class="s1">'section.page__content'</span>
    <span class="n">target_id</span> <span class="o">=</span> <span class="s2">"secure-placeholder-</span><span class="si">#{</span><span class="n">target_selector</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/[^a-zA-Z0-9]/</span><span class="p">,</span> <span class="s1">'-'</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span>
    <span class="n">injection_node</span> <span class="o">=</span> <span class="n">html</span><span class="p">.</span><span class="nf">at_css</span><span class="p">(</span><span class="s2">"#</span><span class="si">#{</span><span class="n">target_id</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span> <span class="o">||</span> <span class="n">html</span><span class="p">.</span><span class="nf">at_css</span><span class="p">(</span><span class="s2">"#secure-placeholder-section-page--content"</span><span class="p">)</span>
    <span class="n">injection_node</span><span class="p">.</span><span class="nf">add_previous_sibling</span><span class="p">(</span><span class="n">secure_ui</span><span class="p">)</span> <span class="k">if</span> <span class="n">injection_node</span>

    <span class="c1"># Save the modified HTML back to the document</span>
    <span class="n">doc</span><span class="p">.</span><span class="nf">output</span> <span class="o">=</span> <span class="n">html</span><span class="p">.</span><span class="nf">to_html</span>
  <span class="k">end</span>
<span class="k">end</span></code></pre></div> </div>
          
<p>Create a new file at <code>_includes/secure_ui.html</code>:</p>

            <div class="language-html highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"secure-post-container"</span> <span class="na">class=</span><span class="s">"page__content"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"secure-prompt"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;h3</span> <span class="na">style=</span><span class="s">"margin-top: 0;"</span><span class="nt">&gt;</span>🔒 Protected Content<span class="nt">&lt;/h3&gt;</span>
    <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"password"</span> <span class="na">id=</span><span class="s">"secure-password"</span> <span class="na">placeholder=</span><span class="s">"Enter password"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;button</span> <span class="na">onclick=</span><span class="s">"decryptPost()"</span><span class="nt">&gt;</span>Unlock<span class="nt">&lt;/button&gt;</span>
    <span class="nt">&lt;p</span> <span class="na">id=</span><span class="s">"secure-error"</span><span class="nt">&gt;</span>Incorrect password.<span class="nt">&lt;/p&gt;</span>
  <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>

<span class="nt">&lt;style&gt;</span>
  <span class="nf">#secure-prompt</span> <span class="p">{</span>
    <span class="nl">text-align</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
    <span class="nl">padding</span><span class="p">:</span> <span class="m">40px</span><span class="p">;</span>
    <span class="nl">border-radius</span><span class="p">:</span> <span class="m">8px</span><span class="p">;</span>
    <span class="nl">background</span><span class="p">:</span> <span class="n">rgba</span><span class="p">(</span><span class="m">0</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">0.05</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="nf">#secure-prompt</span> <span class="nt">input</span> <span class="p">{</span>
    <span class="nl">padding</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span>
    <span class="nl">border</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="m">#ccc</span><span class="p">;</span>
    <span class="nl">border-radius</span><span class="p">:</span> <span class="m">4px</span><span class="p">;</span>
    <span class="nl">margin-right</span><span class="p">:</span> <span class="m">8px</span><span class="p">;</span>
    <span class="nl">max-width</span><span class="p">:</span> <span class="m">200px</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nf">#secure-prompt</span> <span class="nt">button</span> <span class="p">{</span>
    <span class="nl">padding</span><span class="p">:</span> <span class="m">10px</span> <span class="m">20px</span><span class="p">;</span>
    <span class="nl">border-radius</span><span class="p">:</span> <span class="m">4px</span><span class="p">;</span>
    <span class="nl">cursor</span><span class="p">:</span> <span class="nb">pointer</span><span class="p">;</span>
    <span class="nl">border</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
    <span class="nl">background</span><span class="p">:</span> <span class="m">#333</span><span class="p">;</span>
    <span class="nl">color</span><span class="p">:</span> <span class="no">white</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nf">#secure-error</span> <span class="p">{</span>
    <span class="nl">color</span><span class="p">:</span> <span class="m">#d9534f</span><span class="p">;</span>
    <span class="nl">display</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
    <span class="nl">margin-top</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
  <span class="p">}</span>
<span class="nt">&lt;/style&gt;</span>

<span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
<span class="nt">&lt;script&gt;</span>
  <span class="kd">function</span> <span class="nx">decryptPost</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">pwd</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">secure-password</span><span class="dl">'</span><span class="p">).</span><span class="nx">value</span><span class="p">;</span>
    <span class="kd">var</span> <span class="nx">encryptedData</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">&lt;%= b64_ciphertext %&gt;</span><span class="dl">"</span><span class="p">;</span>
    <span class="kd">var</span> <span class="nx">salt</span> <span class="o">=</span> <span class="nx">CryptoJS</span><span class="p">.</span><span class="nx">enc</span><span class="p">.</span><span class="nx">Base64</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="dl">"</span><span class="s2">&lt;%= b64_salt %&gt;</span><span class="dl">"</span><span class="p">);</span>
    <span class="kd">var</span> <span class="nx">iv</span> <span class="o">=</span> <span class="nx">CryptoJS</span><span class="p">.</span><span class="nx">enc</span><span class="p">.</span><span class="nx">Base64</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="dl">"</span><span class="s2">&lt;%= b64_iv %&gt;</span><span class="dl">"</span><span class="p">);</span>

    <span class="k">try</span> <span class="p">{</span>
      <span class="kd">var</span> <span class="nx">key</span> <span class="o">=</span> <span class="nx">CryptoJS</span><span class="p">.</span><span class="nx">PBKDF2</span><span class="p">(</span><span class="nx">pwd</span><span class="p">,</span> <span class="nx">salt</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">keySize</span><span class="p">:</span> <span class="mi">256</span> <span class="o">/</span> <span class="mi">32</span><span class="p">,</span>
        <span class="na">iterations</span><span class="p">:</span> <span class="mi">10000</span><span class="p">,</span>
        <span class="na">hasher</span><span class="p">:</span> <span class="nx">CryptoJS</span><span class="p">.</span><span class="nx">algo</span><span class="p">.</span><span class="nx">SHA256</span>
      <span class="p">});</span>
      <span class="kd">var</span> <span class="nx">cipherParams</span> <span class="o">=</span> <span class="nx">CryptoJS</span><span class="p">.</span><span class="nx">lib</span><span class="p">.</span><span class="nx">CipherParams</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span>
        <span class="na">ciphertext</span><span class="p">:</span> <span class="nx">CryptoJS</span><span class="p">.</span><span class="nx">enc</span><span class="p">.</span><span class="nx">Base64</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">encryptedData</span><span class="p">)</span>
      <span class="p">});</span>
      <span class="kd">var</span> <span class="nx">decrypted</span> <span class="o">=</span> <span class="nx">CryptoJS</span><span class="p">.</span><span class="nx">AES</span><span class="p">.</span><span class="nx">decrypt</span><span class="p">(</span><span class="nx">cipherParams</span><span class="p">,</span> <span class="nx">key</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">iv</span><span class="p">:</span> <span class="nx">iv</span>
      <span class="p">});</span>
      <span class="kd">var</span> <span class="nx">decryptedJson</span> <span class="o">=</span> <span class="nx">decrypted</span><span class="p">.</span><span class="nx">toString</span><span class="p">(</span><span class="nx">CryptoJS</span><span class="p">.</span><span class="nx">enc</span><span class="p">.</span><span class="nx">Utf8</span><span class="p">);</span>

      <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">decryptedJson</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">"</span><span class="s2">Decryption failed</span><span class="dl">"</span><span class="p">);</span>
      <span class="kd">var</span> <span class="nx">elements</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">decryptedJson</span><span class="p">);</span>

      <span class="c1">// Restore each element exactly where its placeholder is</span>
      <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">selector</span> <span class="k">in</span> <span class="nx">elements</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">safeId</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">secure-placeholder-</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">selector</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/</span><span class="se">[^</span><span class="sr">a-zA-Z0-9</span><span class="se">]</span><span class="sr">/g</span><span class="p">,</span> <span class="dl">'</span><span class="s1">-</span><span class="dl">'</span><span class="p">);</span>
        <span class="kd">var</span> <span class="nx">placeholder</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="nx">safeId</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">placeholder</span><span class="p">)</span> <span class="nx">placeholder</span><span class="p">.</span><span class="nx">outerHTML</span> <span class="o">=</span> <span class="nx">elements</span><span class="p">[</span><span class="nx">selector</span><span class="p">];</span>
      <span class="p">}</span>
      <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">secure-post-container</span><span class="dl">'</span><span class="p">).</span><span class="nx">remove</span><span class="p">();</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
      <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">secure-error</span><span class="dl">'</span><span class="p">).</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">block</span><span class="dl">'</span><span class="p">;</span>
      <span class="k">return</span><span class="p">;</span> <span class="c1">// Stop execution if decryption fails</span>
    <span class="p">}</span>

    <span class="c1">// Re-initialize Gumshoe scroll spy init</span>
    <span class="k">try</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="k">typeof</span> <span class="nx">Gumshoe</span> <span class="o">!==</span> <span class="dl">'</span><span class="s1">undefined</span><span class="dl">'</span> <span class="o">&amp;&amp;</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">nav.toc a</span><span class="dl">"</span><span class="p">))</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">spy</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Gumshoe</span><span class="p">(</span><span class="dl">"</span><span class="s2">nav.toc a</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span>
          <span class="c1">// Active classes</span>
          <span class="na">navClass</span><span class="p">:</span> <span class="dl">"</span><span class="s2">active</span><span class="dl">"</span><span class="p">,</span> <span class="c1">// applied to the nav list item</span>
          <span class="na">contentClass</span><span class="p">:</span> <span class="dl">"</span><span class="s2">active</span><span class="dl">"</span><span class="p">,</span> <span class="c1">// applied to the content</span>

          <span class="c1">// Nested navigation</span>
          <span class="na">nested</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="c1">// if true, add classes to parents of active link</span>
          <span class="na">nestedClass</span><span class="p">:</span> <span class="dl">"</span><span class="s2">active</span><span class="dl">"</span><span class="p">,</span> <span class="c1">// applied to the parent items</span>

          <span class="c1">// Offset &amp; reflow</span>
          <span class="na">offset</span><span class="p">:</span> <span class="mi">20</span><span class="p">,</span> <span class="c1">// how far from the top of the page to activate a content area</span>
          <span class="na">reflow</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="c1">// if true, listen for reflows</span>

          <span class="c1">// Event support</span>
          <span class="na">events</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="c1">// if true, emit custom events</span>
        <span class="p">});</span>
      <span class="p">}</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">console</span><span class="p">.</span><span class="nx">warn</span><span class="p">(</span><span class="dl">"</span><span class="s2">Gumshoe failed to load</span><span class="dl">"</span><span class="p">,</span> <span class="nx">e</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="c1">// Re-initialize Disqus comments</span>
    <span class="k">try</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="k">typeof</span> <span class="nx">DISQUS</span> <span class="o">!==</span> <span class="dl">'</span><span class="s1">undefined</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">DISQUS</span><span class="p">.</span><span class="nx">reset</span><span class="p">({</span>
          <span class="na">reload</span><span class="p">:</span> <span class="kc">true</span>
        <span class="p">});</span>
      <span class="p">}</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">console</span><span class="p">.</span><span class="nx">warn</span><span class="p">(</span><span class="dl">"</span><span class="s2">Disqus failed to load</span><span class="dl">"</span><span class="p">,</span> <span class="nx">e</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="c1">// Re-apply image-popup class and re-initialize Magnific Popup</span>
    <span class="k">try</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="k">typeof</span> <span class="nx">jQuery</span> <span class="o">!==</span> <span class="dl">'</span><span class="s1">undefined</span><span class="dl">'</span> <span class="o">&amp;&amp;</span> <span class="nx">jQuery</span><span class="p">.</span><span class="nx">fn</span><span class="p">.</span><span class="nx">magnificPopup</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">$imgLinks</span> <span class="o">=</span> <span class="nx">jQuery</span><span class="p">(</span><span class="dl">"</span><span class="s2">a[href$='.jpg'],a[href$='.jpeg'],a[href$='.JPG'],a[href$='.png'],a[href$='.gif'],a[href$='.webp']</span><span class="dl">"</span><span class="p">);</span>
        <span class="nx">$imgLinks</span><span class="p">.</span><span class="nx">addClass</span><span class="p">(</span><span class="dl">"</span><span class="s2">image-popup</span><span class="dl">"</span><span class="p">);</span>
        <span class="nx">jQuery</span><span class="p">(</span><span class="dl">'</span><span class="s1">.image-popup</span><span class="dl">'</span><span class="p">).</span><span class="nx">magnificPopup</span><span class="p">({</span>
          <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">image</span><span class="dl">'</span><span class="p">,</span>
          <span class="na">tLoading</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Loading image #%curr%...</span><span class="dl">'</span><span class="p">,</span>
          <span class="na">gallery</span><span class="p">:</span> <span class="p">{</span> <span class="na">enabled</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">navigateByImgClick</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">preload</span><span class="p">:</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">]</span> <span class="p">},</span>
          <span class="na">image</span><span class="p">:</span> <span class="p">{</span> <span class="na">tError</span><span class="p">:</span> <span class="dl">'</span><span class="s1">&lt;a href="%url%"&gt;Image #%curr%&lt;/a&gt; could not be loaded.</span><span class="dl">'</span> <span class="p">},</span>
          <span class="na">removalDelay</span><span class="p">:</span> <span class="mi">500</span><span class="p">,</span>
          <span class="na">mainClass</span><span class="p">:</span> <span class="dl">'</span><span class="s1">mfp-zoom-in</span><span class="dl">'</span><span class="p">,</span>
          <span class="na">callbacks</span><span class="p">:</span> <span class="p">{</span>
            <span class="na">beforeOpen</span><span class="p">:</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
              <span class="k">this</span><span class="p">.</span><span class="nx">st</span><span class="p">.</span><span class="nx">image</span><span class="p">.</span><span class="nx">markup</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">st</span><span class="p">.</span><span class="nx">image</span><span class="p">.</span><span class="nx">markup</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="dl">'</span><span class="s1">mfp-figure</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">mfp-figure mfp-with-anim</span><span class="dl">'</span><span class="p">);</span>
            <span class="p">}</span>
          <span class="p">},</span>
          <span class="na">closeOnContentClick</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
          <span class="na">midClick</span><span class="p">:</span> <span class="kc">true</span>
        <span class="p">});</span>
      <span class="p">}</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">console</span><span class="p">.</span><span class="nx">warn</span><span class="p">(</span><span class="dl">"</span><span class="s2">Magnific Popup failed to load</span><span class="dl">"</span><span class="p">,</span> <span class="nx">e</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">DOMContentLoaded</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">pwdInput</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">secure-password</span><span class="dl">'</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">pwdInput</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">pwdInput</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">keypress</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">key</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">Enter</span><span class="dl">'</span><span class="p">)</span> <span class="nx">decryptPost</span><span class="p">();</span>
      <span class="p">});</span>
    <span class="p">}</span>
  <span class="p">});</span>
<span class="nt">&lt;/script&gt;</span></code></pre></div> </div>
          
<p>Can we do the encryption after everything is initialized or generated so that we don’t have to handle the re-initializations of Gumshoe, Disqus, image-popup, etc.?</p>
<p>No, it is fundamentally impossible to do this without completely destroying the security of your private posts.</p>
<p>Here is the architectural reason why re-initialization is mathematically unavoidable for true client-side encryption:</p>
<ul> <li>
<p>The Security Paradox </p>
<p>For plugins like Gumshoe, Disqus, and Magnific Popup to initialize naturally when the page first loads, the raw, unencrypted HTML (your headings, image links, and text) <b>must</b> physically exist in the browser’s Document Object Model (DOM) at the exact moment the page finishes loading.</p> <p>If the unencrypted HTML is in the DOM when the page loads, the encryption is completely fake. Anyone could simply right-click the page, select “View Page Source,” bypass your password prompt, and read the secret content in plain text. Search engine bots would also scrape it immediately.</p> </li> <li>
<p>The Secure Timeline </p>
<p>True privacy requires the server to scramble the data <i>before</i> it ever travels over the network. Here is the timeline of what currently happens, which is the industry standard for secure client-side decryption:</p> <ol> <li>
<b>The Server (Jekyll):</b> Encrypts the HTML and packs it into a scrambled string. The raw content is deleted from the final <code>.html</code> file.</li> <li>
<b>The Browser Load:</b> The browser downloads the page. The JS plugins (Gumshoe, Disqus) fire immediately. They scan the page, see no headings and no galleries, and do nothing.</li> <li>
<b>The User Input:</b> The user types the password. The JavaScript decrypts the scrambled string and injects the restored HTML into the page dynamically.</li> <li>
<b>The Re-initialization:</b> Because the plugins have already run and gone to sleep during Step 2, you <b>must</b> manually wake them up and tell them to scan the page again now that the content has appeared.</li> </ol> </li> <li>
<p>The Verdict </p>
<p>What you are doing right now—manually triggering <code>.magnificPopup()</code> and <code>DISQUS.reset()</code> inside the success block—is actually the fastest, most efficient, and standard best practice in modern web development for handling dynamically injected DOM content (like in Single Page Applications or AJAX calls).</p> <p>Attempting to delay the entire site’s JavaScript execution until after a user types a password would cause massive performance bottlenecks and break the layout of your public elements (like the site header and navigation menu) while the user is looking at the password prompt.</p> </li> </ul>
<h3 id="use-a-single-secure-master-password">Use a single, secure master password</h3>
<p>The industry-standard way to handle secrets without hardcoding them in configuration files or front matter is through Environment Variables. You will store the password as a GitHub Secret for your automated deployments, and pass it inline when testing locally.</p>
<ol> <li>
<p>Update the GitHub Action </p>
<p>First, go to your private repository settings on GitHub (Settings &gt; Secrets and variables &gt; Actions). Add a new Repository Secret named POST_PASSWORD and enter your master password.</p> <p>Then, update your workflow file to pass this secret into the build environment:</p>  <div class="language-diff highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="gd">--- .github/workflows/jekyll.yml
</span><span class="gi">+++ .github/workflows/jekyll.yml
</span><span class="p">@@ -25,2 +25,3 @@</span>
         env:
           JEKYLL_ENV: production
<span class="gi">+          POST_PASSWORD: $</span></code></pre></div> </div>  </li> <li>
<p>Local Usage </p>
<p>When you are writing locally and need to test an encrypted post, you simply pass the variable directly into your terminal command before serving:</p>  <div class="language-sh highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="nv">POST_PASSWORD</span><span class="o">=</span><span class="s2">"your_master_password"</span> bundle <span class="nb">exec </span>jekyll serve</code></pre></div> </div>  <p>If you don’t want to type it every time, you can add to your <code>~/.zshrc</code> or <code>~/.bashrc</code> file:</p>  <div class="language-sh highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">POST_PASSWORD</span><span class="o">=</span><span class="s2">"your_master_password"</span></code></pre></div> </div>  </li> </ol>
<h3 id="use-in-front-matter">Use in Front Matter</h3>
<p>Now, you can define your visibility state across your Markdown or Org files effortlessly.</p>

            <div class="language-text highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>---
title: "My Post"
published: true
hidden: false
search: false
encrypted: true
---</code></pre></div> </div>
          
<h1 id="testing">Testing</h1>
<p>This gallery is used to check if gallery works normally after decryption:</p>






<figure class="custom-grid " style="grid-template-columns: repeat(6, 1fr);"> <figure> <a href="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" title="Use alt for title"> <img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt="Use alt for title"> </a> </figure> <figure> <a href="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" title='&lt;p&gt;Link to post: &lt;a href="/blog/web/local-images/"&gt;Local images&lt;/a&gt;&lt;/p&gt;'> <img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt=""> </a> </figure> <figure> <a href="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg"> <img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt=""> </a> </figure> <figcaption>My custom gallery </figcaption> </figure>]]></content><author><name>Joseph Huang</name></author><category term="Web" /><category term="Minimal Mistakes" /><summary type="html"><![CDATA[This post is password protected.]]></summary></entry><entry><title type="html">Customizing Gallery in Minimal Mistakes</title><link href="https://josephtesfaye.com/blog/web/custom-gallery/" rel="alternate" type="text/html" title="Customizing Gallery in Minimal Mistakes" /><published>2026-02-20T00:00:00+00:00</published><updated>2026-02-20T00:00:00+00:00</updated><id>https://josephtesfaye.com/blog/web/custom-gallery</id><content type="html" xml:base="https://josephtesfaye.com/blog/web/custom-gallery/"><![CDATA[<p>Here are the customizations I’ve made to the <a href="https://mmistakes.github.io/minimal-mistakes/post%20formats/post-gallery/">gallery</a> feature of Minimal Mistakes:</p>
<ol> <li>
<p>Add a <code>columns</code> parameter to each gallery to control the columns of a gallery such as: </p>
<div class="language-text highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>{% include gallery columns=6 %}</code></pre></div> </div>  <p>The image size should be adjusted automatically.</p> </li> <li>
<p>Support adding images to a gallery by only using the <code>image_path</code> parameter or the URL in the front matter such as: </p>
<div class="language-text highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>gallery:
  - image_path: /assets/archive/image/foo/unsplash-gallery-image-1.jpg
  - /assets/archive/image/foo/unsplash-gallery-image-1.jpg
  - /assets/archive/image/foo/unsplash-gallery-image-1.jpg</code></pre></div> </div>  <p>The gallery originally requires both <code>url</code> and <code>image_path</code> in order to have a pop-up view of the images. But I think it’s more convenient to write in this way. But you can still use both.</p> </li> <li>Use <code>alt</code> for <code>title</code> if <code>title</code> is unspecified.</li> <li>
<p>Support Liquid links in the image title, e.g., </p>
<div class="language-text highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>---
gallery:
  - image_path: https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg
    title: "Prompt: [[{{ site.baseurl }}{% post_url 2026-02-19-local-images %}][Local images]]"
---</code></pre></div> </div>  </li> </ol>
<p>All of these customizations are backward-compatible, that is, you can still write a gallery in the original way to get the original feel.</p>
<p>Here’s a demo gallery with the above customizations:</p>

            <div class="language-text highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>---
link_abbrs:
  - link_abbr: foo https://i.postimg.cc/Vv8jFw8D/
gallery:
  - image_path: foo:unsplash-gallery-image-1.jpg
    alt: "Use alt for title"
  - image_path: foo:unsplash-gallery-image-1.jpg
    title: "Link to post: [[{{ site.baseurl }}{% post_url 2026-02-19-local-images %}][Local images]]"
  - image_path: foo:unsplash-gallery-image-1.jpg
  - image_path: foo:unsplash-gallery-image-1.jpg
  - image_path: foo:unsplash-gallery-image-1.jpg
  - image_path: foo:unsplash-gallery-image-1.jpg
  - image_path: foo:unsplash-gallery-image-1.jpg
  - image_path: foo:unsplash-gallery-image-1.jpg
  - image_path: foo:unsplash-gallery-image-1.jpg
---

{% include gallery columns=6 caption="My custom gallery" %}</code></pre></div> </div>
          






<figure class="custom-grid " style="grid-template-columns: repeat(6, 1fr);"> <figure> <a href="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" title="Use alt for title"> <img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt="Use alt for title"> </a> </figure> <figure> <a href="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" title='&lt;p&gt;Link to post: &lt;a href="/blog/web/local-images/"&gt;Local images&lt;/a&gt;&lt;/p&gt;'> <img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt=""> </a> </figure> <figure> <a href="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg"> <img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt=""> </a> </figure> <figure> <a href="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg"> <img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt=""> </a> </figure> <figure> <a href="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg"> <img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt=""> </a> </figure> <figure> <a href="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg"> <img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt=""> </a> </figure> <figure> <a href="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg"> <img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt=""> </a> </figure> <figure> <a href="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg"> <img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt=""> </a> </figure> <figure> <a href="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg"> <img src="https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg" alt=""> </a> </figure> <figcaption>My custom gallery </figcaption> </figure>



<p>Here are the steps to make the changes:</p>
<ol> <li>Copy the default gallery layout from the Minimal Mistakes gem (e.g. <code>~/.gem/ruby/3.1.3/gems/minimal-mistakes-jekyll-4.26.0/_includes/gallery</code>) into your local <code>_includes</code> directory.</li> <li>
<p>Apply these changes: </p>
<div class="language-diff highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="gh">diff -u minimal-mistakes/_includes/gallery josephs-blog/_includes/gallery
</span><span class="gd">--- minimal-mistakes/_includes/gallery	2023-12-15 19:03:51
</span><span class="gi">+++ josephs-blog/_includes/gallery	2026-03-01 23:18:56
</span><span class="p">@@ -6,6 +6,8 @@</span>

 {% if include.layout %}
   {% assign gallery_layout = include.layout %}
<span class="gi">+{% elsif include.columns %}
+  {% assign gallery_layout = "custom-grid" %}
</span> {% else %}
   {% if gallery.size == 2 %}
     {% assign gallery_layout = 'half' %}
<span class="p">@@ -16,6 +18,48 @@</span>
   {% endif %}
 {% endif %}

+{% if include.columns %}
<span class="gi">+
+&lt;figure class="{{ gallery_layout }} {{ include.class }}"
+        style="grid-template-columns: repeat({{ include.columns }}, 1fr);"&gt;
+  {% for img in gallery %}
+    &lt;figure&gt;
+      &lt;a {% if img.url %}
+           href="{{ img.url | relative_url }}"
+         {% elsif img.image_path %}
+           href="{{ img.image_path | relative_url }}"
+         {% else %}
+           href="{{ img | relative_url }}"
+         {% endif %}
+
+         {% if img.title %}
+         {% comment %} Parse Liquid syntax in titles {% endcomment %}
+           title="{{ img.title | liquify | markdownify | remove: '&lt;p&gt;' |
+                     remove: '&lt;/p&gt;' | strip | newline_to_br | split: '&lt;br /&gt;' |
+                     join: '&lt;/p&gt;&lt;p&gt;' | prepend: '&lt;p&gt;' | append: '&lt;/p&gt;' |
+                     escape }}"
+         {% elsif img.alt %}
+           title="{{ img.alt }}"
+         {% endif %}&gt;
+
+        &lt;img {% if img.image_path %}
+               src="{{ img.image_path | relative_url }}"
+             {% else %}
+               src="{{ img | relative_url }}"
+             {% endif %}
+
+             alt="{% if img.alt %}{{ img.alt }}{% endif %}"&gt;
+      &lt;/a&gt;
+    &lt;/figure&gt;
+  {% endfor %}
+  {% if include.caption %}
+    &lt;figcaption&gt;{{ include.caption | markdownify | remove: "&lt;p&gt;" | remove: "&lt;/p&gt;" }}&lt;/figcaption&gt;
+  {% endif %}
+&lt;/figure&gt;
+
+{% else %}
+
+&lt;!-- Preserve the original style --&gt;
</span> &lt;figure class="{{ gallery_layout }} {{ include.class }}"&gt;
   {% for img in gallery %}
     {% if img.url %}
<span class="p">@@ -33,3 +77,5 @@</span>
     &lt;figcaption&gt;{{ include.caption | markdownify | remove: "&lt;p&gt;" | remove: "&lt;/p&gt;" }}&lt;/figcaption&gt;
   {% endif %}
 &lt;/figure&gt;
<span class="gi">+
+{% endif %}</span></code></pre></div> </div>  </li> <li>
<p>Add these styles in your <code>assets/css/main.scss</code>: </p>
<div class="language-css highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="o">//</span> <span class="nt">Enable</span> <span class="nt">more</span> <span class="nt">columns</span> <span class="nt">in</span> <span class="nt">a</span> <span class="nt">gallery</span>
<span class="nc">.page__content</span> <span class="nc">.custom-grid</span> <span class="p">{</span>
  <span class="nl">display</span><span class="p">:</span> <span class="n">grid</span><span class="p">;</span>
  <span class="py">gap</span><span class="p">:</span> <span class="m">0.5em</span><span class="p">;</span>
<span class="p">}</span>

<span class="nc">.custom-grid</span> <span class="nt">figure</span> <span class="p">{</span>
  <span class="nl">margin</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
  <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="p">}</span>

<span class="nc">.custom-grid</span> <span class="nt">figcaption</span> <span class="p">{</span>
  <span class="nl">grid-column</span><span class="p">:</span> <span class="m">1</span> <span class="p">/</span> <span class="m">-1</span><span class="p">;</span>
<span class="p">}</span>

<span class="nc">.custom-grid</span> <span class="nt">img</span> <span class="p">{</span>
  <span class="nl">margin-bottom</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span></code></pre></div> </div>  </li> <li>
<p>Create a new plugin file <code>_plugins/liquify.rb</code> to add a filter that evaluates Liquid syntax inside front matter strings: </p>
<div class="language-ruby highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">Jekyll</span>
  <span class="k">module</span> <span class="nn">LiquifyFilter</span>
    <span class="k">def</span> <span class="nf">liquify</span><span class="p">(</span><span class="n">input</span><span class="p">)</span>
      <span class="no">Liquid</span><span class="o">::</span><span class="no">Template</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">input</span><span class="p">.</span><span class="nf">to_s</span><span class="p">).</span><span class="nf">render</span><span class="p">(</span><span class="vi">@context</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
<span class="no">Liquid</span><span class="o">::</span><span class="no">Template</span><span class="p">.</span><span class="nf">register_filter</span><span class="p">(</span><span class="no">Jekyll</span><span class="o">::</span><span class="no">LiquifyFilter</span><span class="p">)</span></code></pre></div> </div>  </li> </ol>]]></content><author><name>Joseph Huang</name></author><category term="Web" /><category term="Minimal Mistakes" /><summary type="html"><![CDATA[Here are the customizations I’ve made to the gallery feature of Minimal Mistakes:]]></summary></entry><entry><title type="html">Enabling link abbreviations in Minimal Mistakes</title><link href="https://josephtesfaye.com/blog/web/link-abbr/" rel="alternate" type="text/html" title="Enabling link abbreviations in Minimal Mistakes" /><published>2026-02-19T00:00:00+00:00</published><updated>2026-03-06T00:00:00+00:00</updated><id>https://josephtesfaye.com/blog/web/link-abbr</id><content type="html" xml:base="https://josephtesfaye.com/blog/web/link-abbr/"><![CDATA[<p>I want to be able to use link abbreviations in Minimal Mistakes, for example, use <code>foo:&lt;filename&gt;</code> in place of the full link for images and images in a gallery like the following:</p>

            <div class="language-markdown highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">title</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Link</span><span class="nv"> </span><span class="s">Abbreviations"</span>
<span class="na">link_abbrs</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">link_abbr</span><span class="pi">:</span> <span class="s">foo https://i.postimg.cc/Vv8jFw8D/</span>
<span class="na">gallery</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">url</span><span class="pi">:</span> <span class="s">foo:unsplash-gallery-image-1.jpg</span>
    <span class="na">image_path</span><span class="pi">:</span> <span class="s">foo:unsplash-gallery-image-1.jpg</span>
    <span class="na">title</span><span class="pi">:</span> <span class="s">Check this [[foo:unsplash-gallery-image-1.jpg][image]]</span>
  <span class="pi">-</span> <span class="na">image_path</span><span class="pi">:</span> <span class="s">foo:unsplash-gallery-image-1.jpg</span>
  <span class="pi">-</span> <span class="s">foo:unsplash-gallery-image-1.jpg</span>
<span class="nn">---</span>

<span class="p">![</span><span class="nv">An image</span><span class="p">](</span><span class="sx">foo:unsplash-gallery-image-1.jpg</span><span class="p">)</span>

{% capture fig_img %}
<span class="p">![</span><span class="nv">An image</span><span class="p">](</span><span class="sx">foo:unsplash-gallery-image-1.jpg</span><span class="p">)</span>
{% endcapture %}

<span class="nt">&lt;figure&gt;</span>
  {{ fig_img | markdownify | remove: "<span class="nt">&lt;p&gt;</span>" | remove: "<span class="nt">&lt;/p&gt;</span>" }}
  <span class="nt">&lt;figcaption&gt;</span>An image with caption<span class="nt">&lt;/figcaption&gt;</span>
<span class="nt">&lt;/figure&gt;</span>

{% include figure image_path="foo:unsplash-gallery-image-1.jpg" popup=true
alt="" caption="A figure" %}

{% include gallery columns=3 caption="A gallery" %}</code></pre></div> </div>
          
<p>The links can then be translated to their full forms in the built result:</p>

            <div class="language-text highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>https://i.postimg.cc/Vv8jFw8D/unsplash-gallery-image-1.jpg</code></pre></div> </div>
          
<p><b>Create a new Ruby plugin file</b></p>
<p>We can create a plugin under <code>_plugins/link_abbr.rb</code> which handles the link abbreviations:</p>

            <div class="language-ruby highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'addressable/uri'</span>

<span class="no">Jekyll</span><span class="o">::</span><span class="no">Hooks</span><span class="p">.</span><span class="nf">register</span> <span class="p">[</span><span class="ss">:pages</span><span class="p">,</span> <span class="ss">:documents</span><span class="p">],</span> <span class="ss">:pre_render</span> <span class="k">do</span> <span class="o">|</span><span class="n">doc</span><span class="o">|</span>
  <span class="k">next</span> <span class="k">unless</span> <span class="n">doc</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'link_abbrs'</span><span class="p">].</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Array</span><span class="p">)</span>

  <span class="n">doc</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'link_abbrs'</span><span class="p">].</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span>
    <span class="k">next</span> <span class="k">unless</span> <span class="n">item</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Hash</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">item</span><span class="p">[</span><span class="s1">'link_abbr'</span><span class="p">]</span>

    <span class="n">key</span><span class="p">,</span> <span class="n">base_url</span> <span class="o">=</span> <span class="n">item</span><span class="p">[</span><span class="s1">'link_abbr'</span><span class="p">].</span><span class="nf">split</span><span class="p">(</span><span class="s1">' '</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
    <span class="k">next</span> <span class="k">unless</span> <span class="n">key</span> <span class="o">&amp;&amp;</span> <span class="n">base_url</span>

    <span class="c1"># 1. Replaces "](key:filename)" inside the Markdown body content</span>
    <span class="n">doc</span><span class="p">.</span><span class="nf">content</span> <span class="o">=</span> <span class="n">doc</span><span class="p">.</span><span class="nf">content</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/\]\(</span><span class="si">#{</span><span class="no">Regexp</span><span class="p">.</span><span class="nf">escape</span><span class="p">(</span><span class="n">key</span><span class="p">)</span><span class="si">}</span><span class="sr">:([^\)]+)\)/</span><span class="p">)</span> <span class="k">do</span>
      <span class="n">escaped_path</span> <span class="o">=</span> <span class="no">Addressable</span><span class="o">::</span><span class="no">URI</span><span class="p">.</span><span class="nf">escape</span><span class="p">(</span><span class="n">base_url</span> <span class="o">+</span> <span class="vg">$1</span><span class="p">)</span>
      <span class="n">prefix</span> <span class="o">=</span> <span class="n">base_url</span><span class="p">.</span><span class="nf">start_with?</span><span class="p">(</span><span class="s1">'/assets'</span><span class="p">)</span> <span class="p">?</span> <span class="s2">"https://josephtesfaye.com/blog"</span> <span class="p">:</span> <span class="s2">""</span>
      <span class="s2">"](</span><span class="si">#{</span><span class="n">prefix</span><span class="si">}#{</span><span class="n">escaped_path</span><span class="si">}</span><span class="s2">)"</span>
    <span class="k">end</span>

    <span class="c1"># 2. Replace image_path in figures</span>
    <span class="n">doc</span><span class="p">.</span><span class="nf">content</span> <span class="o">=</span> <span class="n">doc</span><span class="p">.</span><span class="nf">content</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/image_path="</span><span class="si">#{</span><span class="no">Regexp</span><span class="p">.</span><span class="nf">escape</span><span class="p">(</span><span class="n">key</span><span class="p">)</span><span class="si">}</span><span class="sr">:([^"]+)"/</span><span class="p">)</span> <span class="p">{</span> <span class="s2">"image_path=</span><span class="se">\"</span><span class="si">#{</span><span class="no">Addressable</span><span class="o">::</span><span class="no">URI</span><span class="p">.</span><span class="nf">escape</span><span class="p">(</span><span class="n">base_url</span> <span class="o">+</span> <span class="vg">$1</span><span class="p">)</span><span class="si">}</span><span class="se">\"</span><span class="s2">"</span> <span class="p">}</span>

    <span class="c1"># 3. Replace inside ANY front matter array (gallery, gallery1, gallery2, etc.)</span>
    <span class="n">doc</span><span class="p">.</span><span class="nf">data</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">_</span><span class="p">,</span> <span class="n">value</span><span class="o">|</span>
      <span class="k">if</span> <span class="n">value</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Array</span><span class="p">)</span>
        <span class="n">value</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">list_item</span><span class="o">|</span>
          <span class="k">next</span> <span class="k">unless</span> <span class="n">list_item</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Hash</span><span class="p">)</span>

          <span class="p">[</span><span class="s1">'url'</span><span class="p">,</span> <span class="s1">'image_path'</span><span class="p">].</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="kp">attr</span><span class="o">|</span>
            <span class="k">if</span> <span class="n">list_item</span><span class="p">[</span><span class="kp">attr</span><span class="p">]</span> <span class="o">&amp;&amp;</span> <span class="n">list_item</span><span class="p">[</span><span class="kp">attr</span><span class="p">].</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">start_with?</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">key</span><span class="si">}</span><span class="s2">:"</span><span class="p">)</span>
              <span class="c1"># Replace the exact abbreviation prefix with the base URL</span>
              <span class="n">full_path</span> <span class="o">=</span> <span class="n">list_item</span><span class="p">[</span><span class="kp">attr</span><span class="p">].</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">sub</span><span class="p">(</span><span class="sr">/^</span><span class="si">#{</span><span class="no">Regexp</span><span class="p">.</span><span class="nf">escape</span><span class="p">(</span><span class="n">key</span><span class="p">)</span><span class="si">}</span><span class="sr">:/</span><span class="p">,</span> <span class="n">base_url</span><span class="p">)</span>
              <span class="n">list_item</span><span class="p">[</span><span class="kp">attr</span><span class="p">]</span> <span class="o">=</span> <span class="no">Addressable</span><span class="o">::</span><span class="no">URI</span><span class="p">.</span><span class="nf">escape</span><span class="p">(</span><span class="n">full_path</span><span class="p">)</span>
            <span class="k">end</span>
          <span class="k">end</span>

          <span class="k">if</span> <span class="n">list_item</span><span class="p">[</span><span class="s1">'title'</span><span class="p">]</span>
            <span class="n">list_item</span><span class="p">[</span><span class="s1">'title'</span><span class="p">]</span> <span class="o">=</span> <span class="n">list_item</span><span class="p">[</span><span class="s1">'title'</span><span class="p">].</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/\]\(</span><span class="si">#{</span><span class="no">Regexp</span><span class="p">.</span><span class="nf">escape</span><span class="p">(</span><span class="n">key</span><span class="p">)</span><span class="si">}</span><span class="sr">:([^\)]+)\)/</span><span class="p">)</span> <span class="k">do</span>
              <span class="n">escaped_path</span> <span class="o">=</span> <span class="no">Addressable</span><span class="o">::</span><span class="no">URI</span><span class="p">.</span><span class="nf">escape</span><span class="p">(</span><span class="n">base_url</span> <span class="o">+</span> <span class="vg">$1</span><span class="p">)</span>
              <span class="n">prefix</span> <span class="o">=</span> <span class="n">base_url</span><span class="p">.</span><span class="nf">start_with?</span><span class="p">(</span><span class="s1">'/assets'</span><span class="p">)</span> <span class="p">?</span> <span class="s2">"https://josephtesfaye.com/blog"</span> <span class="p">:</span> <span class="s2">""</span>
              <span class="s2">"](</span><span class="si">#{</span><span class="n">prefix</span><span class="si">}#{</span><span class="n">escaped_path</span><span class="si">}</span><span class="s2">)"</span>
            <span class="k">end</span>
          <span class="k">end</span>
        <span class="k">end</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span></code></pre></div> </div>
          
<p>The <code>github-pages</code> gem enforces strict safe mode and completely disables the <code>_plugins</code> directory to mimic GitHub’s legacy deployment environment, overriding any <code>safe: false</code> settings in your configuration.</p>
<p>To allow custom plugins to run locally, you need to replace it with the standard Jekyll gem. Since you already have all your plugins explicitly listed in your Gemfile, this is a safe swap.</p>
<p>Gemfile:</p>

            <div class="language-diff highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="gd">-gem "github-pages", group: :jekyll_plugins
</span><span class="gi">+gem "jekyll"
+gem "kramdown-parser-gfm"
+gem "faraday-retry"</span></code></pre></div> </div>
          
<p>_config.yml:</p>

            <div class="language-diff highlighter-rouge"> <div class="highlight"><pre class="highlight"><code><span class="gd">-remote_theme: mmistakes/minimal-mistakes@4.20.2
</span><span class="gi">+theme: minimal-mistakes-jekyll</span></code></pre></div> </div>
          
<p>Terminal:</p>

            <div class="language-sh highlighter-rouge"> <div class="highlight"><pre class="highlight"><code>bundle <span class="nb">install
</span>bundle <span class="nb">exec </span>jekyll serve</code></pre></div> </div>
          
<p><b>Use GitHub Actions to build your GitHub Pages site</b></p>
<p>This affects the default GitHub Pages build. The classic GitHub Pages build environment strictly enforces safe mode and ignores all files in the <code>_plugins</code> directory. If you push the site using the default settings, it will still host successfully, but your custom plugin will not run and the link abbreviations will remain unparsed.</p>
<p>To use custom plugins while hosting on GitHub Pages, you must switch your deployment method to GitHub Actions. This modern approach uses your exact Gemfile, executes your Ruby scripts, and then hosts the final output on GitHub Pages.</p>
<ol> <li>Go to your repository on GitHub.</li> <li>Click on Settings, then select Pages from the left sidebar.</li> <li>Under Build and deployment, change the Source from “Deploy from a branch” to “GitHub Actions”.</li> <li>GitHub will suggest a default Jekyll workflow. Click Configure.</li> <li>Commit the provided <code>.github/workflows/jekyll.yml</code> file to your repository.</li> </ol>
<p>With this workflow active, GitHub will read your updated Gemfile, run the <code>link_abbr.rb</code> plugin during the build step, and deploy the fully rendered HTML to your live site.</p>]]></content><author><name>Joseph Huang</name></author><category term="Web" /><category term="Minimal Mistakes" /><summary type="html"><![CDATA[I want to be able to use link abbreviations in Minimal Mistakes, for example, use foo:&lt;filename&gt; in place of the full link for images and images in a gallery like the following:]]></summary></entry><entry><title type="html">Loading Local Images in Minimal Mistakes</title><link href="https://josephtesfaye.com/blog/web/local-images/" rel="alternate" type="text/html" title="Loading Local Images in Minimal Mistakes" /><published>2026-02-19T00:00:00+00:00</published><updated>2026-02-19T00:00:00+00:00</updated><id>https://josephtesfaye.com/blog/web/local-images</id><content type="html" xml:base="https://josephtesfaye.com/blog/web/local-images/"><![CDATA[<p>I want to be able to load local images when the site is served locally. However,
the URL format <code class="language-plaintext highlighter-rouge">file:///Users/&lt;username&gt;/archive/image/20260218184820.png</code>
doesn’t work.</p>

<h2 id="why-img-srcfileusers-wont-load">Why <code class="language-plaintext highlighter-rouge">&lt;img src="file:///Users/..."&gt;</code> won’t load?</h2>

<p>Your page is being served over HTTP (e.g. <a href="http://localhost:4000">http://localhost:4000</a>). Modern
browsers block pages from <code class="language-plaintext highlighter-rouge">http(s)://…</code> from loading local files via <code class="language-plaintext highlighter-rouge">file://…</code>
for security (“cross-origin” / local file access). So even though the HTML is
“correctly parsed”, the browser refuses to fetch it.</p>

<p>That means: a <code class="language-plaintext highlighter-rouge">file://</code> URL will not work on a website (local dev server or
deployed site). It only works when the page itself is also opened via <code class="language-plaintext highlighter-rouge">file://</code>
(and even then, policies vary).</p>

<h2 id="method-1-copying-files-to-project-assets">Method 1: Copying files to project assets</h2>

<p>The simplest method is to copy the images to <code class="language-plaintext highlighter-rouge">&lt;project-root&gt;/assets/images/</code> and
then refer to them using the standard syntax:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>---
gallery:
  - url: /assets/images/20260218184820.png
    image_path: /assets/images/20260218184820.png
---

![alt]({{ site.url }}{{ site.baseurl }}/assets/images/filename.jpg)

&lt;img src="https://josephtesfaye.com/blog/assets/images/filename.jpg" alt=""&gt;

{% include gallery %}
</code></pre></div></div>

<p>However, this has the overhead of managing files manually, which can be tedious.
The following methods give you better experience.</p>

<h2 id="method-2-using-symlinks">Method 2: Using symlinks</h2>

<ol>
  <li>
    <p>Create a symlink to the target data inside the site root. Say, if the local
images reside in <code class="language-plaintext highlighter-rouge">~/Downloads/temp/archive/image/foo/</code> you can create a
symlink to it under <code class="language-plaintext highlighter-rouge">&lt;project-root&gt;/_site/assets/archive/image/foo</code>:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> &lt;project-root&gt;/_site/
<span class="nb">mkdir</span> <span class="nt">-p</span> assets/archive/image/
<span class="nb">ln</span> <span class="nt">-s</span> <span class="s2">"~/Downloads/temp/archive/image/foo"</span> <span class="s2">"assets/archive/image/foo"</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>Then you can refer to the images using the symlink like the following:</p>

    <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>---
gallery:
  - url: /assets/archive/image/foo/20260218184820.png
    image_path: /assets/archive/image/foo/20260218184820.png
  - url: /assets/archive/image/foo/20260218184820.png
    image_path: /assets/archive/image/foo/20260218184820.png
---

![]({{site.url}}{{site.baseurl}}/assets/archive/image/foo/20260218184820.png)

{% include gallery %}
</code></pre></div>    </div>
  </li>
  <li>
    <p>The symlinks created this way are cleared every time the site is re-built.
To make them persist we can write a plugin
<code class="language-plaintext highlighter-rouge">_plugins/symlink_external_assets.rb</code> to create them automatically according
to the following front matter of a post:</p>

    <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>---
symlinks:
  - /assets/archive/image/foo ~/Downloads/temp/archive/image/foo
  - /assets/archive/video/foo ~/Downloads/temp/archive/video/foo
---
</code></pre></div>    </div>

    <p><code class="language-plaintext highlighter-rouge">symlink_external_assets.rb</code>:</p>

    <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'fileutils'</span>
<span class="nb">require</span> <span class="s1">'shellwords'</span>

<span class="no">Jekyll</span><span class="o">::</span><span class="no">Hooks</span><span class="p">.</span><span class="nf">register</span> <span class="ss">:site</span><span class="p">,</span> <span class="ss">:post_read</span> <span class="k">do</span> <span class="o">|</span><span class="n">site</span><span class="o">|</span>
  <span class="k">return</span> <span class="k">if</span> <span class="no">Jekyll</span><span class="p">.</span><span class="nf">env</span> <span class="o">==</span> <span class="s1">'production'</span>

  <span class="c1"># Store the intended symlinks so we can recreate them in the _site folder later</span>
  <span class="n">site</span><span class="p">.</span><span class="nf">config</span><span class="p">[</span><span class="s1">'dynamic_symlinks'</span><span class="p">]</span> <span class="o">||=</span> <span class="p">{}</span>

  <span class="n">docs</span> <span class="o">=</span> <span class="n">site</span><span class="p">.</span><span class="nf">pages</span> <span class="o">+</span> <span class="n">site</span><span class="p">.</span><span class="nf">collections</span><span class="p">.</span><span class="nf">values</span><span class="p">.</span><span class="nf">flat_map</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:docs</span><span class="p">)</span>
  <span class="n">docs</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">doc</span><span class="o">|</span>
    <span class="k">next</span> <span class="k">unless</span> <span class="n">doc</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'symlinks'</span><span class="p">].</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Array</span><span class="p">)</span>

    <span class="n">doc</span><span class="p">.</span><span class="nf">data</span><span class="p">[</span><span class="s1">'symlinks'</span><span class="p">].</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">symlink_def</span><span class="o">|</span>
      <span class="k">next</span> <span class="k">unless</span> <span class="n">symlink_def</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">String</span><span class="p">)</span>

      <span class="n">parts</span> <span class="o">=</span> <span class="no">Shellwords</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="n">symlink_def</span><span class="p">)</span>
      <span class="k">next</span> <span class="k">unless</span> <span class="n">parts</span><span class="p">.</span><span class="nf">size</span> <span class="o">==</span> <span class="mi">2</span>

      <span class="n">link_path_raw</span><span class="p">,</span> <span class="n">target_path_raw</span> <span class="o">=</span> <span class="n">parts</span>

      <span class="n">target_path</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">expand_path</span><span class="p">(</span><span class="n">target_path_raw</span><span class="p">)</span>
      <span class="n">relative_link_path</span> <span class="o">=</span> <span class="n">link_path_raw</span><span class="p">.</span><span class="nf">sub</span><span class="p">(</span><span class="sr">%r{^/}</span><span class="p">,</span> <span class="s1">''</span><span class="p">)</span>
      <span class="n">link_path</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">site</span><span class="p">.</span><span class="nf">dest</span><span class="p">,</span> <span class="n">relative_link_path</span><span class="p">)</span>

      <span class="c1"># Verify target exists</span>
      <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">exist?</span><span class="p">(</span><span class="n">target_path</span><span class="p">)</span>
        <span class="no">Jekyll</span><span class="p">.</span><span class="nf">logger</span><span class="p">.</span><span class="nf">warn</span> <span class="s2">"Symlink Plugin:"</span><span class="p">,</span> <span class="s2">"Skipped. Target does not exist: </span><span class="si">#{</span><span class="n">link_path</span><span class="si">}</span><span class="s2"> -&gt; </span><span class="si">#{</span><span class="n">target_path</span><span class="si">}</span><span class="s2">"</span>
        <span class="k">next</span>
      <span class="k">end</span>

      <span class="c1"># Save for the post_write phase</span>
      <span class="n">site</span><span class="p">.</span><span class="nf">config</span><span class="p">[</span><span class="s1">'dynamic_symlinks'</span><span class="p">][</span><span class="n">relative_link_path</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
        <span class="ss">target: </span><span class="n">target_path</span><span class="p">,</span>
        <span class="ss">link: </span><span class="n">link_path</span>
      <span class="p">}</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="c1"># Inject the symlinks directly into the final build folder</span>
<span class="no">Jekyll</span><span class="o">::</span><span class="no">Hooks</span><span class="p">.</span><span class="nf">register</span> <span class="ss">:site</span><span class="p">,</span> <span class="ss">:post_write</span> <span class="k">do</span> <span class="o">|</span><span class="n">site</span><span class="o">|</span>
  <span class="k">return</span> <span class="k">if</span> <span class="no">Jekyll</span><span class="p">.</span><span class="nf">env</span> <span class="o">==</span> <span class="s1">'production'</span>

  <span class="n">symlinks</span> <span class="o">=</span> <span class="n">site</span><span class="p">.</span><span class="nf">config</span><span class="p">[</span><span class="s1">'dynamic_symlinks'</span><span class="p">]</span> <span class="o">||</span> <span class="p">{}</span>
  <span class="n">symlinks</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">relative_path</span><span class="p">,</span> <span class="n">link</span><span class="o">|</span>
    <span class="n">target_path</span> <span class="o">=</span> <span class="n">link</span><span class="p">[</span><span class="ss">:target</span><span class="p">]</span>
    <span class="n">link_path</span> <span class="o">=</span> <span class="n">link</span><span class="p">[</span><span class="ss">:link</span><span class="p">]</span>
    <span class="n">needs_creation</span> <span class="o">=</span> <span class="kp">false</span>

    <span class="c1"># If the link already exists but is broken, recreate it. If it's not broken</span>
    <span class="c1"># or is a file or directory, leave it. Otherwise, create the link.</span>
    <span class="k">if</span> <span class="no">File</span><span class="p">.</span><span class="nf">symlink?</span><span class="p">(</span><span class="n">link_path</span><span class="p">)</span>
      <span class="k">if</span> <span class="o">!</span><span class="no">File</span><span class="p">.</span><span class="nf">exist?</span><span class="p">(</span><span class="n">link_path</span><span class="p">)</span>
        <span class="no">File</span><span class="p">.</span><span class="nf">unlink</span><span class="p">(</span><span class="n">link_path</span><span class="p">)</span>
        <span class="n">needs_creation</span> <span class="o">=</span> <span class="kp">true</span>
      <span class="k">elsif</span> <span class="no">File</span><span class="p">.</span><span class="nf">readlink</span><span class="p">(</span><span class="n">link_path</span><span class="p">)</span> <span class="o">!=</span> <span class="n">target_path</span> <span class="c1"># Link has valid target</span>
        <span class="no">Jekyll</span><span class="p">.</span><span class="nf">logger</span><span class="p">.</span><span class="nf">warn</span> <span class="s2">"Symlink Plugin: Symlink already exists: </span><span class="si">#{</span><span class="n">link_path</span><span class="si">}</span><span class="s2">"</span>
      <span class="k">end</span>
    <span class="k">elsif</span> <span class="o">!</span><span class="no">File</span><span class="p">.</span><span class="nf">exist?</span><span class="p">(</span><span class="n">link_path</span><span class="p">)</span>
      <span class="n">needs_creation</span> <span class="o">=</span> <span class="kp">true</span>
    <span class="k">else</span>                        <span class="c1"># Link path is taken by a file or directory</span>
      <span class="no">Jekyll</span><span class="p">.</span><span class="nf">logger</span><span class="p">.</span><span class="nf">warn</span> <span class="s2">"Symlink Plugin: Symlink path is taken: </span><span class="si">#{</span><span class="n">link_path</span><span class="si">}</span><span class="s2">"</span>
    <span class="k">end</span>

    <span class="k">if</span> <span class="n">needs_creation</span>
      <span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir_p</span><span class="p">(</span><span class="no">File</span><span class="p">.</span><span class="nf">dirname</span><span class="p">(</span><span class="n">link_path</span><span class="p">))</span>
      <span class="k">begin</span>
        <span class="no">File</span><span class="p">.</span><span class="nf">symlink</span><span class="p">(</span><span class="n">target_path</span><span class="p">,</span> <span class="n">link_path</span><span class="p">)</span>
        <span class="c1"># Jekyll.logger.info "Symlink Plugin:", "Symlink created: #{link_path} -&gt; #{target_path}"</span>
      <span class="k">rescue</span> <span class="o">=&gt;</span> <span class="n">e</span>
        <span class="no">Jekyll</span><span class="p">.</span><span class="nf">logger</span><span class="p">.</span><span class="nf">warn</span> <span class="s2">"Symlink Plugin:"</span><span class="p">,</span> <span class="s2">"Failed to create symlink </span><span class="si">#{</span><span class="n">link_path</span><span class="si">}</span><span class="s2">: </span><span class="si">#{</span><span class="n">e</span><span class="p">.</span><span class="nf">message</span><span class="si">}</span><span class="s2">"</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div>    </div>
  </li>
</ol>

<h2 id="method-3-serving-the-files-locally-with-a-web-server">Method 3: Serving the files locally with a web server</h2>

<p>For example, you can serve the images locally with Python’s built-in web server:</p>

<ol>
  <li>
    <p>Serve your image folder</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/Downloads/temp/archive/image/foo/
python3 <span class="nt">-m</span> http.server 8123 <span class="nt">--bind</span> 127.0.0.1
</code></pre></div>    </div>

    <p>If you want the server to run in the background:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">nohup </span>python3 <span class="nt">-m</span> http.server 8123 <span class="nt">--bind</span> 127.0.0.1 <span class="o">&gt;</span>/tmp/imgserver.log 2&gt;&amp;1 &amp;
</code></pre></div>    </div>

    <p>To stop it later:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>lsof <span class="nt">-iTCP</span>:8123 <span class="nt">-sTCP</span>:LISTEN
<span class="nb">kill</span> &lt;PID&gt;
</code></pre></div>    </div>
  </li>
  <li>
    <p>Use that URL in your post</p>

    <pre><code class="language-example">![](http://127.0.0.1:8123/foo/20260218184820.png)
</code></pre>
  </li>
</ol>

<p>You can serve from the directory <code class="language-plaintext highlighter-rouge">~/Downloads/temp/archive/image/foo/</code> but use a
different mount path in the URL to refer to that directory, e.g.,
<a href="http://localhost:8123/igps/20260218184820.png">http://localhost:8123/igps/20260218184820.png</a>. To achieve this we can write
and run this python script:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~/Downloads/temp/archive/image/foo/
python3 server.py
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">server.py</code>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#!/usr/bin/env python3
</span><span class="kn">from</span> <span class="nn">http.server</span> <span class="kn">import</span> <span class="n">ThreadingHTTPServer</span><span class="p">,</span> <span class="n">SimpleHTTPRequestHandler</span>
<span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">sys</span>

<span class="c1"># Directory you want to expose
</span><span class="n">ROOT</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">expanduser</span><span class="p">(</span><span class="s">"~/Downloads/temp/archive/image/foo"</span><span class="p">)).</span><span class="n">resolve</span><span class="p">()</span>

<span class="n">MOUNT</span> <span class="o">=</span> <span class="s">"/igps/"</span>
<span class="n">HOST</span> <span class="o">=</span> <span class="s">"127.0.0.1"</span>
<span class="n">PORT</span> <span class="o">=</span> <span class="mi">8123</span>


<span class="k">class</span> <span class="nc">MountHandler</span><span class="p">(</span><span class="n">SimpleHTTPRequestHandler</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">translate_path</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">path</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="s">"""
        Map URLs under /igps/... to files under ROOT/...
        Reject everything else with a 404.
        """</span>
        <span class="c1"># Strip query/fragment
</span>        <span class="n">path</span> <span class="o">=</span> <span class="n">path</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">"?"</span><span class="p">,</span> <span class="mi">1</span><span class="p">)[</span><span class="mi">0</span><span class="p">].</span><span class="n">split</span><span class="p">(</span><span class="s">"#"</span><span class="p">,</span> <span class="mi">1</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>

        <span class="k">if</span> <span class="ow">not</span> <span class="n">path</span><span class="p">.</span><span class="n">startswith</span><span class="p">(</span><span class="n">MOUNT</span><span class="p">):</span>
            <span class="c1"># Send 404 for non-mounted paths
</span>            <span class="k">return</span> <span class="nb">str</span><span class="p">(</span><span class="n">ROOT</span> <span class="o">/</span> <span class="s">"__nonexistent__"</span><span class="p">)</span>

        <span class="n">rel</span> <span class="o">=</span> <span class="n">path</span><span class="p">[</span><span class="nb">len</span><span class="p">(</span><span class="n">MOUNT</span><span class="p">):]</span>  <span class="c1"># e.g. "20260218184820.png"
</span>        <span class="c1"># Normalize and prevent path traversal
</span>        <span class="n">rel_path</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">rel</span><span class="p">)</span>
        <span class="n">full</span> <span class="o">=</span> <span class="p">(</span><span class="n">ROOT</span> <span class="o">/</span> <span class="n">rel_path</span><span class="p">).</span><span class="n">resolve</span><span class="p">()</span>

        <span class="k">if</span> <span class="n">ROOT</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">full</span><span class="p">.</span><span class="n">parents</span> <span class="ow">and</span> <span class="n">full</span> <span class="o">!=</span> <span class="n">ROOT</span><span class="p">:</span>
            <span class="k">return</span> <span class="nb">str</span><span class="p">(</span><span class="n">ROOT</span> <span class="o">/</span> <span class="s">"__nonexistent__"</span><span class="p">)</span>

        <span class="k">return</span> <span class="nb">str</span><span class="p">(</span><span class="n">full</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">log_message</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">fmt</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">):</span>
        <span class="c1"># optional: quieter logs
</span>        <span class="n">sys</span><span class="p">.</span><span class="n">stderr</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="s">"%s - - [%s] %s</span><span class="se">\n</span><span class="s">"</span> <span class="o">%</span> <span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">client_address</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
                                              <span class="bp">self</span><span class="p">.</span><span class="n">log_date_time_string</span><span class="p">(),</span>
                                              <span class="n">fmt</span> <span class="o">%</span> <span class="n">args</span><span class="p">))</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">ROOT</span><span class="p">.</span><span class="n">exists</span><span class="p">():</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"ERROR: directory does not exist: </span><span class="si">{</span><span class="n">ROOT</span><span class="si">}</span><span class="s">"</span><span class="p">,</span> <span class="nb">file</span><span class="o">=</span><span class="n">sys</span><span class="p">.</span><span class="n">stderr</span><span class="p">)</span>
        <span class="n">sys</span><span class="p">.</span><span class="nb">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>

    <span class="n">httpd</span> <span class="o">=</span> <span class="n">ThreadingHTTPServer</span><span class="p">((</span><span class="n">HOST</span><span class="p">,</span> <span class="n">PORT</span><span class="p">),</span> <span class="n">MountHandler</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Serving </span><span class="si">{</span><span class="n">ROOT</span><span class="si">}</span><span class="s"> at http://</span><span class="si">{</span><span class="n">HOST</span><span class="si">}</span><span class="s">:</span><span class="si">{</span><span class="n">PORT</span><span class="si">}{</span><span class="n">MOUNT</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="n">httpd</span><span class="p">.</span><span class="n">serve_forever</span><span class="p">()</span>
</code></pre></div></div>]]></content><author><name>Joseph Huang</name></author><category term="Web" /><category term="Minimal Mistakes" /><summary type="html"><![CDATA[I want to be able to load local images when the site is served locally. However, the URL format file:///Users/&lt;username&gt;/archive/image/20260218184820.png doesn’t work.]]></summary></entry><entry><title type="html">Executing SQL Source Code Blocks in Org Mode</title><link href="https://josephtesfaye.com/blog/emacs/org-sqls/" rel="alternate" type="text/html" title="Executing SQL Source Code Blocks in Org Mode" /><published>2026-02-17T00:00:00+00:00</published><updated>2026-02-17T00:00:00+00:00</updated><id>https://josephtesfaye.com/blog/emacs/org-sqls</id><content type="html" xml:base="https://josephtesfaye.com/blog/emacs/org-sqls/"><![CDATA[<p>You can execute SQL source code blocks in Org Mode.</p>

<h2 id="using-built-in-package-ob-sql">Using built-in package <code class="language-plaintext highlighter-rouge">ob-sql</code></h2>

<p>Emacs has a built-in way to run SQL source code blocks in Org Mode through the
packages <a href="p:emacs/lisp/progmodes/sql.el">sql</a> and
<a href="p:emacs/lisp/org/ob-sql.el">ob-sql</a>. In order to evaluate an SQL source code
block you must have a properly installed RDBMS. Org mode supports mysql,
postgresql, oracle, etc.</p>

<p>You’ll need to activate SQL source code blocks in your init file.</p>

<div class="language-elisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nv">org-babel-do-load-languages</span>
 <span class="ss">'org-babel-load-languages</span>
 <span class="o">'</span><span class="p">((</span><span class="nv">sql</span> <span class="o">.</span> <span class="no">t</span><span class="p">)))</span>
</code></pre></div></div>

<p>Examples:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Put the header arguments on the same line:

#+begin_src sql :engine mysql :dbhost localhost :dbport 3306 :dbuser root :dbpassword "123456" :database nocobase
SELECT * FROM mytable WHERE id &gt; 500
#+end_src

Put the header arguments on separate lines:

#+name: my-query
#+header: :engine mysql
#+header: :dbhost localhost
#+header: :dbuser root
#+header: :dbpassword "123456"
#+header: :database nocobase
#+begin_src sql
DESC users;
#+end_src
</code></pre></div></div>

<p>See <a href="https://orgmode.org/worg/org-contrib/babel/languages/ob-doc-sql.html">SQL Source Code Blocks in Org
Mode</a> for
more.</p>

<h2 id="using-the-package-ob-sql-mode">Using the package <code class="language-plaintext highlighter-rouge">ob-sql-mode</code></h2>

<p>The built-in package <code class="language-plaintext highlighter-rouge">ob-sql</code> currently doesn’t support sessions, that is, it
makes a connection (session) every time you run the block, which is somewhat
slower than using an existing session. A better approach is to use the package
<a href="https://github.com/nikclayton/ob-sql-mode">ob-sql-mode</a>, which supports
sessions.</p>

<p>You can evaluate SQLs in Org mode directly via the package <code class="language-plaintext highlighter-rouge">ob-sql-mode</code>, which
is an Org-Babel support for evaluating SQL using sql-mode.</p>

<p>Install the package:</p>

<div class="language-elisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nv">setq-default</span> <span class="nv">dotspacemacs-additional-packages</span> <span class="o">'</span><span class="p">(</span><span class="nv">ob-sql-mode</span><span class="p">))</span>
<span class="p">(</span><span class="nb">use-package</span> <span class="nv">ob-sql-mode</span> <span class="ss">:after</span> <span class="p">(</span><span class="nv">org</span><span class="p">))</span>
</code></pre></div></div>

<p>Examples:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>* Header 1
Now you can run the following code block with ~org-ctrl-c-ctrl-c~ (=C-c C-c=):

#+begin_src sql-mode :product mysql :session local
DESC users;
#+end_src

** Header 2
:PROPERTIES:
:header-args:sql-mode: :product mysql
:header-args:sql-mode+: :session local
:END:

You can put the header arguments in a drawer.

#+begin_src sql-mode
DESC users;
#+end_src
</code></pre></div></div>

<p>See <code class="language-plaintext highlighter-rouge">(describe-package 'ob-sql-mode)</code> for more details on setup.</p>

<h2 id="related">Related</h2>

<ul>
  <li><a href="/blog/emacs/spacemacs-sqls/">Connecting Databases and executing SQLs in Spacemacs</a></li>
</ul>]]></content><author><name>Joseph Huang</name></author><category term="Emacs" /><category term="Org Mode" /><category term="Database" /><summary type="html"><![CDATA[You can execute SQL source code blocks in Org Mode.]]></summary></entry><entry><title type="html">Connecting Databases and executing SQLs in Spacemacs</title><link href="https://josephtesfaye.com/blog/emacs/spacemacs-sqls/" rel="alternate" type="text/html" title="Connecting Databases and executing SQLs in Spacemacs" /><published>2026-02-16T00:00:00+00:00</published><updated>2026-02-16T00:00:00+00:00</updated><id>https://josephtesfaye.com/blog/emacs/spacemacs-sqls</id><content type="html" xml:base="https://josephtesfaye.com/blog/emacs/spacemacs-sqls/"><![CDATA[<p>Here’s how you can connect to databases and execute SQLs in Spacemacs (Emacs).</p>

<h2 id="configuration">Configuration</h2>

<p>Prerequisites:</p>

<ul>
  <li>Ruby</li>
  <li>Go</li>
</ul>

<p>Make sure these are already installed on your system.</p>

<p>Configuration:</p>

<ol>
  <li>
    <p>Enable <code class="language-plaintext highlighter-rouge">sql</code> layer</p>

    <p>Add <code class="language-plaintext highlighter-rouge">sql</code> to the existing <code class="language-plaintext highlighter-rouge">dotspacemacs-configuration-layers</code> list in your
init file.</p>
  </li>
  <li>
    <p>Install external dependencies</p>

    <ol>
      <li>
        <p>Syntax Checking: Install the <code class="language-plaintext highlighter-rouge">sqlint</code> gem.</p>

        <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gem <span class="nb">install </span>sqlint
</code></pre></div>        </div>
      </li>
      <li>
        <p>Formatting: Install <code class="language-plaintext highlighter-rouge">sqlfmt</code> and move it to your <code class="language-plaintext highlighter-rouge">$PATH</code>.</p>

        <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew <span class="nb">install </span>sqlfmt
sqlfmt <span class="nt">-v</span>
</code></pre></div>        </div>
      </li>
      <li>
        <p>LSP supporting: Use <code class="language-plaintext highlighter-rouge">sqls</code> (Go implementation)</p>

        <ol>
          <li>
            <p>Install <code class="language-plaintext highlighter-rouge">sqls</code></p>

            <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>go <span class="nb">install </span>github.com/sqls-server/sqls@latest
</code></pre></div>            </div>

            <p>This installs <code class="language-plaintext highlighter-rouge">sqls</code> under <code class="language-plaintext highlighter-rouge">go env GOPATH</code> (e.g., <code class="language-plaintext highlighter-rouge">~/go/bin</code>). Check:</p>

            <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~/go/bin/sqls <span class="nt">--version</span>
</code></pre></div>            </div>

            <p>Ensure Emacs can find the bin by either adding it to <code class="language-plaintext highlighter-rouge">$PATH</code>:</p>

            <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">PATH</span><span class="o">=</span><span class="nv">$PATH</span>:<span class="nv">$HOME</span>/go/bin
</code></pre></div>            </div>

            <p>Or set the variable <code class="language-plaintext highlighter-rouge">lsp-sqls-server</code> to its absolute path:</p>

            <div class="language-elisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nv">with-eval-after-load</span> <span class="ss">'lsp-sqls</span> <span class="p">(</span><span class="k">setq</span> <span class="nv">lsp-sqls-server</span> <span class="s">"~/go/bin/sqls"</span><span class="p">))</span>
</code></pre></div>            </div>
          </li>
          <li>
            <p>Set the variable <code class="language-plaintext highlighter-rouge">sql-backend</code> to <code class="language-plaintext highlighter-rouge">'lsp</code>, and the variable
<code class="language-plaintext highlighter-rouge">sql-lsp-sqls-workspace-config-path</code> to <code class="language-plaintext highlighter-rouge">'workspace</code> or <code class="language-plaintext highlighter-rouge">'root</code>.</p>

            <div class="language-elisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nv">setq-default</span>
 <span class="nv">dotspacemacs-configuration-layers</span>
 <span class="o">'</span><span class="p">((</span><span class="nv">sql</span> <span class="ss">:variables</span>
        <span class="nv">sql-backend</span> <span class="ss">'lsp</span>
        <span class="nv">sql-lsp-sqls-workspace-config-path</span> <span class="ss">'workspace</span><span class="p">)))</span>
</code></pre></div>            </div>

            <p>The difference between <code class="language-plaintext highlighter-rouge">'workspace</code> and <code class="language-plaintext highlighter-rouge">'root</code> depends on whether you
are working in a simple project or a multi-module project (monorepo).
For a simple project, they are usually the same. The distinction matters
when your project has nested folders.</p>

            <ul>
              <li>
                <p><code class="language-plaintext highlighter-rouge">'root</code> (Project Root):</p>

                <ul>
                  <li>
                    <p>This is the top-level directory of your entire project, usually
determined by the version control system (e.g., where the <code class="language-plaintext highlighter-rouge">.git/</code>
folder lives).</p>
                  </li>
                  <li>
                    <p>Use Case: You want one single configuration file to apply to the
entire repository, regardless of which sub-folder you are working
in.</p>
                  </li>
                </ul>
              </li>
              <li>
                <p><code class="language-plaintext highlighter-rouge">'workspace</code> (LSP Workspace Folder):</p>

                <ul>
                  <li>
                    <p>This is the specific directory that lsp-mode considers the “root”
for the current server instance. In a monorepo, this is often a
subdirectory of the Git root.</p>
                  </li>
                  <li>
                    <p>Use Case: You have a monorepo with multiple distinct services (e.g.,
<code class="language-plaintext highlighter-rouge">backend/</code> and <code class="language-plaintext highlighter-rouge">frontend/</code>), and you want a different SQL
configuration for each one.</p>
                  </li>
                </ul>
              </li>
            </ul>
          </li>
          <li>
            <p>Put <code class="language-plaintext highlighter-rouge">.sqls/config.json</code> files under those directories, and <code class="language-plaintext highlighter-rouge">lsp-sqls</code>
will automatically connect DB when a <code class="language-plaintext highlighter-rouge">.sqls</code> file is first opened.</p>

            <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"sqls"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"connections"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
            </span><span class="p">{</span><span class="w">
                </span><span class="nl">"driver"</span><span class="p">:</span><span class="w"> </span><span class="s2">"mysql"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"dataSourceName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"user1:password1@tcp(localhost:3306)/sample_db"</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="p">{</span><span class="w">
                </span><span class="nl">"driver"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sqlite3"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"dataSourceName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/path/to/file.db"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"alias"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sqlite-local"</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">]</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>            </div>

            <p>To add multiple connections of the same driver (e.g., two different
SQLite databases or two PostgreSQL environments), you simply add
another object to the connections array in your configuration file.
Each connection entry is independent, even if they share the same
“driver”. You can use the “alias” field to give them unique names.</p>

            <p>By default, <code class="language-plaintext highlighter-rouge">sqls</code> automatically selects the first connection listed
in your connections array in <code class="language-plaintext highlighter-rouge">.sqls/config.json</code>. If you open a
<code class="language-plaintext highlighter-rouge">.sql</code> file and do not manually switch connections, the LSP server
will attempt to provide completions and run queries against that
top-most entry. There is no explicit <code class="language-plaintext highlighter-rouge">"default": true</code> property in
the <code class="language-plaintext highlighter-rouge">sqls</code> configuration schema. To change the default connection,
you simply reorder the list in your configuration file.</p>
          </li>
          <li>
            <p>Another way to connect DB is through the variable <code class="language-plaintext highlighter-rouge">lsp-sqls-connections</code>.</p>

            <div class="language-elisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">setq</span> <span class="nv">lsp-sqls-connections</span>
      <span class="o">'</span><span class="p">(((</span><span class="nv">driver</span> <span class="o">.</span> <span class="s">"mysql"</span><span class="p">)</span>
         <span class="p">(</span><span class="nv">dataSourceName</span> <span class="o">.</span> <span class="s">"&lt;user&gt;:&lt;password&gt;@tcp(localhost:3306)/&lt;database&gt;"</span><span class="p">))</span>
        <span class="p">((</span><span class="nv">driver</span> <span class="o">.</span> <span class="s">"mssql"</span><span class="p">)</span>
         <span class="p">(</span><span class="nv">dataSourceName</span> <span class="o">.</span> <span class="s">"Server=localhost;Database=sammy;User Id=yyoncho;Password=hunter2;"</span><span class="p">))</span>
        <span class="p">((</span><span class="nv">driver</span> <span class="o">.</span> <span class="s">"postgresql"</span><span class="p">)</span>
         <span class="p">(</span><span class="nv">dataSourceName</span> <span class="o">.</span> <span class="s">"host=127.0.0.1 port=5432 user=yyoncho password=123456 dbname=sammy sslmode=disable"</span><span class="p">))))</span>
</code></pre></div>            </div>
          </li>
        </ol>
      </li>
    </ol>
  </li>
</ol>

<h3 id="notes-on-the-difference-between-sqls-and-sql-ls">Notes on the difference between <code class="language-plaintext highlighter-rouge">sqls</code> and <code class="language-plaintext highlighter-rouge">sql-ls</code></h3>

<p>I want to enable and use the <code class="language-plaintext highlighter-rouge">sql</code> layer in Spacemacs. I’ve installed
<a href="https://github.com/sqls-server/sqls">sqls</a> as per the
<a href="https://github.com/syl20bnr/spacemacs/blob/develop/layers/%2Blang/sql/README.org">doc</a>.
But when I opened a <code class="language-plaintext highlighter-rouge">.sql</code> file it’s the language server
<a href="https://github.com/joe-re/sql-language-server">sql-language-server</a> (<code class="language-plaintext highlighter-rouge">sql-ls</code>)
that is installed and started automatically.</p>

<p>Under the <span class="spurious-link" target="~/projects/spacemacs/elpa/29.1/develop/lsp-mode-20250708.39"><em>lsp-mode
installation directory</em></span> I find two files related to SQL support:
<code class="language-plaintext highlighter-rouge">lsp-sql.el</code> and <code class="language-plaintext highlighter-rouge">lsp-sqls.el</code>.</p>

<p>It seems the first one is an implementation using the language server <code class="language-plaintext highlighter-rouge">sql-ls</code>
(written in TypeScript) and the second is another implementation using the
language server <code class="language-plaintext highlighter-rouge">sqls</code> (written in Go).</p>

<p>How do you use the Go version only? — You can disable <code class="language-plaintext highlighter-rouge">sql-ls</code> with:</p>

<div class="language-elisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nv">add-to-list</span> <span class="ss">'lsp-disabled-clients</span> <span class="ss">'sql-ls</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="connecting-db-and-executing-sqls">Connecting DB and Executing SQLs</h2>

<p>You can connect to a database by opening an interactive REPL buffer (SQLi) using
any of these commands:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">sql-show-sqli-buffer</code> (<code class="language-plaintext highlighter-rouge">C-c C-z</code>, <code class="language-plaintext highlighter-rouge">M-m m b b</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">sql-product-interactive</code> (<code class="language-plaintext highlighter-rouge">C-c TAB</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">spacemacs/sql-start</code> (<code class="language-plaintext highlighter-rouge">M-m m '</code>, <code class="language-plaintext highlighter-rouge">M-m m s i</code>)</li>
</ul>

<p>They ask for the connection parameters from user input and create a new session
if there isn’t an existing SQLi session.</p>

<p>Another method is using <code class="language-plaintext highlighter-rouge">sql-connect</code> and <code class="language-plaintext highlighter-rouge">sql-connection-alist</code>:</p>

<ol>
  <li>
    <p>Set an alist of connection parameters in variable <code class="language-plaintext highlighter-rouge">sql-connection-alist</code>.</p>

    <div class="language-elisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">setq</span> <span class="nv">sql-connection-alist</span>
      <span class="o">'</span><span class="p">((</span><span class="s">"mysql-local"</span>                  <span class="c1">; Used in the SQLi buffer name</span>
         <span class="p">(</span><span class="nv">sql-product</span> <span class="ss">'mysql</span><span class="p">)</span>           <span class="c1">; See sql-add-product</span>
         <span class="p">(</span><span class="nv">sql-server</span> <span class="s">"localhost"</span><span class="p">)</span>
         <span class="p">(</span><span class="nv">sql-port</span> <span class="mi">3306</span><span class="p">)</span>
         <span class="p">(</span><span class="nv">sql-user</span> <span class="s">"root"</span><span class="p">)</span>
         <span class="p">(</span><span class="nv">sql-password</span> <span class="s">"123456"</span><span class="p">)</span>
         <span class="p">(</span><span class="nv">sql-database</span> <span class="s">"mybase"</span><span class="p">))))</span>
</code></pre></div>    </div>

    <p>Connections defined here appear in the submenu SQL-&gt;Start… for making new
SQLi sessions.</p>
  </li>
  <li>
    <p>Start a SQLi session by calling <code class="language-plaintext highlighter-rouge">sql-connect</code> (<code class="language-plaintext highlighter-rouge">M-m m b c</code>), which asks for
the above configured connection name.</p>

    <div class="language-elisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nv">sql-connect</span> <span class="s">"mysql-local"</span><span class="p">)</span>
</code></pre></div>    </div>
  </li>
</ol>

<p>Once a connection is created you can execute SQLs in the SQLi buffer directly.
You can also send the SQLs from elsewhere to the SQLi buffer. For example, from
a <code class="language-plaintext highlighter-rouge">sql-mode</code> buffer you can send SQLs in the current line or region with
<code class="language-plaintext highlighter-rouge">sql-send-line-and-next</code> (<code class="language-plaintext highlighter-rouge">C-c C-n</code>) or <code class="language-plaintext highlighter-rouge">sql-send-region</code> (<code class="language-plaintext highlighter-rouge">C-c C-r</code>) among
others.</p>

<p>The SQLs are sent to the last created SQLi buffer by default. You can change the
associated SQLi buffer for a <code class="language-plaintext highlighter-rouge">sql-mode</code> buffer or the globally default SQLi
buffer using <code class="language-plaintext highlighter-rouge">sql-set-sqli-buffer</code>.</p>

<h2 id="commands-in-sql-mode">Commands in <code class="language-plaintext highlighter-rouge">sql-mode</code></h2>

<ul>
  <li>
    <p>Change SQL dialect for the current buffer: <code class="language-plaintext highlighter-rouge">spacemacs/sql-highlight</code>
(<code class="language-plaintext highlighter-rouge">M-m m h k</code>)</p>
  </li>
  <li>
    <p>Capitalize keywords in region: <code class="language-plaintext highlighter-rouge">sqlup-capitalize-keywords-in-region</code>
(<code class="language-plaintext highlighter-rouge">M-m m c</code>)</p>
  </li>
  <li>
    <p>Format codes</p>

    <ul>
      <li>in buffer: <code class="language-plaintext highlighter-rouge">sqlfmt-buffer</code> (<code class="language-plaintext highlighter-rouge">M-m m = =</code>)</li>
      <li>in region: <code class="language-plaintext highlighter-rouge">lsp-format-region</code> (<code class="language-plaintext highlighter-rouge">M-m m = r</code>)</li>
    </ul>
  </li>
  <li>Connections
    <ul>
      <li>LSP (sqls)
        <ul>
          <li>Show all connection: <code class="language-plaintext highlighter-rouge">lsp-sql-show-connections</code></li>
          <li>Switch to another connection: <code class="language-plaintext highlighter-rouge">lsp-sql-switch-connection</code></li>
        </ul>
      </li>
      <li>Connect DB in a SQLi buffer
        <ul>
          <li><code class="language-plaintext highlighter-rouge">sql-show-sqli-buffer</code> (<code class="language-plaintext highlighter-rouge">C-c C-z</code>, <code class="language-plaintext highlighter-rouge">M-m m b b</code>)</li>
          <li><code class="language-plaintext highlighter-rouge">sql-product-interactive</code> (<code class="language-plaintext highlighter-rouge">C-c TAB</code>)</li>
          <li><code class="language-plaintext highlighter-rouge">spacemacs/sql-start</code> (M-m m ‘, M-m m s i)</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>
    <p>Databases:</p>

    <ul>
      <li>Show all: <code class="language-plaintext highlighter-rouge">lsp-sql-show-databases</code></li>
      <li>Switch: <code class="language-plaintext highlighter-rouge">lsp-sql-switch-database</code></li>
    </ul>
  </li>
  <li>
    <p>Tables</p>

    <ul>
      <li>Show all tables in the current database:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">sql-list-all</code> (<code class="language-plaintext highlighter-rouge">C-c C-l a</code>, <code class="language-plaintext highlighter-rouge">M-m m l a</code>)</li>
          <li><code class="language-plaintext highlighter-rouge">lsp-sql-show-tables</code>,  <code class="language-plaintext highlighter-rouge">lsp-execute-code-action</code> (<code class="language-plaintext highlighter-rouge">M-m m a a</code>)</li>
        </ul>
      </li>
      <li>Show all fields in a table:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">sql-list-table</code> (<code class="language-plaintext highlighter-rouge">C-c C-l t</code>, <code class="language-plaintext highlighter-rouge">M-m m l t</code>)</li>
          <li><code class="language-plaintext highlighter-rouge">lsp-describe-thing-at-point</code> (<code class="language-plaintext highlighter-rouge">C-.</code>)</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>
    <p>Switch associated SQLi buffer to another: <code class="language-plaintext highlighter-rouge">sql-set-sqli-buffer</code>,
<code class="language-plaintext highlighter-rouge">sql-set-sqli-buffer-generally</code></p>
  </li>
  <li>
    <p>Execute SQLs in the current:</p>

    <ul>
      <li>line:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">sql-send-line-and-next</code> (<code class="language-plaintext highlighter-rouge">C-c C-n</code>)</li>
          <li><code class="language-plaintext highlighter-rouge">spacemacs/sql-send-line-and-next</code> (<code class="language-plaintext highlighter-rouge">M-m m s l</code>)</li>
          <li><code class="language-plaintext highlighter-rouge">spacemacs/sql-send-line-and-next-and-focus</code> (<code class="language-plaintext highlighter-rouge">M-m m s L</code>)</li>
        </ul>
      </li>
      <li>region:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">sql-send-region</code> (<code class="language-plaintext highlighter-rouge">C-c C-r</code>)</li>
          <li><code class="language-plaintext highlighter-rouge">spacemacs/sql-send-region</code> (<code class="language-plaintext highlighter-rouge">M-m m s r</code>)</li>
          <li><code class="language-plaintext highlighter-rouge">spacemacs/sql-send-region-and-focus</code> (<code class="language-plaintext highlighter-rouge">M-m m s R</code>)</li>
          <li><code class="language-plaintext highlighter-rouge">lsp-sql-execute-query</code></li>
        </ul>
      </li>
      <li>paragraph:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">sql-send-paragraph</code> (<code class="language-plaintext highlighter-rouge">C-c C-c</code>)</li>
          <li><code class="language-plaintext highlighter-rouge">spacemacs/sql-send-paragraph</code> (<code class="language-plaintext highlighter-rouge">M-m m s f</code>)</li>
          <li><code class="language-plaintext highlighter-rouge">spacemacs/sql-send-paragraph-and-focus</code> (<code class="language-plaintext highlighter-rouge">M-m m s F</code>)</li>
          <li><code class="language-plaintext highlighter-rouge">lsp-sql-execute-paragraph</code></li>
        </ul>
      </li>
      <li>buffer:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">sql-send-buffer</code> (<code class="language-plaintext highlighter-rouge">C-c C-b</code>)</li>
          <li><code class="language-plaintext highlighter-rouge">spacemacs/sql-send-buffer</code> (<code class="language-plaintext highlighter-rouge">M-m m s b</code>)</li>
          <li><code class="language-plaintext highlighter-rouge">spacemacs/sql-send-buffer-and-focus</code> (<code class="language-plaintext highlighter-rouge">M-m m s B</code>)</li>
          <li><code class="language-plaintext highlighter-rouge">lsp-sql-execute-query</code></li>
        </ul>
      </li>
      <li>minibuffer:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">sql-send-string</code> (<code class="language-plaintext highlighter-rouge">C-c C-s</code>)</li>
          <li><code class="language-plaintext highlighter-rouge">spacemacs/sql-send-string</code> (<code class="language-plaintext highlighter-rouge">M-m m s q</code>)</li>
          <li><code class="language-plaintext highlighter-rouge">spacemacs/sql-send-string-and-focus</code> (<code class="language-plaintext highlighter-rouge">M-m m s Q</code>)</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="commands-in-sql-interactive-mode">Commands in <code class="language-plaintext highlighter-rouge">sql-interactive-mode</code></h2>

<ul>
  <li>Rename buffer
    <ul>
      <li>by appending a number: <code class="language-plaintext highlighter-rouge">sql-rename-buffer</code> (<code class="language-plaintext highlighter-rouge">M-m m b r</code>)</li>
      <li>to another name: Pass a <code class="language-plaintext highlighter-rouge">universal-argument</code> to the above</li>
    </ul>
  </li>
  <li>Save the connection information of the current session to
<code class="language-plaintext highlighter-rouge">sql-connection-alist</code> if it wasn’t started with a connection name:
<code class="language-plaintext highlighter-rouge">sql-save-connection</code> (<code class="language-plaintext highlighter-rouge">M-m m b S</code>)</li>
</ul>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://emacs-lsp.github.io/lsp-mode/page/lsp-sqls/">https://emacs-lsp.github.io/lsp-mode/page/lsp-sqls/</a></li>
  <li><a href="https://www.spacemacs.org/layers/+lang/sql/README.html">https://www.spacemacs.org/layers/+lang/sql/README.html</a></li>
  <li><a href="https://github.com/sqls-server/sqls">https://github.com/sqls-server/sqls</a></li>
  <li>help:sql-help</li>
</ul>

<h2 id="related">Related</h2>

<ul>
  <li><a href="/blog/emacs/org-sqls/">Executing SQL Source Code Blocks in Org Mode</a></li>
</ul>]]></content><author><name>Joseph Huang</name></author><category term="Emacs" /><category term="Database" /><summary type="html"><![CDATA[Here’s how you can connect to databases and execute SQLs in Spacemacs (Emacs).]]></summary></entry><entry><title type="html">Differences between the commands `gem` and `bundle`</title><link href="https://josephtesfaye.com/blog/web/gem-bundle/" rel="alternate" type="text/html" title="Differences between the commands `gem` and `bundle`" /><published>2026-02-15T00:00:00+00:00</published><updated>2026-02-15T00:00:00+00:00</updated><id>https://josephtesfaye.com/blog/web/gem-bundle</id><content type="html" xml:base="https://josephtesfaye.com/blog/web/gem-bundle/"><![CDATA[<p>In the Ruby ecosystem, <code class="language-plaintext highlighter-rouge">gem</code> and <code class="language-plaintext highlighter-rouge">bundle</code> serve distinct but complementary roles
in managing code libraries (known as <strong>gems</strong>). While <code class="language-plaintext highlighter-rouge">gem</code> is the primary
interface for the <strong>RubyGems</strong> package manager, <code class="language-plaintext highlighter-rouge">bundle</code> is the command used for
<strong>Bundler</strong>, a tool built on top of RubyGems to manage project-specific
dependencies.</p>

<h2 id="core-differences-at-a-glance">Core Differences at a Glance</h2>

<table>
  <thead>
    <tr>
      <th>Feature</th>
      <th><code class="language-plaintext highlighter-rouge">gem</code> (RubyGems)</th>
      <th><code class="language-plaintext highlighter-rouge">bundle</code> (Bundler)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Primary Goal</td>
      <td>General package management (install/uninstall/search).</td>
      <td>Project-specific dependency management.</td>
    </tr>
    <tr>
      <td>Scope</td>
      <td>System-wide: Installs gems for the entire Ruby environment.</td>
      <td>Project-local: Manages exact versions needed for a specific app.</td>
    </tr>
    <tr>
      <td>Config File</td>
      <td>None (uses direct command arguments).</td>
      <td>Uses a Gemfile to list and lock dependencies.</td>
    </tr>
    <tr>
      <td>Installation</td>
      <td>Built into Ruby (v1.9+).</td>
      <td>Must be installed as a gem itself (via <code class="language-plaintext highlighter-rouge">gem install bundler</code>).</td>
    </tr>
  </tbody>
</table>

<h2 id="the-gem-command-the-package-manager">The <code class="language-plaintext highlighter-rouge">gem</code> Command: The Package Manager</h2>

<p>The <code class="language-plaintext highlighter-rouge">gem</code> command interacts with the global Ruby environment. It is used for
broad administrative tasks rather than managing a specific application's needs.</p>

<ul>
  <li>
    <p>Key Functions: Used to search, install, list, and uninstall libraries from
<a href="https://rubygems.org">https://rubygems.org</a>.</p>
  </li>
  <li>
    <p>Common Commands:</p>

    <ul>
      <li><code class="language-plaintext highlighter-rouge">gem install &lt;gem_name&gt;</code>: Downloads and installs a library to your system.</li>
      <li><code class="language-plaintext highlighter-rouge">gem list</code>: Shows all gems currently installed in your global Ruby
environment.</li>
      <li><code class="language-plaintext highlighter-rouge">gem search &lt;query&gt;</code>: Finds available libraries on remote servers.</li>
    </ul>
  </li>
  <li>
    <p>Limitation: It does not track which versions of a gem a specific project
needs, often leading to "dependency hell" if different projects require
conflicting versions of the same gem.</p>
  </li>
</ul>

<h2 id="the-bundle-command-the-dependency-manager">The <code class="language-plaintext highlighter-rouge">bundle</code> Command: The Dependency Manager</h2>

<p>Bundler solves the version conflict problem by creating an isolated environment
for each project. It ensures that every developer on a team uses the exact same
gem versions.</p>

<ul>
  <li>
    <p>How It Works: It reads a <code class="language-plaintext highlighter-rouge">Gemfile</code>, resolves all dependencies, and creates a
<code class="language-plaintext highlighter-rouge">Gemfile.lock</code> to record the specific versions used.</p>
  </li>
  <li>
    <p>Key Functions:</p>

    <ul>
      <li><code class="language-plaintext highlighter-rouge">bundle install</code>: Reads the <code class="language-plaintext highlighter-rouge">Gemfile</code>, resolves dependencies, and installs
missing gems.</li>
      <li><code class="language-plaintext highlighter-rouge">bundle exec &lt;command&gt;</code>: Runs a script (like <code class="language-plaintext highlighter-rouge">jekyll</code> or <code class="language-plaintext highlighter-rouge">rake</code>)
specifically using the gem versions defined in the project's lock file.</li>
      <li><code class="language-plaintext highlighter-rouge">bundle update</code>: Updates gems to the latest allowed versions and updates the
lock file.</li>
    </ul>
  </li>
  <li>
    <p>Consistency: It guarantees that the development, staging, and production
environments are identical, preventing "it works on my machine" bugs.</p>
  </li>
</ul>

<h2 id="when-to-use-which">When to Use Which?</h2>

<ul>
  <li>Use <code class="language-plaintext highlighter-rouge">gem</code> when you want to install a general-purpose tool on your computer,
like a Ruby version manager or Bundler itself.</li>
  <li>Use <code class="language-plaintext highlighter-rouge">bundle</code> for almost everything related to your actual application code,
such as adding new libraries to your project or running project-specific
tasks.</li>
</ul>]]></content><author><name>Joseph Huang</name></author><category term="Web" /><category term="Ruby" /><summary type="html"><![CDATA[In the Ruby ecosystem, gem and bundle serve distinct but complementary roles in managing code libraries (known as gems). While gem is the primary interface for the RubyGems package manager, bundle is the command used for Bundler, a tool built on top of RubyGems to manage project-specific dependencies.]]></summary></entry></feed>