<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sanghun Kang</title>
    <description>The latest articles on DEV Community by Sanghun Kang (@sanghunkang).</description>
    <link>https://dev.to/sanghunkang</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4144229%2Fe34183f6-86d3-4a94-aa15-7b4860cdb4e8.png</url>
      <title>DEV Community: Sanghun Kang</title>
      <link>https://dev.to/sanghunkang</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sanghunkang"/>
    <language>en</language>
    <item>
      <title>Multilingual Ghost CMS Blog: 9. Language Switching on Author and Post Pages</title>
      <dc:creator>Sanghun Kang</dc:creator>
      <pubDate>Wed, 07 Oct 2026 02:30:37 +0000</pubDate>
      <link>https://dev.to/sanghunkang/multilingual-ghost-cms-blog-9-language-switching-on-author-and-post-pages-1i7h</link>
      <guid>https://dev.to/sanghunkang/multilingual-ghost-cms-blog-9-language-switching-on-author-and-post-pages-1i7h</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.magicpages.co%2Fsang.mymagic.page%2F2024%2F04%2Fghost-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.magicpages.co%2Fsang.mymagic.page%2F2024%2F04%2Fghost-1.png" alt="Multilingual Ghost CMS Blog: 9. Language Switching on Author and Post Pages" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There were still two issues left in the existing theme.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;On Tag pages, only posts in the selected language are shown, but on Author pages, both English and Korean posts appear together.&lt;/li&gt;
&lt;li&gt;Even if a translated post exists, changing the language in the Language selector takes you not to that post, but to the main page of the selected language.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In 0.0.9, I updated the Author page to work the same way as the existing Tag page, and I changed &lt;code&gt;#multi&lt;/code&gt; posts so they go directly to the translated post.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fsanghunkang.com%2Fcontent%2Fimages%2F2026%2F08%2F0.0.9-author-before-0.0.8.png" alt="In 0.0.8, the Author page shows both English and Korean posts together" width="800" height="514"&gt;&lt;p&gt;This is the &lt;code&gt;/author/sanghun/&lt;/code&gt; screen in theme 0.0.8. The selector is set to English, but English and Korean posts still appear together in one list.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/sanghunka/ghost-multilingual-theme/commit/60adab1?ref=sanghunkang.com" rel="noopener noreferrer"&gt;0.0.9 · Author filtering and post language switching&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="adding-multilingual-handling-to-the-author-page"&gt;Adding multilingual handling to the Author page&lt;/h2&gt;
&lt;p&gt;The current Author page uses this single URL.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/author/sanghun/
&lt;span class="line-numbers-rows"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When Ghost renders this page, it fetches all of the author's posts without separating them by language. Because the language stored in the browser's &lt;code&gt;localStorage&lt;/code&gt; cannot be known on the server, the Handlebars template also cannot prefetch English and Korean posts separately.&lt;/p&gt;
&lt;p&gt;I solved this issue the same way as the existing Tag page.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;On the server, all posts are rendered.&lt;/li&gt;
&lt;li&gt;Each post card includes all tags for that post.&lt;/li&gt;
&lt;li&gt;In the browser, the currently selected language is checked.&lt;/li&gt;
&lt;li&gt;Only cards with matching &lt;code&gt;#en&lt;/code&gt; or &lt;code&gt;#ko&lt;/code&gt; are shown.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="authorhbs"&gt;author.hbs&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;data-tags&lt;/code&gt; attribute of each post card includes both public tags and internal tags.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class="token handlebars language-handlebars"&gt;&lt;span&gt;{{&lt;/span&gt;&lt;span&gt;#foreach&lt;/span&gt; &lt;span&gt;posts&lt;/span&gt;&lt;span&gt;}}&lt;/span&gt;&lt;/span&gt;
    &lt;span class="token tag"&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;post-card&lt;span&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span&gt;data-tags&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;'&lt;/span&gt;[&lt;span&gt;&lt;span&gt;{{&lt;/span&gt;&lt;span&gt;#foreach&lt;/span&gt; &lt;span&gt;tags&lt;/span&gt; &lt;span&gt;visibility&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"all"&lt;/span&gt;&lt;span&gt;}}&lt;/span&gt;&lt;/span&gt;"&lt;span&gt;&lt;span&gt;{{&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;}}&lt;/span&gt;&lt;/span&gt;"&lt;span&gt;&lt;span&gt;{{&lt;/span&gt;&lt;span&gt;#unless&lt;/span&gt; &lt;span&gt;@&lt;/span&gt;&lt;span&gt;last&lt;/span&gt;&lt;span&gt;}}&lt;/span&gt;&lt;/span&gt;,&lt;span&gt;&lt;span&gt;{{&lt;/span&gt;&lt;span&gt;/unless&lt;/span&gt;&lt;span&gt;}}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;{{&lt;/span&gt;&lt;span&gt;/foreach&lt;/span&gt;&lt;span&gt;}}&lt;/span&gt;&lt;/span&gt;]&lt;span&gt;'&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class="token handlebars language-handlebars"&gt;&lt;span&gt;{{&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt; &lt;span&gt;"post-card"&lt;/span&gt;&lt;span&gt;}}&lt;/span&gt;&lt;/span&gt;
    &lt;span class="token tag"&gt;&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class="token handlebars language-handlebars"&gt;&lt;span&gt;{{&lt;/span&gt;&lt;span&gt;/foreach&lt;/span&gt;&lt;span&gt;}}&lt;/span&gt;&lt;/span&gt;
&lt;span class="line-numbers-rows"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important part here is &lt;code&gt;visibility="all"&lt;/code&gt;. Since &lt;code&gt;#en&lt;/code&gt;, &lt;code&gt;#ko&lt;/code&gt;, and &lt;code&gt;#multi&lt;/code&gt; are internal tags, they would not be included in the &lt;code&gt;{{#foreach tags}}&lt;/code&gt; result without this option.&lt;/p&gt;
&lt;h3 id="assetsjsutiljs"&gt;assets/js/util.js&lt;/h3&gt;
&lt;p&gt;I extracted the shared logic into &lt;code&gt;filterArchivePostsByLanguage()&lt;/code&gt; so the Tag page and Author page can use the same filtering function.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class="token keyword"&gt;function&lt;/span&gt; &lt;span class="token function"&gt;filterArchivePostsByLanguage&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
  &lt;span class="token keyword"&gt;const&lt;/span&gt; supportedLanguages &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token string"&gt;"en"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token string"&gt;"ko"&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  &lt;span class="token keyword"&gt;const&lt;/span&gt; selectedLanguage &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token function"&gt;getSelectedLanguage&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  &lt;span class="token keyword"&gt;const&lt;/span&gt; postCards &lt;span class="token operator"&gt;=&lt;/span&gt; document&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;querySelectorAll&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"div.post-card[data-tags]"&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  &lt;span class="token keyword"&gt;let&lt;/span&gt; visibleCount &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token number"&gt;0&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

  postCards&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;forEach&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token parameter"&gt;card&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
    &lt;span class="token keyword"&gt;let&lt;/span&gt; tagNames &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

    &lt;span class="token keyword"&gt;try&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
      tagNames &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token constant"&gt;JSON&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;parse&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;card&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;getAttribute&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;"data-tags"&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;||&lt;/span&gt; &lt;span class="token string"&gt;"[]"&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt; &lt;span class="token keyword"&gt;catch&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;error&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
      tagNames &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;[&lt;/span&gt;&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;

    &lt;span class="token keyword"&gt;const&lt;/span&gt; cardLanguage &lt;span class="token operator"&gt;=&lt;/span&gt; supportedLanguages&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;find&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token parameter"&gt;language&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt;
      tagNames&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;includes&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token template-string"&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;#&lt;/span&gt;&lt;span&gt;&lt;span&gt;${&lt;/span&gt;language&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
    &lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

    &lt;span class="token keyword"&gt;if&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;cardLanguage &lt;span class="token operator"&gt;!==&lt;/span&gt; selectedLanguage&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
      card&lt;span class="token punctuation"&gt;.&lt;/span&gt;style&lt;span class="token punctuation"&gt;.&lt;/span&gt;display &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string"&gt;"none"&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
      &lt;span class="token keyword"&gt;return&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;

    visibleCount &lt;span class="token operator"&gt;+=&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token function"&gt;setTimeout&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
      card&lt;span class="token punctuation"&gt;.&lt;/span&gt;style&lt;span class="token punctuation"&gt;.&lt;/span&gt;opacity &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
      card&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;querySelectorAll&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;".post-card-excerpt"&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;forEach&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token parameter"&gt;excerpt&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token operator"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
        excerpt&lt;span class="token punctuation"&gt;.&lt;/span&gt;style&lt;span class="token punctuation"&gt;.&lt;/span&gt;opacity &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token number"&gt;1&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
      &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
    &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token number"&gt;10&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
  &lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;

  &lt;span class="token keyword"&gt;return&lt;/span&gt; visibleCount&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;
&lt;span class="line-numbers-rows"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In &lt;code&gt;author.hbs&lt;/code&gt; and &lt;code&gt;tag.hbs&lt;/code&gt;, this function is called after the DOM is ready.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;document&lt;span class="token punctuation"&gt;.&lt;/span&gt;&lt;span class="token function"&gt;addEventListener&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token string"&gt;'DOMContentLoaded'&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt; &lt;span class="token keyword"&gt;function&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
    &lt;span class="token function"&gt;filterArchivePostsByLanguage&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="line-numbers-rows"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now on the Author page as well, if the Language selector is set to English, only &lt;code&gt;#en&lt;/code&gt; posts are shown, and if it is set to Korean, only &lt;code&gt;#ko&lt;/code&gt; posts are shown.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fsanghunkang.com%2Fcontent%2Fimages%2F2026%2F08%2F0.0.9-author-after-en.png" alt="In 0.0.9, the Author page shows only English posts" width="799" height="514"&gt;&lt;p&gt;This is the result of selecting English on the same &lt;code&gt;/author/sanghun/&lt;/code&gt; URL. Only &lt;code&gt;#en&lt;/code&gt; posts remain.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fsanghunkang.com%2Fcontent%2Fimages%2F2026%2F08%2F0.0.9-author-after-ko.png" alt="In 0.0.9, the Author page shows only Korean posts" width="799" height="514"&gt;&lt;p&gt;If you select Korean without changing the URL, only &lt;code&gt;#ko&lt;/code&gt; posts remain.&lt;/p&gt;
&lt;h2 id="moving-to-the-translated-post-from-a-multi-post"&gt;Moving to the translated post from a &lt;code&gt;#multi&lt;/code&gt; post&lt;/h2&gt;
&lt;p&gt;In this blog, posts that are translations of each other follow these rules.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The slug of an English post ends with &lt;code&gt;-en&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The slug of a Korean post ends with &lt;code&gt;-ko&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Both posts have the &lt;code&gt;#multi&lt;/code&gt; internal tag.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, the following two posts are translations of each other.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/example-post-en/
/ko/example-post-ko/
&lt;span class="line-numbers-rows"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In 0.0.8, I used these rules to generate the &lt;code&gt;hreflang&lt;/code&gt; URL automatically. In 0.0.9, I extended that so the Language selector can also use the URL that was already computed.&lt;/p&gt;
&lt;h3 id="partialshreflang-multihbs"&gt;partials/hreflang-multi.hbs&lt;/h3&gt;
&lt;p&gt;I store the current page's content type, whether it is a &lt;code&gt;#multi&lt;/code&gt; post, the current language, and the language-specific URLs in a global object.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window&lt;span class="token punctuation"&gt;.&lt;/span&gt;casperMultiLanguageContext &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
    contentType&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;"{{contentType}}"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    isMulti&lt;span class="token operator"&gt;:&lt;/span&gt; isMulti&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    currentLanguage&lt;span class="token operator"&gt;:&lt;/span&gt; lang&lt;span class="token punctuation"&gt;,&lt;/span&gt;
    urls&lt;span class="token operator"&gt;:&lt;/span&gt; urls
&lt;span class="token punctuation"&gt;}&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="line-numbers-rows"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If it is a &lt;code&gt;#multi&lt;/code&gt; post, &lt;code&gt;urls&lt;/code&gt; has the following form.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class="token punctuation"&gt;{&lt;/span&gt;
  en&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;"https://example.com/example-post-en/"&lt;/span&gt;&lt;span class="token punctuation"&gt;,&lt;/span&gt;
  ko&lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;"https://example.com/ko/example-post-ko/"&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;
&lt;span class="line-numbers-rows"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="defaulthbs"&gt;default.hbs&lt;/h3&gt;
&lt;p&gt;When the Language selector value changes, it checks whether the current page is a &lt;code&gt;#multi&lt;/code&gt; post. If the conditions match, it immediately moves to the post URL for the selected language.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class="token keyword"&gt;if&lt;/span&gt; &lt;span class="token punctuation"&gt;(&lt;/span&gt;
    contentContext &lt;span class="token operator"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    contentContext&lt;span class="token punctuation"&gt;.&lt;/span&gt;contentType &lt;span class="token operator"&gt;===&lt;/span&gt; &lt;span class="token string"&gt;'post'&lt;/span&gt; &lt;span class="token operator"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    contentContext&lt;span class="token punctuation"&gt;.&lt;/span&gt;isMulti &lt;span class="token operator"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    contentContext&lt;span class="token punctuation"&gt;.&lt;/span&gt;urls&lt;span class="token punctuation"&gt;[&lt;/span&gt;selectedLanguage&lt;span class="token punctuation"&gt;]&lt;/span&gt;
&lt;span class="token punctuation"&gt;)&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
    window&lt;span class="token punctuation"&gt;.&lt;/span&gt;location&lt;span class="token punctuation"&gt;.&lt;/span&gt;href &lt;span class="token operator"&gt;=&lt;/span&gt; contentContext&lt;span class="token punctuation"&gt;.&lt;/span&gt;urls&lt;span class="token punctuation"&gt;[&lt;/span&gt;selectedLanguage&lt;span class="token punctuation"&gt;]&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt; &lt;span class="token keyword"&gt;else&lt;/span&gt; &lt;span class="token punctuation"&gt;{&lt;/span&gt;
    window&lt;span class="token punctuation"&gt;.&lt;/span&gt;location&lt;span class="token punctuation"&gt;.&lt;/span&gt;href &lt;span class="token operator"&gt;=&lt;/span&gt; selectedLanguage &lt;span class="token operator"&gt;===&lt;/span&gt; &lt;span class="token string"&gt;'ko'&lt;/span&gt; &lt;span class="token operator"&gt;?&lt;/span&gt; &lt;span class="token string"&gt;'/ko/'&lt;/span&gt; &lt;span class="token operator"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;'/'&lt;/span&gt;&lt;span class="token punctuation"&gt;;&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;
&lt;span class="line-numbers-rows"&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The final behavior is as follows.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;#multi&lt;/code&gt; post: move to the version of that post in the other language&lt;/li&gt;
&lt;li&gt;Post without &lt;code&gt;#multi&lt;/code&gt;: move to the main page of the selected language&lt;/li&gt;
&lt;li&gt;Page: move to the main page of the selected language&lt;/li&gt;
&lt;li&gt;Tag or Author page: reload the current URL and show only posts in the selected language&lt;/li&gt;
&lt;li&gt;Any other screen: move to the main page of the selected language&lt;/li&gt;
&lt;/ul&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fsanghunkang.com%2Fcontent%2Fimages%2F2026%2F08%2F0.0.9-post-switch-before-ko.png" alt="Korean #multi post before switching" width="799" height="514"&gt;&lt;p&gt;On the Korean translated post, the selector points to Korean.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fsanghunkang.com%2Fcontent%2Fimages%2F2026%2F08%2F0.0.9-post-switch-after-en.png" alt="Result after selecting English and moving to the corresponding English post" width="799" height="514"&gt;&lt;p&gt;If you switch the selector to English, it goes directly not to the English main page but to the corresponding &lt;code&gt;/orca-codex-threads-chatgpt-en/&lt;/code&gt; post.&lt;/p&gt;
&lt;h2 id="limitations-of-author-page-filtering"&gt;Limitations of Author page filtering&lt;/h2&gt;
&lt;p&gt;Language filtering on the Author page works by hiding cards that have already been rendered in the browser. As a result, the pagination calculated by Ghost is still based on the full set of posts before language filtering.&lt;/p&gt;
&lt;p&gt;As the number of posts grows, the following issues may appear.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The number of posts actually visible on a page may decrease.&lt;/li&gt;
&lt;li&gt;Some pages may contain very few posts in the selected language.&lt;/li&gt;
&lt;li&gt;The total number of pages may not match the number of posts in the selected language.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the current blog, this causes no practical problem, so I chose the same approach as the existing Tag page. If accurate language-specific pagination becomes necessary later, it can be changed to a method that adds language-specific Channels without modifying Ghost Core.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/sanghunka/ghost-multilingual-theme/blob/feature/0.0.9-author-language-switch/AUTHOR_LANGUAGE_CHANNEL_PROPOSAL.md?ref=sanghunkang.com" rel="noopener noreferrer"&gt;Proposal for Language-Specific Author Channels&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="result"&gt;Result&lt;/h2&gt;
&lt;p&gt;In 0.0.9, the behavior of the Language selector was changed to fit the context of each screen.&lt;/p&gt;
&lt;p&gt;On the Author page, you can now view only posts in the selected language just like on the existing Tag page. On translated &lt;code&gt;#multi&lt;/code&gt; posts, it goes directly to the corresponding post without passing through the main page.&lt;/p&gt;
&lt;p&gt;I did not modify Ghost Core or the database. Since this change was also handled entirely within the theme, it can be applied as-is in managed hosting environments like ghost.io.&lt;/p&gt;
    

&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://sanghunkang.com/multilingual-ghost-cms-9-author-and-post-language-switch-en/" rel="noopener noreferrer"&gt;Sanghun Kang’s blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>css</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Make Orca Codex Threads Appear in the ChatGPT App</title>
      <dc:creator>Sanghun Kang</dc:creator>
      <pubDate>Wed, 07 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/sanghunkang/how-to-make-orca-codex-threads-appear-in-the-chatgpt-app-h1e</link>
      <guid>https://dev.to/sanghunkang/how-to-make-orca-codex-threads-appear-in-the-chatgpt-app-h1e</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzx8i9kaxnefpm2gjmxqd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzx8i9kaxnefpm2gjmxqd.png" alt="How to Make Orca Codex Threads Appear in the ChatGPT App" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/stablyai/orca?ref=sanghunkang.com" rel="noopener noreferrer"&gt;Orca&lt;/a&gt; was getting a lot of attention, so I gave it a try. It turned out to be genuinely useful for managing worktrees and terminal sessions in one place. Then I hit a deal-breaker: Codex threads created in Orca did not appear in the ChatGPT app. The reverse worked fine. Threads created in ChatGPT were visible when I launched Codex from Orca. For someone who wants the same setup everywhere, that asymmetry was a serious problem.&lt;/p&gt;

&lt;p&gt;The cause was simple. Orca was using its own &lt;code&gt;CODEX_HOME&lt;/code&gt;. Pointing it to the same directory as the ChatGPT app fixed the issue.&lt;/p&gt;

&lt;p&gt;The solution is to set &lt;code&gt;CODEX_HOME&lt;/code&gt; to &lt;code&gt;~/.codex&lt;/code&gt; for the Codex process that Orca launches.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;env CODEX_HOME="$HOME/.codex" codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is one catch: putting this in &lt;code&gt;.zshrc&lt;/code&gt; does not work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;A Codex thread is represented by roughly two kinds of data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sessions/.../rollout-*.jsonl&lt;/code&gt;: the actual conversation and execution events&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;state_5.sqlite&lt;/code&gt;: an index containing the thread ID, title, working directory, timestamps, and JSONL path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ChatGPT app and a regular Codex CLI session use this directory by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.codex/
├── sessions/
└── state_5.sqlite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the time, Orca forced Codex to use a separate managed directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/Library/Application Support/orca/codex-runtime-home/home/
├── sessions/
└── state_5.sqlite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That split the thread storage into two independent paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Thread started in the ChatGPT app
└── ~/.codex

Thread started in Orca
└── ~/Library/Application Support/orca/codex-runtime-home/home
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ChatGPT app reads &lt;code&gt;~/.codex&lt;/code&gt;, so it could not display threads that existed only in Orca's managed directory. Orca, however, could import existing sessions from &lt;code&gt;~/.codex&lt;/code&gt;. That one-way import explained the odd behavior: ChatGPT threads were visible in Orca, but Orca threads were missing from ChatGPT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Cause
&lt;/h2&gt;

&lt;p&gt;Reading Orca's source revealed the following launch sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Orca prepares its own managed runtime home.&lt;/li&gt;
&lt;li&gt;It starts Codex with &lt;code&gt;CODEX_HOME&lt;/code&gt; set to that directory.&lt;/li&gt;
&lt;li&gt;It reapplies the managed value after the shell starts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is why adding the following line to &lt;code&gt;.zshrc&lt;/code&gt; is not enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export CODEX_HOME="$HOME/.codex"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shell may load the value, but Orca overwrites it when launching Codex.&lt;/p&gt;

&lt;p&gt;Symlinking only the &lt;code&gt;sessions&lt;/code&gt; directory is not a complete fix either. The conversation body lives in JSONL files, but the thread list and metadata also live in &lt;code&gt;state_5.sqlite&lt;/code&gt;. If the JSONL files and SQLite index point to different stores, a thread may exist on disk without appearing in the app.&lt;/p&gt;

&lt;p&gt;The important part is to make Codex use the same &lt;code&gt;CODEX_HOME&lt;/code&gt; as the ChatGPT app &lt;strong&gt;from the moment the process starts&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Open the following setting in Orca:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Settings &amp;gt; Agents &amp;gt; Codex &amp;gt; Command
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the default &lt;code&gt;codex&lt;/code&gt; command with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;env CODEX_HOME="$HOME/.codex" codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not add this command to &lt;code&gt;.zshrc&lt;/code&gt;. It belongs in Orca's &lt;strong&gt;Codex Command&lt;/strong&gt; field as a command override.&lt;/p&gt;

&lt;p&gt;New Codex threads started from Orca will now be written to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.codex/sessions/
~/.codex/state_5.sqlite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the ChatGPT app reads the same store, those threads appear in its sidebar as well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Existing Orca Threads Are Not Migrated Automatically
&lt;/h3&gt;

&lt;p&gt;This setting changes where future threads are stored. Threads created before the change remain in Orca's managed store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/Library/Application Support/orca/codex-runtime-home/home/sessions/
~/Library/Application Support/orca/codex-runtime-home/home/state_5.sqlite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make an existing Orca thread visible in ChatGPT, copy or hard-link its JSONL file into &lt;code&gt;~/.codex/sessions/&lt;/code&gt;, then migrate the matching &lt;code&gt;threads&lt;/code&gt; row into &lt;code&gt;~/.codex/state_5.sqlite&lt;/code&gt;. Back up both databases first and move only the required row. Replacing the entire database can wipe out threads that already exist in ChatGPT.&lt;/p&gt;




&lt;h2&gt;
  
  
  Detours and Dead Ends
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why not just set CODEX_HOME in .zshrc?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My first attempt was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export CODEX_HOME="$HOME/.codex"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It failed because Orca sets &lt;code&gt;CODEX_HOME&lt;/code&gt; to its managed runtime home when Codex launches and reapplies that value after shell startup. Changing only the shell configuration is therefore not reliable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does “Codex home to import from” do?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This option does not change the home directory used by the Codex process. It performs a one-way import of existing sessions from &lt;code&gt;~/.codex/sessions&lt;/code&gt; into Orca's managed home.&lt;/p&gt;

&lt;p&gt;That behavior explains the asymmetry: threads created in ChatGPT were visible in Orca, while threads created in Orca were invisible in ChatGPT.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://sanghunkang.com/orca-codex-threads-chatgpt-en/" rel="noopener noreferrer"&gt;Sanghun Kang’s blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nocode</category>
    </item>
    <item>
      <title>Multilingual Ghost CMS Blog: 8. Automating hreflang for #multi Posts</title>
      <dc:creator>Sanghun Kang</dc:creator>
      <pubDate>Tue, 06 Oct 2026 02:04:46 +0000</pubDate>
      <link>https://dev.to/sanghunkang/multilingual-ghost-cms-blog-8-automating-hreflang-for-multi-posts-3233</link>
      <guid>https://dev.to/sanghunkang/multilingual-ghost-cms-blog-8-automating-hreflang-for-multi-posts-3233</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffi3acpzvwpn9jjt8kz19.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffi3acpzvwpn9jjt8kz19.png" alt="Multilingual Ghost CMS Blog: 8. Automating hreflang for #multi Posts" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 On Managed Ghost hosting, you cannot modify Ghost Core or register custom Handlebars helpers. This is the approach I chose to eliminate repetitive Code Injection within that constraint.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For each post, I could manually insert &lt;code&gt;hreflang&lt;/code&gt; into Ghost Admin's Code Injection. But every time I publish a translated post, I would have to enter both the English URL and the Korean URL repeatedly on both posts. There is also a chance of copying a URL incorrectly or adding it to only one side.&lt;/p&gt;

&lt;p&gt;In 0.0.8, I changed this so the theme handles the job automatically. I also removed the tag-order dependency issue that surfaced along with it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqqr79ricgum05c91ezkm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqqr79ricgum05c91ezkm.png" alt="The slug and internal tags of the Korean translation post as shown in Ghost Admin" width="799" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a screen showing a real published post imported into local Ghost. The slug ends with &lt;code&gt;orca-codex-threads-chatgpt-ko&lt;/code&gt;, and the tags include both &lt;code&gt;#ko&lt;/code&gt; and &lt;code&gt;#multi&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sanghunka/ghost-multilingual-theme/commit/5492342?ref=sanghunkang.com" rel="noopener noreferrer"&gt;0.0.8 · Automated bilingual hreflang and tag detection&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules for multilingual posts
&lt;/h2&gt;

&lt;p&gt;I use the following rules for automation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The English post slug ends with &lt;code&gt;-en&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Korean post slug ends with &lt;code&gt;-ko&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add the &lt;code&gt;#multi&lt;/code&gt; internal tag to posts whose translations are all ready.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Each post must have either the &lt;code&gt;#en&lt;/code&gt; or &lt;code&gt;#ko&lt;/code&gt; internal tag to indicate its own language.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, the two posts below are one translation pair.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;English slug: multilingual-ghost-cms-en
Korean slug:  multilingual-ghost-cms-ko
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual URLs are generated like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://sanghunkang.com/multilingual-ghost-cms-en/
https://sanghunkang.com/ko/multilingual-ghost-cms-ko/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  partials/hreflang-multi.hbs
&lt;/h2&gt;

&lt;p&gt;I separated the code that generates multilingual URLs for posts and pages into a separate partial.&lt;/p&gt;

&lt;p&gt;First, set the supported languages and the default language.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;LANGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ko&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_LANG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, turn all of the post's tag names into a JavaScript array.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;var tagNames = [
    &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;foreach&lt;/span&gt; &lt;span class="nv"&gt;tags&lt;/span&gt; &lt;span class="nv"&gt;visibility&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"all"&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
        "&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;"&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;unless&lt;/span&gt; &lt;span class="na"&gt;@last&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;,&lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;unless&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;foreach&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason for specifying &lt;code&gt;visibility="all"&lt;/code&gt; is that &lt;code&gt;#multi&lt;/code&gt;, &lt;code&gt;#en&lt;/code&gt;, and &lt;code&gt;#ko&lt;/code&gt; are all internal tags. Without this option, Ghost excludes internal tags from the loop result.&lt;/p&gt;

&lt;p&gt;If there is no &lt;code&gt;#multi&lt;/code&gt;, do nothing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tagNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#multi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The current post's language is also found from the tag array.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;LANGS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tagNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you remove the trailing &lt;code&gt;-en&lt;/code&gt; or &lt;code&gt;-ko&lt;/code&gt; from the slug, you get the stem shared by the translated posts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;suffixPattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;LANGS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;|&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;)$&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;stem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;suffixPattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Append each language suffix and URL prefix back onto this stem to build the URL for each language.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_LANG&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;siteUrl&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;stem&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, add &lt;code&gt;hreflang&lt;/code&gt; links to &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;LANGS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;link rel="alternate" hreflang="&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;" href="&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;buildUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;"/&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;link rel="alternate" hreflang="x-default" href="&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;buildUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DEFAULT_LANG&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;"/&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertAdjacentHTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;beforeend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Calling the partial in post and page contexts
&lt;/h2&gt;

&lt;p&gt;In the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; of &lt;code&gt;default.hbs&lt;/code&gt;, call the partial when the current view is a post or a page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;is&lt;/span&gt; &lt;span class="s2"&gt;"post"&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;post&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
        &lt;span class="k"&gt;{{&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"hreflang-multi"&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;post&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;is&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;

&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;is&lt;/span&gt; &lt;span class="s2"&gt;"page"&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;page&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
        &lt;span class="k"&gt;{{&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"hreflang-multi"&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;page&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;is&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It must be called inside the &lt;code&gt;{{#post}}&lt;/code&gt; and &lt;code&gt;{{#page}}&lt;/code&gt; contexts so the partial can use the current content's &lt;code&gt;{{slug}}&lt;/code&gt; and &lt;code&gt;{{tags}}&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem that depended on tag order
&lt;/h2&gt;

&lt;p&gt;The existing Tag page used the rule that the second tag was the language tag.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;data-language="&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this approach, if the tag order changes, language filtering breaks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Ghost, #ko, #multi]  -&amp;gt; works
[#multi, Ghost, #ko]  -&amp;gt; fails
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In 0.0.8, all tags of each post are placed into &lt;code&gt;data-tags&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"post-card"&lt;/span&gt; &lt;span class="na"&gt;data-tags=&lt;/span&gt;&lt;span class="s"&gt;'[&lt;/span&gt;&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;foreach&lt;/span&gt; &lt;span class="nv"&gt;tags&lt;/span&gt; &lt;span class="nv"&gt;visibility&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"all"&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;unless&lt;/span&gt; &lt;span class="na"&gt;@last&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;unless&lt;/span&gt;&lt;span class="k"&gt;}}{{/&lt;/span&gt;&lt;span class="nn"&gt;foreach&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;]'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JavaScript searches the entire array for the language tag.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cardLang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;LANGS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tagNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;cardLang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it does not matter which position &lt;code&gt;#en&lt;/code&gt; or &lt;code&gt;#ko&lt;/code&gt; is in.&lt;/p&gt;

&lt;p&gt;I also did not use Ghost's &lt;code&gt;{{#has tag="..."}}&lt;/code&gt; helper to detect internal tags. The display name of the internal tag is &lt;code&gt;#multi&lt;/code&gt;, but its actual slug is &lt;code&gt;hash-multi&lt;/code&gt;, so if you confuse the name and slug, the condition can fail silently. Passing all internal tag names into an array and comparing them explicitly in JavaScript was more predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking the result
&lt;/h2&gt;

&lt;p&gt;Open Developer Tools on a &lt;code&gt;#multi&lt;/code&gt; post and run the code below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;link[rel="alternate"][hreflang]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hreflang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On both the English post and the Korean post, three links, &lt;code&gt;en&lt;/code&gt;, &lt;code&gt;ko&lt;/code&gt;, and &lt;code&gt;x-default&lt;/code&gt;, should appear, and each URL should point to the correct translated post.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhocf35piogbs6nakfczm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhocf35piogbs6nakfczm.png" alt="hreflang inspection result for the Korean #multi post in 0.0.8" width="799" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the result of inspecting the DOM on the Korean Orca post. &lt;code&gt;en&lt;/code&gt; points to the English translated post, &lt;code&gt;ko&lt;/code&gt; points to the current Korean post, and &lt;code&gt;x-default&lt;/code&gt; points to the English translated post. The black panel is an overlay showing the actual DOM query result on the screen.&lt;/p&gt;

&lt;p&gt;On the Tag page, even if you change the order of a post's tags, only posts in the selected language should still display correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things to watch out for
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;#multi&lt;/code&gt; should be added only when all language versions actually exist. If there is only a Korean post but you add &lt;code&gt;#multi&lt;/code&gt;, the theme will also generate an English URL according to the rule, and that URL will return a 404.&lt;/p&gt;

&lt;p&gt;Also, the post's &lt;code&gt;hreflang&lt;/code&gt; is added to &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; by browser JavaScript. Google renders JavaScript, so it can process this approach, but some crawlers with weak JavaScript rendering may miss the links.&lt;/p&gt;




&lt;p&gt;Starting from 0.0.8, if you set the language tag and &lt;code&gt;#multi&lt;/code&gt; correctly on translated posts, &lt;code&gt;hreflang&lt;/code&gt; is generated automatically.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>css</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Syncing Codex Settings and Conversation History Across Multiple Macs</title>
      <dc:creator>Sanghun Kang</dc:creator>
      <pubDate>Tue, 06 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/sanghunkang/syncing-codex-settings-and-conversation-history-across-multiple-macs-f51</link>
      <guid>https://dev.to/sanghunkang/syncing-codex-settings-and-conversation-history-across-multiple-macs-f51</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn9ir3vlvspa3jamdmyvg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn9ir3vlvspa3jamdmyvg.png" alt="Syncing Codex Settings and Conversation History Across Multiple Macs" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I use Codex on both a MacBook and a Mac Mini. I wanted not only the same skills and settings on both machines, but also the ability to continue a conversation started on either one.&lt;/p&gt;

&lt;p&gt;I added Codex's &lt;code&gt;.codex&lt;/code&gt; directory to Syncthing using the same approach I had used for &lt;a href="https://sanghunkang.com/syncing-claude-code-config-across-multiple-macs-with-syncthing-en/" rel="noopener noreferrer"&gt;syncing Claude Code settings across multiple Macs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The files synchronized correctly, but sessions created on the other machine still did not appear in the left sidebar.&lt;/p&gt;

&lt;p&gt;Syncthing reported that everything was up to date, yet a Codex session created on the Mac Mini was missing from the MacBook sidebar.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Conversation data lives in &lt;code&gt;~/.codex/sessions/**/*.jsonl&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The sidebar reads each machine's local &lt;code&gt;~/.codex/state_5.sqlite&lt;/code&gt; index.&lt;/li&gt;
&lt;li&gt;The JSONL file had arrived, but its session ID was absent from the receiving machine's SQLite database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post explains how I synchronize Codex settings and conversation JSONL files in both directions, then automatically repair each machine's local sidebar index as soon as Syncthing finishes receiving a file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What lives inside ~/.codex
&lt;/h2&gt;

&lt;p&gt;Codex keeps its settings and local task data under &lt;code&gt;~/.codex/&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Sync&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AGENTS.md&lt;/td&gt;
&lt;td&gt;Global instructions&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;config.toml&lt;/td&gt;
&lt;td&gt;Codex configuration&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rules/&lt;/td&gt;
&lt;td&gt;Execution rules&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;skills/&lt;/td&gt;
&lt;td&gt;Custom skills&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;automations/&lt;/td&gt;
&lt;td&gt;Automation settings&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sessions/&lt;/td&gt;
&lt;td&gt;Active task JSONL files&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;archived_sessions/&lt;/td&gt;
&lt;td&gt;Archived task JSONL files&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;history.jsonl&lt;/td&gt;
&lt;td&gt;Input history&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;session_index.jsonl&lt;/td&gt;
&lt;td&gt;Auxiliary session index&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;attachments/&lt;/td&gt;
&lt;td&gt;Task attachments&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;state_5.sqlite*&lt;/td&gt;
&lt;td&gt;Machine-specific sidebar and runtime state&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;auth.json and other credentials&lt;/td&gt;
&lt;td&gt;Machine-specific authentication&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caches, logs, and plugin runtimes&lt;/td&gt;
&lt;td&gt;Re-creatable or machine-specific&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When I synchronized Claude Code settings, I excluded sessions. This time, continuing Codex tasks across machines was the point, so I included both &lt;code&gt;sessions/&lt;/code&gt; and &lt;code&gt;archived_sessions/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not synchronize state_5.sqlite directly?
&lt;/h2&gt;

&lt;p&gt;Synchronizing SQLite would seem to make both sidebars identical automatically. I did not like that approach.&lt;/p&gt;

&lt;p&gt;While Codex is running, SQLite may use all three of these files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;state_5.sqlite
state_5.sqlite-wal
state_5.sqlite-shm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Syncthing does not transfer these three files as one atomic unit.&lt;/li&gt;
&lt;li&gt;Both machines may modify the database at the same time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That can create conflict files or cause one machine's changes to overwrite the other's.&lt;/p&gt;

&lt;p&gt;The safer design is to synchronize only the JSONL source records and let each machine rebuild its own SQLite index.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual problem
&lt;/h2&gt;

&lt;p&gt;A Codex session created on the Mac Mini had already synchronized its JSONL record to the MacBook, but the session did not appear in the MacBook sidebar.&lt;/p&gt;

&lt;p&gt;The JSONL file on the MacBook was byte-for-byte identical to the one on the Mini, and &lt;code&gt;.stignore&lt;/code&gt; was not excluding it. The ID existed in the Mini's &lt;code&gt;state_5.sqlite&lt;/code&gt;, but not in the MacBook's database.&lt;/p&gt;

&lt;p&gt;Syncthing had finished its job. Codex simply had not registered the newly received JSONL file in the local sidebar index.&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;codex resume [SESSION_ID]&lt;/code&gt; could reopen the session. But finding an ID and manually resuming it every time is not synchronization. It is manual recovery, which was not what I wanted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Syncthing setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Install Syncthing
&lt;/h3&gt;

&lt;p&gt;Install and run Syncthing on both machines with Homebrew.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;syncthing
brew services start syncthing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The web UI is available at &lt;code&gt;http://localhost:8384&lt;/code&gt; by default.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Connect the two machines
&lt;/h3&gt;

&lt;p&gt;Copy the Device ID from one machine and add it as a Remote Device on the other. Approve the connection request to begin bidirectional synchronization.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Register ~/.codex as a shared folder
&lt;/h3&gt;

&lt;p&gt;Use the same Folder ID on both machines.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Folder Label: codex-config
Folder ID: codex-config
Folder Path: /Users/&amp;lt;username&amp;gt;/.codex
Folder Type: Send &amp;amp; Receive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Use an allowlist-style .stignore
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;.codex&lt;/code&gt; directory mixes portable configuration with credentials and machine-specific runtime state. An allowlist is safer than continually expanding an exclusion list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Codex history + portable setup
!/config.toml
!/AGENTS.md
!/rules
!/rules/**
!/skills
!/skills/**
!/automations
!/automations/**
!/sessions
!/sessions/**
!/archived_sessions
!/archived_sessions/**
!/history.jsonl
!/session_index.jsonl
!/attachments
!/attachments/**

// Ignore everything else
*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Following a suggestion from &lt;a href="https://automationama.com/?ref=sanghunkang.com" rel="noopener noreferrer"&gt;Sean Dotson&lt;/a&gt;, I added &lt;code&gt;/&lt;/code&gt; to anchor the allowlist patterns at the folder root. This includes only the intended top-level files and folders and reduces unnecessary traversal of subdirectories.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With this setup, &lt;code&gt;state_5.sqlite&lt;/code&gt;, its WAL and SHM files, credentials, caches, and logs are excluded by the final &lt;code&gt;*&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repairing a missing sidebar entry after JSONL arrives
&lt;/h2&gt;

&lt;p&gt;Codex App Server's &lt;code&gt;thread/list&lt;/code&gt; method supports the &lt;code&gt;useStateDbOnly&lt;/code&gt; option.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;true&lt;/code&gt;: return only tasks already registered in SQLite.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;false&lt;/code&gt;: scan JSONL task logs and repair missing metadata.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no need to modify SQLite directly. Ask Codex itself to scan the JSONL files again.&lt;/p&gt;

&lt;p&gt;The essential request looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"thread/list"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"archived"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sourceKinds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cli"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vscode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"appServer"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"useStateDbOnly"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paginating through both active and archived tasks registers JSONL-only tasks in the local state database. The operation is safe to repeat and never writes to SQLite directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the right trigger
&lt;/h2&gt;

&lt;p&gt;The trigger required more thought than the repair script itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  First attempt: a five-minute LaunchAgent
&lt;/h3&gt;

&lt;p&gt;My first version ran once at login, watched &lt;code&gt;session_index.jsonl&lt;/code&gt;, and rescanned every five minutes in case it missed an event.&lt;/p&gt;

&lt;p&gt;It was reliable, but imprecise. A file could arrive and still wait until the next interval, while a full scan ran even when nothing had changed.&lt;/p&gt;

&lt;p&gt;More importantly, I disliked wasting resources for no reason.&lt;/p&gt;

&lt;h3&gt;
  
  
  Second attempt: Codex SessionStart and SessionEnd
&lt;/h3&gt;

&lt;p&gt;Codex can run hooks at session start and end. These hooks still did not match the event I needed.&lt;/p&gt;

&lt;p&gt;The relevant event is not "a Codex session started." It is "Syncthing finished receiving a remote JSONL file." Starting or closing the app does not correspond to file arrival.&lt;/p&gt;

&lt;p&gt;An app launch or shutdown hook would have made the solution much simpler, but Codex does not currently provide one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final approach: Syncthing ItemFinished events
&lt;/h3&gt;

&lt;p&gt;Syncthing provides a REST Event API. &lt;code&gt;ItemFinished&lt;/code&gt; is emitted after a newer file version finishes synchronizing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /rest/events?events=ItemFinished
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A listener on each machine long-polls this endpoint and handles only events matching all four conditions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Folder ID is &lt;code&gt;codex-config&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;error&lt;/code&gt; is empty.&lt;/li&gt;
&lt;li&gt;The event is an &lt;code&gt;update&lt;/code&gt; to file contents.&lt;/li&gt;
&lt;li&gt;The path matches &lt;code&gt;sessions/**/*.jsonl&lt;/code&gt; or &lt;code&gt;archived_sessions/**/*.jsonl&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Several files may arrive in quick succession, so the listener briefly debounces them and runs one repair. It also performs a complete repair when it starts, covering events missed while the listener was offline. A file lock prevents automatic and manual reconciliation from overlapping.&lt;/p&gt;

&lt;p&gt;The final flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a task on the Mini
  → Syncthing sends the JSONL file to the MacBook
  → The MacBook ItemFinished listener receives the event
  → MacBook Codex App Server scans the JSONL histories
  → The MacBook repairs its local SQLite index
  → Reopen Codex to show the task in the MacBook sidebar

Tasks created on the MacBook follow the same flow in reverse
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Limitation: the running app's UI cache
&lt;/h3&gt;

&lt;p&gt;Even after this automation, an already-running Codex app does not refresh its sidebar immediately. JSONL synchronization and local SQLite index repair finish automatically, but the task list already held in memory does not appear to learn that an external process updated the index.&lt;/p&gt;

&lt;p&gt;I could not find an official command or supported interface for forcing a sidebar refresh. To see a newly synchronized task in the sidebar, I currently need to close and reopen Codex.&lt;/p&gt;

&lt;p&gt;It is not fully real-time synchronization, but it is good enough for how I work. Reopening Codex after working on the other machine shows the latest sessions, and I am happy with that result.&lt;/p&gt;

&lt;h2&gt;
  
  
  LaunchAgent configuration
&lt;/h2&gt;

&lt;p&gt;Syncthing itself already runs as a Homebrew LaunchAgent on both machines. I installed the event listener as a separate LaunchAgent instead of modifying Homebrew's service file. This survives &lt;code&gt;brew upgrade&lt;/code&gt; and makes it possible to restart and inspect Syncthing and the listener independently.&lt;/p&gt;

&lt;p&gt;Bidirectional task synchronization requires installing this LaunchAgent on both Macs because each receiving machine must register incoming JSONL files in its own local index.&lt;/p&gt;

&lt;p&gt;The complete implementation is available in the public &lt;a href="https://github.com/sanghunka/reconcile-codex-threads?ref=sanghunkang.com" rel="noopener noreferrer"&gt;&lt;code&gt;reconcile-codex-threads&lt;/code&gt;&lt;/a&gt; repository. Automatic repair uses these three files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/sanghunka/reconcile-codex-threads/blob/main/skill/reconcile-codex-threads/scripts/codex_thread_sync_listener.py?ref=sanghunkang.com" rel="noopener noreferrer"&gt;&lt;code&gt;codex_thread_sync_listener.py&lt;/code&gt;&lt;/a&gt;: waits for Syncthing &lt;code&gt;ItemFinished&lt;/code&gt; events.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/sanghunka/reconcile-codex-threads/blob/main/skill/reconcile-codex-threads/scripts/codex_thread_index_reconciler.py?ref=sanghunkang.com" rel="noopener noreferrer"&gt;&lt;code&gt;codex_thread_index_reconciler.py&lt;/code&gt;&lt;/a&gt;: repairs the local index through Codex App Server.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/sanghunka/reconcile-codex-threads/blob/main/skill/reconcile-codex-threads/scripts/install_launch_agent.py?ref=sanghunkang.com" rel="noopener noreferrer"&gt;&lt;code&gt;install_launch_agent.py&lt;/code&gt;&lt;/a&gt;: installs a LaunchAgent using the current user's paths.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The essential LaunchAgent configuration is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Label&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;local.reconcile-codex-threads.listener&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;ProgramArguments&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/usr/bin/python3&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/Users/&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;username&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;/.local/libexec/codex_thread_sync_listener.py&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;--syncthing-config&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/Users/&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;username&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;/Library/Application Support/Syncthing/config.xml&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;--reconciler&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/Users/&lt;span class="ni"&gt;&amp;amp;lt;&lt;/span&gt;username&lt;span class="ni"&gt;&amp;amp;gt;&lt;/span&gt;/.local/libexec/codex_thread_index_reconciler.py&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;RunAtLoad&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;KeepAlive&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The listener waits for Syncthing events and runs only when a relevant JSONL file arrives. It reads the API key from each machine's Syncthing &lt;code&gt;config.xml&lt;/code&gt; at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual reconciliation skill
&lt;/h2&gt;

&lt;p&gt;I also created &lt;a href="https://github.com/sanghunka/reconcile-codex-threads/blob/main/skill/reconcile-codex-threads/SKILL.md?ref=sanghunkang.com" rel="noopener noreferrer"&gt;&lt;code&gt;$reconcile-codex-threads&lt;/code&gt;&lt;/a&gt; as a manual inspection path. It performs the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Request an immediate rescan of the &lt;code&gt;codex-config&lt;/code&gt; folder on both machines.&lt;/li&gt;
&lt;li&gt;Reconcile both local indexes from JSONL histories.&lt;/li&gt;
&lt;li&gt;Retrieve both state-only sidebar inventories through App Server.&lt;/li&gt;
&lt;li&gt;Compare task IDs and archive state.&lt;/li&gt;
&lt;li&gt;Report titles and IDs that exist on only one machine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The controller can also be run directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/usr/bin/python3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CODEX_HOME&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="p"&gt;/.codex&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/skills/reconcile-codex-threads/scripts/reconcile_codex_threads.py"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact filename is &lt;a href="https://github.com/sanghunka/reconcile-codex-threads/blob/main/skill/reconcile-codex-threads/scripts/reconcile_codex_threads.py?ref=sanghunkang.com" rel="noopener noreferrer"&gt;&lt;code&gt;reconcile_codex_threads.py&lt;/code&gt;&lt;/a&gt;. It does not transfer files itself. It requests a Syncthing rescan, repairs both local indexes, and compares the results.&lt;/p&gt;

&lt;p&gt;On the machine used to run the controller, create the non-synchronized local configuration file &lt;code&gt;~/.config/reconcile-codex-threads/config.json&lt;/code&gt; and specify the other Mac's SSH alias.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ssh_host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"other-mac"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A converged result looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"converged"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"local"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"thread_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;277&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"active_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;268&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"archived_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"remote"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"thread_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;277&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"active_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;268&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"archived_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"only_local"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"only_remote"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"archive_state_mismatches"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;converged&lt;/code&gt; means both task inventories and archive states match. Project and section organization, pins, display order, icons, and colors are not synchronized. These could be implemented separately, but I left them out because my goal was to continue the same conversations on another Mac. You may need to reopen Codex for new tasks to appear in the running app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  JSONL and the sidebar are different states
&lt;/h3&gt;

&lt;p&gt;A file existing on disk does not guarantee that it will appear in the sidebar. The conversation record and the local display index must be diagnosed separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run one complete repair when the listener starts
&lt;/h3&gt;

&lt;p&gt;Syncthing's Event API is not an infinite permanent event log. The listener may miss events while it is stopped, so it should scan all JSONL histories once at startup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Active tasks change frequently
&lt;/h3&gt;

&lt;p&gt;The same JSONL file can arrive repeatedly while a conversation is active. Running a complete repair on every event would be wasteful, so a short debounce and a single-execution lock are important.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/sanghunka/reconcile-codex-threads?ref=sanghunkang.com" rel="noopener noreferrer"&gt;reconcile-codex-threads GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.openai.com/codex/app-server?ref=sanghunkang.com" rel="noopener noreferrer"&gt;Codex App Server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.syncthing.net/dev/rest.html?ref=sanghunkang.com#event-endpoints" rel="noopener noreferrer"&gt;Syncthing Event API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.syncthing.net/v2.0.0/events/itemfinished.html?ref=sanghunkang.com" rel="noopener noreferrer"&gt;Syncthing ItemFinished&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  -update- 2026-10-09: Thread registration stopped after an app update
&lt;/h2&gt;

&lt;p&gt;The setup eventually stopped working again. Syncthing reported that synchronization was complete, but recent conversations from the other Mac were missing from the Codex task list.&lt;/p&gt;

&lt;p&gt;The JSONL files had reached both machines, and both Syncthing and the event listener were running. The failure was in the executable used for index repair. The LaunchAgent still passed this fixed path through &lt;code&gt;--codex-bin&lt;/code&gt;, but the currently installed app no longer contained that file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/Applications/ChatGPT.app/Contents/Resources/codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;File arrival events were being received normally. Each repair then failed with &lt;code&gt;No such file or directory&lt;/code&gt;. The executable in the current app installation was at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/Applications/ChatGPT.app/Contents/Resources/codex-cli/bin/codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Use the current executable and discover it at runtime
&lt;/h3&gt;

&lt;p&gt;I first ran the existing reconciler with the new executable. The &lt;code&gt;thread/list&lt;/code&gt; request with &lt;code&gt;useStateDbOnly: false&lt;/code&gt; still worked and registered recent Mini conversations that were missing locally. I connected to the Mini over SSH and verified repair in the other direction as well.&lt;/p&gt;

&lt;p&gt;I then changed how the scripts select the executable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The reconciler checks the current app's &lt;code&gt;codex-cli/bin/codex&lt;/code&gt; layout first. Older app layouts, the managed App Server's &lt;code&gt;~/.codex/packages/app-server-daemon/current/bin/codex&lt;/code&gt;, and a CLI on &lt;code&gt;PATH&lt;/code&gt; are fallback candidates.&lt;/li&gt;
&lt;li&gt;I removed the obsolete &lt;code&gt;--codex-bin&lt;/code&gt; argument pair from the LaunchAgent.&lt;/li&gt;
&lt;li&gt;The listener no longer fixes the executable path at startup. Unless an explicit override is supplied, each repair invocation lets the reconciler discover an available executable again.&lt;/li&gt;
&lt;li&gt;The installer no longer writes a resolved executable path into the LaunchAgent by default. Explicit &lt;code&gt;--codex-bin&lt;/code&gt; and &lt;code&gt;CODEX_BIN&lt;/code&gt; overrides remain available.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I applied the updated listener, reconciler, and LaunchAgent on both Macs and restarted the services. The trigger remains Syncthing's &lt;code&gt;ItemFinished&lt;/code&gt; event. The repair still uses Codex App Server rather than editing or synchronizing SQLite directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recovery and verification
&lt;/h3&gt;

&lt;p&gt;With the updated scripts installed, a one-time repair no longer needs an explicit executable path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/usr/bin/python3 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.local/libexec/codex_thread_sync_listener.py"&lt;/span&gt; &lt;span class="nt"&gt;--once&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command assumes the new discovery logic is installed. To test an older copy of the script first, pass the current app path shown above through &lt;code&gt;--codex-bin&lt;/code&gt; and check whether the repair API succeeds.&lt;/p&gt;

&lt;p&gt;At verification time, all 12 of the Mini's newest threads were registered in the local index. A conversation running locally was also registered on the Mini, and the local desktop task list returned the imported Mini conversations. After restarting the services, both machines logged automatic repairs triggered by received JSONL files with &lt;code&gt;status: ok&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The complete historical inventories still differed. There were 14 older archived entries with listing differences and one archive-state mismatch. Their JSONL files existed on both machines, with old index paths or states still differing. This repair restored recent-thread registration and automatic reconciliation; it did not establish complete historical convergence.&lt;/p&gt;

&lt;p&gt;If the running app still holds a cached task list after index repair, closing and reopening Codex may be necessary. If the symptom returns, check the listener logs and the actual Codex executable path as well as Syncthing's status.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://sanghunkang.com/syncing-codex-config-and-threads-across-multiple-macs-with-syncthing-en/" rel="noopener noreferrer"&gt;Sanghun Kang’s blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Multilingual Ghost CMS Blog: 7. Adding hreflang</title>
      <dc:creator>Sanghun Kang</dc:creator>
      <pubDate>Mon, 05 Oct 2026 02:03:59 +0000</pubDate>
      <link>https://dev.to/sanghunkang/multilingual-ghost-cms-blog-7-adding-hreflang-2dl9</link>
      <guid>https://dev.to/sanghunkang/multilingual-ghost-cms-blog-7-adding-hreflang-2dl9</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffi3acpzvwpn9jjt8kz19.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffi3acpzvwpn9jjt8kz19.png" alt="Multilingual Ghost CMS Blog: 7. Adding hreflang" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The English home is served at &lt;code&gt;/&lt;/code&gt;, and the Korean home at &lt;code&gt;/ko/&lt;/code&gt;, but search engines had no information indicating that the two pages were versions in different languages.&lt;/p&gt;

&lt;p&gt;Providing users with a Language selector and explaining multilingual relationships to search engines are separate issues. For search engines, you need to use &lt;code&gt;hreflang&lt;/code&gt; to indicate the relationship between each language URL.&lt;/p&gt;

&lt;p&gt;In 0.0.7, I added &lt;code&gt;hreflang&lt;/code&gt; to the English and Korean home pages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6kmlmbpqzaixl31mlszq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6kmlmbpqzaixl31mlszq.png" alt="0.0.6 Korean home without hreflang links" width="799" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sanghunka/ghost-multilingual-theme/commit/69c5e15?ref=sanghunkang.com" rel="noopener noreferrer"&gt;0.0.7 · Rename theme folder&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sanghunka/ghost-multilingual-theme/commit/5492342?ref=sanghunkang.com" rel="noopener noreferrer"&gt;0.0.7~0.0.8 · Home and post hreflang&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The hreflang needed for the home page
&lt;/h2&gt;

&lt;p&gt;The home URLs for this blog are as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;English: https://sanghunkang.com/
Korean:  https://sanghunkang.com/ko/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wanted to put the following three links in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; of both pages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://sanghunkang.com/"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"ko"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://sanghunkang.com/ko/"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"x-default"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://sanghunkang.com/"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;en&lt;/code&gt; and &lt;code&gt;ko&lt;/code&gt; point to the home page for each language. &lt;code&gt;x-default&lt;/code&gt; is the default page to use when the user's language does not clearly match either one. Since this blog uses English as the default language, I set it to &lt;code&gt;/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;{{#is "home"}}&lt;/code&gt; did not solve it
&lt;/h2&gt;

&lt;p&gt;At first, I tried to check whether the current page was the home page in &lt;code&gt;default.hbs&lt;/code&gt; and then output &lt;code&gt;hreflang&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;is&lt;/span&gt; &lt;span class="s2"&gt;"home"&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    ...
&lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;is&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, in Ghost, &lt;code&gt;{{#is "home"}}&lt;/code&gt; is true only at the site root, &lt;code&gt;/&lt;/code&gt;. Since &lt;code&gt;/ko/&lt;/code&gt; is the first page of a Collection created with &lt;code&gt;routes.yaml&lt;/code&gt;, Ghost does not treat it as the site home.&lt;/p&gt;

&lt;p&gt;As a result, &lt;code&gt;hreflang&lt;/code&gt; appeared on the English home, but not on the Korean home.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling it with contentFor
&lt;/h2&gt;

&lt;p&gt;I changed it so that &lt;code&gt;index-en.hbs&lt;/code&gt; and &lt;code&gt;index-ko.hbs&lt;/code&gt;, which are used by each language home, pass the same &lt;code&gt;hreflang&lt;/code&gt; block, and &lt;code&gt;default.hbs&lt;/code&gt; outputs it in &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  index-en.hbs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{!&amp;lt; index}}&lt;/span&gt;
&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;contentFor&lt;/span&gt; &lt;span class="s2"&gt;"lang"&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;en&lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;contentFor&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;contentFor&lt;/span&gt; &lt;span class="s2"&gt;"hreflang-home"&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="na"&gt;@site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;/"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"ko"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="na"&gt;@site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;/ko/"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"x-default"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="na"&gt;@site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;/"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;contentFor&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  index-ko.hbs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{!&amp;lt; index}}&lt;/span&gt;
&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;contentFor&lt;/span&gt; &lt;span class="s2"&gt;"lang"&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;ko&lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;contentFor&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;contentFor&lt;/span&gt; &lt;span class="s2"&gt;"hreflang-home"&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="na"&gt;@site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;/"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"ko"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="na"&gt;@site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;/ko/"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"x-default"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="na"&gt;@site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;/"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;contentFor&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both templates pass the same set of URLs. Since &lt;code&gt;hreflang&lt;/code&gt; on multilingual pages must reference each other, both the English page and the Korean page should have the same list of language URLs.&lt;/p&gt;

&lt;h3&gt;
  
  
  default.hbs
&lt;/h3&gt;

&lt;p&gt;Output the passed block before &lt;code&gt;{{ghost_head}}&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="k"&gt;{{{&lt;/span&gt;&lt;span class="nv"&gt;block&lt;/span&gt; &lt;span class="s2"&gt;"hreflang-home"&lt;/span&gt;&lt;span class="k"&gt;}}}&lt;/span&gt;

&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;ghost_head&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach works regardless of whether Ghost treats &lt;code&gt;/ko/&lt;/code&gt; as the site home. If the page uses &lt;code&gt;index-ko.hbs&lt;/code&gt;, the &lt;code&gt;hreflang-home&lt;/code&gt; block is passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking the result
&lt;/h2&gt;

&lt;p&gt;Open the developer tools on the English and Korean home pages respectively, and run the code below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;link[rel="alternate"][hreflang]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hreflang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On both pages, all three entries should appear: &lt;code&gt;en&lt;/code&gt;, &lt;code&gt;ko&lt;/code&gt;, and &lt;code&gt;x-default&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmpv0zxe59afw19ujmdbm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmpv0zxe59afw19ujmdbm.png" alt="0.0.7 hreflang check result on the English home" width="799" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the English home &lt;code&gt;/&lt;/code&gt;, the three links &lt;code&gt;en&lt;/code&gt;, &lt;code&gt;ko&lt;/code&gt;, and &lt;code&gt;x-default&lt;/code&gt; were generated.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1zcn0o0bq92bgaybhlkh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1zcn0o0bq92bgaybhlkh.png" alt="0.0.7 hreflang check result on the Korean home" width="799" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>[App] Multiple Codex (ChatGPT) accounts on one machine</title>
      <dc:creator>Sanghun Kang</dc:creator>
      <pubDate>Mon, 05 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/sanghunkang/app-multiple-codex-chatgpt-accounts-on-one-machine-37k7</link>
      <guid>https://dev.to/sanghunkang/app-multiple-codex-chatgpt-accounts-on-one-machine-37k7</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdsctnxokro60dm3q099c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdsctnxokro60dm3q099c.png" alt="[App] Multiple Codex (ChatGPT) accounts on one machine" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fast track: throw this post's URL at your AI and tell it to set this up for you. (The app version is trickier because of signing, so double-check the result.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run two (or more) ChatGPT desktop apps, one per account. At the same time.&lt;/li&gt;
&lt;li&gt;Updates follow automatically, when you open the app.&lt;/li&gt;
&lt;li&gt;Caveat: &lt;strong&gt;passkey login won't work.&lt;/strong&gt; Log in with OTP, password, or SSO.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;~/.local/bin/chatgpt2-rebuild&lt;/code&gt;. (You need the original &lt;code&gt;ChatGPT.app&lt;/code&gt; installed, and &lt;code&gt;clang&lt;/code&gt; to compile the stub → &lt;code&gt;xcode-select --install&lt;/code&gt;.) The launcher synchronizes the sidebar state in both directions. It copies ChatGPT's state to ChatGPT2 before launch, then copies ChatGPT2's final state back to ChatGPT after ChatGPT2 exits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;SRC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/Applications/ChatGPT.app"&lt;/span&gt;
&lt;span class="nv"&gt;DST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/Applications/ChatGPT2.app"&lt;/span&gt;
&lt;span class="nv"&gt;NEW_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"com.openai.codex2"&lt;/span&gt;
&lt;span class="nv"&gt;NEW_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ChatGPT2"&lt;/span&gt;
&lt;span class="nv"&gt;CODEX_HOME2&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.codex2"&lt;/span&gt;
&lt;span class="nv"&gt;USERDATA2&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/Library/Application Support/Codex2"&lt;/span&gt;
&lt;span class="nv"&gt;P&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/libexec/PlistBuddy

&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SRC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ERROR: &lt;/span&gt;&lt;span class="nv"&gt;$SRC&lt;/span&gt;&lt;span class="s2"&gt; not found"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; clang &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ERROR: clang needed (xcode-select --install)"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# don't clobber a running instance&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;pgrep &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"ChatGPT2.app/Contents/MacOS/ChatGPT.bin"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ChatGPT2 is running — quit it first, then re-run."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;EXE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$P&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Print :CFBundleExecutable"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SRC&lt;/span&gt;&lt;span class="s2"&gt;/Contents/Info.plist"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[1/5] APFS clone of &lt;/span&gt;&lt;span class="nv"&gt;$SRC&lt;/span&gt;&lt;span class="s2"&gt; → &lt;/span&gt;&lt;span class="nv"&gt;$DST&lt;/span&gt;&lt;span class="s2"&gt; …"&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-Rc&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SRC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;                      &lt;span class="c"&gt;# clonefile: copy-on-write, ~0 extra disk&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[2/5] patch Info.plist…"&lt;/span&gt;
&lt;span class="nv"&gt;PL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DST&lt;/span&gt;&lt;span class="s2"&gt;/Contents/Info.plist"&lt;/span&gt;
&lt;span class="nv"&gt;$P&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Set :CFBundleIdentifier &lt;/span&gt;&lt;span class="nv"&gt;$NEW_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;$P&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Set :CFBundleName &lt;/span&gt;&lt;span class="nv"&gt;$NEW_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$P&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Add :CFBundleName string &lt;/span&gt;&lt;span class="nv"&gt;$NEW_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;$P&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Set :CFBundleDisplayName &lt;/span&gt;&lt;span class="nv"&gt;$NEW_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$P&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Add :CFBundleDisplayName string &lt;/span&gt;&lt;span class="nv"&gt;$NEW_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;$P&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Add :LSEnvironment:CODEX_HOME string &lt;/span&gt;&lt;span class="nv"&gt;$CODEX_HOME2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$P&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Set :LSEnvironment:CODEX_HOME &lt;/span&gt;&lt;span class="nv"&gt;$CODEX_HOME2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;$P&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Add :SUEnableAutomaticChecks bool false"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$P&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Set :SUEnableAutomaticChecks false"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[3/5] install lazy launcher (update-on-use script + tiny Mach-O stub; main exe → *.bin)…"&lt;/span&gt;
&lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DST&lt;/span&gt;&lt;span class="s2"&gt;/Contents/MacOS/&lt;/span&gt;&lt;span class="nv"&gt;$EXE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DST&lt;/span&gt;&lt;span class="s2"&gt;/Contents/MacOS/&lt;/span&gt;&lt;span class="nv"&gt;$EXE&lt;/span&gt;&lt;span class="s2"&gt;.bin"&lt;/span&gt;

&lt;span class="c"&gt;# (a) launch script — on every open: compare versions, re-clone + reopen if they differ&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DST&lt;/span&gt;&lt;span class="s2"&gt;/Contents/Resources/cg2-launch.sh"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;LAUNCH&lt;/span&gt;&lt;span class="sh"&gt;'
#!/bin/bash
set -u
SDIR="&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_SOURCE&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"   # …/ChatGPT2.app/Contents/Resources
APP="&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SDIR&lt;/span&gt;&lt;span class="s2"&gt;/../.."&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"                        # …/ChatGPT2.app
BIN="&lt;/span&gt;&lt;span class="nv"&gt;$SDIR&lt;/span&gt;&lt;span class="sh"&gt;/../MacOS/ChatGPT.bin"
SRC="/Applications/ChatGPT.app"
P=/usr/libexec/PlistBuddy
ver(){ "&lt;/span&gt;&lt;span class="nv"&gt;$P&lt;/span&gt;&lt;span class="sh"&gt;" -c "Print :CFBundleShortVersionString" "&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="sh"&gt;/Contents/Info.plist" 2&amp;gt;/dev/null; }
cur="&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ver &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SRC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"; mine="&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ver &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"
if [ -n "&lt;/span&gt;&lt;span class="nv"&gt;$cur&lt;/span&gt;&lt;span class="sh"&gt;" ] &amp;amp;&amp;amp; [ -n "&lt;/span&gt;&lt;span class="nv"&gt;$mine&lt;/span&gt;&lt;span class="sh"&gt;" ] &amp;amp;&amp;amp; [ "&lt;/span&gt;&lt;span class="nv"&gt;$cur&lt;/span&gt;&lt;span class="sh"&gt;" != "&lt;/span&gt;&lt;span class="nv"&gt;$mine&lt;/span&gt;&lt;span class="sh"&gt;" ]; then
  # original updated → re-clone in a detached helper, then reopen (this launch is handed off)
  /usr/bin/nohup /bin/bash -c '
    "&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="sh"&gt;/.local/bin/chatgpt2-rebuild" &amp;gt;/tmp/chatgpt2-autoupdate.log 2&amp;gt;&amp;amp;1 &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="sh"&gt;
      &amp;amp;&amp;amp; /usr/bin/env -u CODEX_HOME -u ORCA_CODEX_HOME open -a /Applications/ChatGPT2.app
  ' &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;
  exit 0
fi
# up-to-date → normal launch of account-2 instance
export CODEX_HOME="&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="sh"&gt;/.codex2"

PRIMARY_STATE="&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="sh"&gt;/.codex/.codex-global-state.json"
SECONDARY_STATE="&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="sh"&gt;/.codex2/.codex-global-state.json"

sync_state() {
  local from="&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="sh"&gt;"
  local to="&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="sh"&gt;"
  local tmp="&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;.sync.&lt;/span&gt;&lt;span class="nv"&gt;$$&lt;/span&gt;&lt;span class="sh"&gt;"

  [ -f "&lt;/span&gt;&lt;span class="nv"&gt;$from&lt;/span&gt;&lt;span class="sh"&gt;" ] || return 0

  if /bin/cp -p "&lt;/span&gt;&lt;span class="nv"&gt;$from&lt;/span&gt;&lt;span class="sh"&gt;" "&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="sh"&gt;"; then
    /bin/mv -f "&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="sh"&gt;" "&lt;/span&gt;&lt;span class="nv"&gt;$to&lt;/span&gt;&lt;span class="sh"&gt;"
  else
    /bin/rm -f "&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="sh"&gt;"
    return 1
  fi
}

# Keep the sidebar up to date: ChatGPT → ChatGPT2 before launch.
sync_state "&lt;/span&gt;&lt;span class="nv"&gt;$PRIMARY_STATE&lt;/span&gt;&lt;span class="sh"&gt;" "&lt;/span&gt;&lt;span class="nv"&gt;$SECONDARY_STATE&lt;/span&gt;&lt;span class="sh"&gt;" || &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="sh"&gt;
  echo "WARNING: could not import ChatGPT sidebar state" &amp;gt;&amp;amp;2

# Keep the sidebar up to date: ChatGPT2 → ChatGPT after normal exit.
trap 'sync_state "&lt;/span&gt;&lt;span class="nv"&gt;$SECONDARY_STATE&lt;/span&gt;&lt;span class="sh"&gt;" "&lt;/span&gt;&lt;span class="nv"&gt;$PRIMARY_STATE&lt;/span&gt;&lt;span class="sh"&gt;" || true' EXIT

"&lt;/span&gt;&lt;span class="nv"&gt;$BIN&lt;/span&gt;&lt;span class="sh"&gt;" "--user-data-dir=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="sh"&gt;/Library/Application Support/Codex2" "&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="sh"&gt;"
status=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="sh"&gt;
exit "&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="sh"&gt;"
&lt;/span&gt;&lt;span class="no"&gt;LAUNCH
&lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DST&lt;/span&gt;&lt;span class="s2"&gt;/Contents/Resources/cg2-launch.sh"&lt;/span&gt;

&lt;span class="c"&gt;# (b) Mach-O stub — launchd won't run a script as a bundle's main executable (POSIX 162),&lt;/span&gt;
&lt;span class="c"&gt;#     so this tiny stub just hands off to the script above.&lt;/span&gt;
&lt;span class="nv"&gt;SRC_C&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt; /tmp/cg2launcher.XXXX.c&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SRC_C&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;limits.h&amp;gt;
#include &amp;lt;libgen.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;mach-o/dyld.h&amp;gt;
int main(int argc, char **argv){
    char exe[PATH_MAX]; uint32_t sz=sizeof(exe);
    if(_NSGetExecutablePath(exe,&amp;amp;sz)!=0) return 71;
    char dir[PATH_MAX]; strncpy(dir,dirname(exe),sizeof(dir));
    char sh[PATH_MAX];
    snprintf(sh,sizeof(sh),"%s/../Resources/cg2-launch.sh",dir);
    char **n=calloc(argc+3,sizeof(char*)); int k=0;
    n[k++]="/bin/bash"; n[k++]=sh;
    for(int i=1;i&amp;lt;argc;i++) n[k++]=argv[i];
    n[k]=NULL;
    execv("/bin/bash",n); perror("execv"); return 72;
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;clang &lt;span class="nt"&gt;-O2&lt;/span&gt; &lt;span class="nt"&gt;-arch&lt;/span&gt; arm64 &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DST&lt;/span&gt;&lt;span class="s2"&gt;/Contents/MacOS/&lt;/span&gt;&lt;span class="nv"&gt;$EXE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SRC_C&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SRC_C&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[4/5] sign — DEEP ad-hoc reseal of the whole clone…"&lt;/span&gt;
codesign &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;--deep&lt;/span&gt; &lt;span class="nt"&gt;--sign&lt;/span&gt; - &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[5/5] clear quarantine + register…"&lt;/span&gt;
xattr &lt;span class="nt"&gt;-dr&lt;/span&gt; com.apple.quarantine &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USERDATA2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Done (APFS clone)."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This assumes that the two apps are used one at a time. A normal quit is required for the final ChatGPT2 → ChatGPT sync. A force-quit, crash, or sudden shutdown may skip that final copy.&lt;/p&gt;

&lt;p&gt;Then run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pkill &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"ChatGPT2.app"&lt;/span&gt; 2&amp;gt;/dev/null   &lt;span class="c"&gt;# quit it if running&lt;/span&gt;
chatgpt2-rebuild                       &lt;span class="c"&gt;# ~7s, creates /Applications/ChatGPT2.app&lt;/span&gt;
&lt;span class="nb"&gt;env&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; CODEX_HOME &lt;span class="nt"&gt;-u&lt;/span&gt; ORCA_CODEX_HOME open &lt;span class="nt"&gt;-a&lt;/span&gt; /Applications/ChatGPT2.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On first launch, log in to account 2. (Use OTP/password/SSO, not passkey — see the caveat below.)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: if you &lt;code&gt;open&lt;/code&gt; from a shell that exports &lt;code&gt;CODEX_HOME&lt;/code&gt; (e.g. Orca), that value leaks into the app and crosses up your accounts. Open with &lt;code&gt;env -u&lt;/code&gt; as shown above.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why it works
&lt;/h2&gt;

&lt;p&gt;The app's "identity" is its bundle id + signature. After cloning, we change the id to &lt;code&gt;com.openai.codex2&lt;/code&gt;, and the launcher injects &lt;code&gt;CODEX_HOME=~/.codex2&lt;/code&gt; + &lt;code&gt;--user-data-dir=…/Codex2&lt;/code&gt;. That splits the account (&lt;code&gt;auth.json&lt;/code&gt;), the web login, and the Electron single-instance lock, so both run at the same time. (&lt;code&gt;--user-data-dir&lt;/code&gt; is the key: Electron's lock is keyed by the user-data-dir, not the bundle id, so changing the id alone won't let two run.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why clone, not symlink?&lt;/strong&gt; If you symlink the frameworks/binary back to the original, the Chromium helper (&lt;code&gt;Codex (Service)&lt;/code&gt;) crashes the instant it starts, because its sandbox / framework path resolution needs the real bundle path and a symlink throws it off. The main process then retries the helper forever, pinned at 100% CPU. So "symlink to auto-follow updates" is impossible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;--deep&lt;/code&gt; re-sign?&lt;/strong&gt; The inner frameworks/helpers carry OpenAI's team signature (hardened runtime). If only the top bundle is ad-hoc, the team mismatch makes AMFI reject the launch outright (&lt;code&gt;-420&lt;/code&gt;). So the whole clone must be re-signed ad-hoc to unify the identity, and then the helper starts. Since the clone is an independent copy, &lt;code&gt;--deep&lt;/code&gt; never touches the original. (Conversely, never &lt;code&gt;--deep&lt;/code&gt;-sign while the frameworks are symlinks — it would re-sign the original through the links.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Updates: automatic, on open
&lt;/h2&gt;

&lt;p&gt;The clone is a snapshot from copy time, so it doesn't auto-update on its own. Instead, when you open the app, the launcher compares its version to the original and, if they differ, re-clones automatically (~7s) and relaunches. The original updates as usual via Sparkle, and ChatGPT2 rides on top when you open it. No background daemon, no polling. (Letting ChatGPT2 self-update via Sparkle would be rejected for a signature mismatch, or would wipe the customization, so its self-update is disabled.)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you run the updater in ChatGPT2, the update fails with a signature mismatch, as shown in the screenshot below.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxhcb4rc71c1yerawnmj2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxhcb4rc71c1yerawnmj2.jpg" alt="The signature error shown when attempting to update ChatGPT2" width="636" height="564"&gt;&lt;/a&gt;&lt;em&gt;The signature error shown when attempting to update ChatGPT2&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When the original app is updated, ChatGPT2 is updated automatically as well, so there is no need to try to update the copied app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveat: passkey doesn't work
&lt;/h2&gt;

&lt;p&gt;The moment you re-sign, the app loses its Team ID and keychain access. Passkeys live in the iCloud Keychain / Secure Enclave, and access is gated by exactly that, so in ChatGPT2 the "Verify your identity → passkey" screen spins forever. It can't be fixed (that would need OpenAI's real signing certificate, which we don't have). Use &lt;strong&gt;"Try another method"&lt;/strong&gt; with &lt;strong&gt;OTP, password, or Google/Apple/MS SSO&lt;/strong&gt; instead. The session is stored as a cookie, so it persists across restarts.&lt;/p&gt;

&lt;p&gt;Part 1 (CLI) is here: &lt;a href="https://sanghunkang.com/codex-cli-dual-account-en/" rel="noopener noreferrer"&gt;Multiple Codex (ChatGPT) accounts on one machine&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://sanghunkang.com/codex-app-dual-account-en/" rel="noopener noreferrer"&gt;Sanghun Kang’s blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nocode</category>
    </item>
    <item>
      <title>Multilingual Ghost CMS Blog: 6. Fixing Invisible Post Excerpts on Tag Pages</title>
      <dc:creator>Sanghun Kang</dc:creator>
      <pubDate>Sun, 04 Oct 2026 17:59:50 +0000</pubDate>
      <link>https://dev.to/sanghunkang/multilingual-ghost-cms-blog-6-fixing-invisible-post-excerpts-on-tag-pages-5djh</link>
      <guid>https://dev.to/sanghunkang/multilingual-ghost-cms-blog-6-fixing-invisible-post-excerpts-on-tag-pages-5djh</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffi3acpzvwpn9jjt8kz19.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffi3acpzvwpn9jjt8kz19.png" alt="Multilingual Ghost CMS Blog: 6. Fixing Invisible Post Excerpts on Tag Pages" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the previous version, I also applied multilingual filtering to the Tag page. It shows only the post cards that match the language selected in the Language selector, and I also added an opacity transition so the cards fade in smoothly.&lt;/p&gt;

&lt;p&gt;But when I actually used it, there was a problem: the post card titles and images were visible, but the post excerpts remained transparent.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3mpopfk18h6uz93egy46.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3mpopfk18h6uz93egy46.png" alt="0.0.5 On the Tag page, the title is visible but the excerpt remains transparent" width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The card title and image are visible, but the excerpt does not appear.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sanghunka/ghost-multilingual-theme/commit/ba6ef6b?ref=sanghunkang.com" rel="noopener noreferrer"&gt;0.0.6 · Fix Tag page excerpt fade-in&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause
&lt;/h2&gt;

&lt;p&gt;On the Tag page, all post cards are initially made transparent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.post-card-excerpt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="nc"&gt;.post-card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;opacity&lt;/span&gt; &lt;span class="m"&gt;0.5s&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then only the cards that match the selected language are changed to &lt;code&gt;opacity: 1&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The issue is that &lt;code&gt;.post-card-excerpt&lt;/code&gt; also has &lt;code&gt;opacity: 0&lt;/code&gt; applied separately. Even if the parent &lt;code&gt;card&lt;/code&gt; is changed to be visible, the child &lt;code&gt;.post-card-excerpt&lt;/code&gt; still has an opacity of 0.&lt;/p&gt;

&lt;p&gt;In other words, both the entire card and the excerpt were made transparent, but only the card was made visible again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating tag.hbs
&lt;/h2&gt;

&lt;p&gt;When displaying a card, also find the &lt;code&gt;.post-card-excerpt&lt;/code&gt; inside that card and change it to &lt;code&gt;opacity: 1&lt;/code&gt; as well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;excerpts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.post-card-excerpt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;excerpts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;excerpt&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;excerpt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;card.style.opacity = 1&lt;/code&gt;: Shows the entire card.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;excerpt.style.opacity = 1&lt;/code&gt;: Shows the post excerpt inside the card.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Handle both inside the same &lt;code&gt;setTimeout&lt;/code&gt; so the card and excerpt appear together.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;Now, when I change the Language selector on the Tag page, the post cards and excerpts for the selected language appear together.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F93sjtrddnx7rwpbuq83m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F93sjtrddnx7rwpbuq83m.png" alt="0.0.6 On the Tag page, both the title and excerpt are visible together" width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I applied theme version 0.0.6 while keeping the same content and language. When the cards appear, the excerpt opacity is restored as well, so both the title and the post summary are visible.&lt;/p&gt;

&lt;p&gt;This was not a problem with the multilingual handling itself. It was a small UI bug caused by applying opacity separately to the parent and child in CSS, while restoring only the parent in JavaScript.&lt;/p&gt;

</description>
      <category>css</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>[CLI] Multiple Codex (ChatGPT) accounts on one machine</title>
      <dc:creator>Sanghun Kang</dc:creator>
      <pubDate>Sun, 04 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/sanghunkang/cli-multiple-codex-chatgpt-accounts-on-one-machine-17ef</link>
      <guid>https://dev.to/sanghunkang/cli-multiple-codex-chatgpt-accounts-on-one-machine-17ef</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ldb6ktg1xgvgq2nimy6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ldb6ktg1xgvgq2nimy6.png" alt="[CLI] Multiple Codex (ChatGPT) accounts on one machine" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ran out of usage, logged out, logged into the other account, got tired of it, so I automated it.&lt;/p&gt;

&lt;p&gt;Fast track: throw this post's URL at your AI and tell it to set this up for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make several codex CLIs (two, in this post).&lt;/li&gt;
&lt;li&gt;Share everything. Chat history, memory, skills, all of it stays as one. (It literally is one, thanks to symlinks.)&lt;/li&gt;
&lt;li&gt;Only the login differs.&lt;/li&gt;
&lt;li&gt;This way you can use two or more accounts. For simultaneous use, see the database-locking caveat below.&lt;/li&gt;
&lt;li&gt;CLI only. Running two or more desktop apps is covered in the next post.&lt;/li&gt;
&lt;li&gt;The same approach works for Claude Code too, not just Codex.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How
&lt;/h2&gt;

&lt;p&gt;Call the existing command &lt;code&gt;codex&lt;/code&gt;, and the new one &lt;code&gt;codex2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1) alias&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.zshrc&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;codex2&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'CODEX_HOME="$HOME/.codex2" codex'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first time you run &lt;code&gt;codex2&lt;/code&gt; and log in, &lt;code&gt;~/.codex2/auth.json&lt;/code&gt; is created and you're good to go.&lt;/p&gt;

&lt;p&gt;The catch: now &lt;code&gt;codex&lt;/code&gt; and &lt;code&gt;codex2&lt;/code&gt; keep separate chat history, memory, skills, and so on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2) symlink setup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Make &lt;code&gt;~/.codex2&lt;/code&gt; a selective mirror of &lt;code&gt;~/.codex&lt;/code&gt;. Keep only the account file (&lt;code&gt;auth.json&lt;/code&gt;) and the volatile runtime files separate; symlink everything else so it's shared. Save this as &lt;code&gt;~/.local/bin/codex2-sync&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;SRC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.codex"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;DST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.codex2"&lt;/span&gt;

&lt;span class="c"&gt;# Kept separate (not shared): the account + volatile runtime&lt;/span&gt;
&lt;span class="nv"&gt;EXCLUDE&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;auth.json ipc tmp .tmp cache log mcp-oauth-locks
         process_manager node_repl shell_snapshots computer-use
         .DS_Store .stfolder .stignore&lt;span class="o"&gt;)&lt;/span&gt;
excluded&lt;span class="o"&gt;(){&lt;/span&gt; &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;e &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;EXCLUDE&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;path &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SRC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SRC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/.&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"."&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;".."&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
  &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
  &lt;/span&gt;excluded &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;continue
  &lt;/span&gt;&lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DST&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;readlink&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi
done&lt;/span&gt;
&lt;span class="c"&gt;# Prune dangling links whose source is gone&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="nb"&gt;link &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/.&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$link&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$link&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$link&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Synced &lt;/span&gt;&lt;span class="nv"&gt;$DST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x ~/.local/bin/codex2-sync &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; codex2-sync
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can run &lt;code&gt;codex&lt;/code&gt; (the original) and &lt;code&gt;codex2&lt;/code&gt; (the new one) as separate commands. The limitations of simultaneous use are explained below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it works
&lt;/h2&gt;

&lt;p&gt;All of codex's state lives under &lt;code&gt;CODEX_HOME&lt;/code&gt; (default &lt;code&gt;~/.codex&lt;/code&gt;), and the "account" is really just &lt;code&gt;auth.json&lt;/code&gt;. So splitting &lt;code&gt;auth.json&lt;/code&gt; alone gives you a separate account.&lt;/p&gt;

&lt;p&gt;Both accounts use the same SQLite databases to share conversation history. WAL supports concurrent access, but Codex's lock handling can cause startup failures or temporary stalls when multiple instances run at the same time.&lt;/p&gt;

&lt;p&gt;I switch to the other account when one account runs out of usage. I do not run both instances at the same time, and I have not encountered these issues in my day-to-day use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveat: the desktop app is a different story
&lt;/h2&gt;

&lt;p&gt;That's all it takes for the CLI. The ChatGPT desktop app, though, is a bit more involved. &lt;a href="https://sanghunkang.com/codex-app-dual-account-en/" rel="noopener noreferrer"&gt;Next post.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://sanghunkang.com/codex-cli-dual-account-en/" rel="noopener noreferrer"&gt;Sanghun Kang’s blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cli</category>
    </item>
    <item>
      <title>Multilingual Ghost CMS Blog: 5. Making Home Links Language Aware</title>
      <dc:creator>Sanghun Kang</dc:creator>
      <pubDate>Sat, 03 Oct 2026 02:02:55 +0000</pubDate>
      <link>https://dev.to/sanghunkang/multilingual-ghost-cms-blog-5-making-home-links-language-aware-1di9</link>
      <guid>https://dev.to/sanghunkang/multilingual-ghost-cms-blog-5-making-home-links-language-aware-1di9</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffi3acpzvwpn9jjt8kz19.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffi3acpzvwpn9jjt8kz19.png" alt="Multilingual Ghost CMS Blog: 5. Making Home Links Language Aware" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This explains how to make the home links in the navigation bar, footer, and error page point to the correct language-specific homepage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frcgge00vpnfxp40ehrhw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frcgge00vpnfxp40ehrhw.png" alt="Home link in the navigation bar" width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Home link in the navigation bar&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy22ppmfluqb3mdy4aaxe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy22ppmfluqb3mdy4aaxe.png" alt="Home link in the footer" width="798" height="168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Home link in the footer&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb4s0dk8lkbwl6kksaw0o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb4s0dk8lkbwl6kksaw0o.png" alt="Home link on the error page" width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Home link on the error page&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There are home links in the navigation bar, footer, and error page. These links are set to &lt;code&gt;@site.url&lt;/code&gt;, and regardless of the multilingual settings, they only point to the main page of the default language. Let's modify them so they point to the currently active language instead.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0xevnc1snr7omxu68tx9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0xevnc1snr7omxu68tx9.png" alt="default.hbs" width="800" height="238"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;default.hbs&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"gh-head-logo&lt;/span&gt;&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;unless&lt;/span&gt; &lt;span class="na"&gt;@site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;logo&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="s"&gt; no-image&lt;/span&gt;&lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;unless&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="k"&gt;{{#if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;block&lt;/span&gt; &lt;span class="s2"&gt;"lang"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;}}{{&lt;/span&gt;&lt;span class="na"&gt;@site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;block&lt;/span&gt; &lt;span class="s2"&gt;"lang"&lt;/span&gt;&lt;span class="k"&gt;}}{{else}}{{&lt;/span&gt;&lt;span class="na"&gt;@site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="k"&gt;}}{{/if}}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In default.hbs, it works properly if you handle it by using the block "lang" in an if condition, the same way as before.&lt;br&gt;&lt;br&gt;
However, this approach does not work properly on the error page or tag page. That is because these pages do not include index-ko or index-en, and they also do not explicitly assign "lang" values like post or page do. So I decided not to rely on lang.&lt;/p&gt;

&lt;p&gt;The new idea is to change the approach so that, as in tag.hbs, it retrieves the selectedLanguage value stored in localStorage and updates the URL based on that value.&lt;/p&gt;

&lt;p&gt;Since this needs to be done in default, tag, and error as well, I wanted to create a separate file and reuse it. I made separate functions for these two tasks and stored them in a separate util.js file so they could be reused across multiple files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getSelectedLanguage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;selectedLanguage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;siteUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;href&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;siteUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;siteUrl&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;href&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;siteUrl&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;assets/js/util.js&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Using these functions, let's update all files that contain &lt;code&gt;@site.url&lt;/code&gt; links.&lt;/p&gt;

&lt;p&gt;You can load the newly created js file using the {{asset}} helper.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;script type="text/javascript" src="{{asset 'js/util.js'}}"&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;br&gt;&lt;br&gt;
Reference:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ghost.org/docs/themes/helpers/asset/?ref=sanghunkang.com" rel="noopener noreferrer"&gt;Ghost Handlebars Theme Helpers: asset&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Full code
&lt;/h2&gt;

&lt;p&gt;You can check the full code here on GitHub.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sanghunka/ghost-multilingual-theme/commit/59b6cc146fbb63f1a6ae0f3a50be37c00780acf4?ref=sanghunkang.com" rel="noopener noreferrer"&gt;0.0.5 · sanghunka/ghost-multilingual-theme@59b6cc1&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  error-404.hbs
&lt;/h2&gt;

&lt;p&gt;Let's add this code at the very bottom.&lt;/p&gt;

&lt;p&gt;It's simple. It gets selectedLanguage and changes the href of .error-link to match it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text/javascript"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"{{asset 'js/util.js'}}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMContentLoaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selectedLanguage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSelectedLanguage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectedLanguage&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;updateLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.error-link&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedLanguage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try entering any URL (e.g. {host}/ko/404error) and check whether the home link correctly points to {host}/ko/.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn0ro7mcizpkti3zq5fwf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn0ro7mcizpkti3zq5fwf.png" alt="ko" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;ko&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz8tntiu6fuds811medx8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz8tntiu6fuds811medx8.png" alt="en" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;en&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  error.hbs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/javascript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{{asset 'js/util.js'}}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMContentLoaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selectedLanguage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSelectedLanguage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectedLanguage&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;updateLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.error-link&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedLanguage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;updateLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.site-nav-logo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedLanguage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  tag.hbs
&lt;/h2&gt;

&lt;p&gt;I replaced this code, which I had implemented before,&lt;/p&gt;

&lt;p&gt;&lt;code&gt;const selectedLanguage = localStorage.getItem('selectedLanguage') || 'en';&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;with the newly created function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/javascript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{{asset 'js/util.js'}}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selectedLanguage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSelectedLanguage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  default.hbs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/javascript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{{asset 'js/util.js'}}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;initializeLanguageSelector&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;selector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;languageSelector&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;currentUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;currentUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tag/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;urlLanguage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ko/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ko&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;selectedLanguage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;urlLanguage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;urlLanguage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;selectedLanguage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;selectedLanguage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;currentUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tag/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ko&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ko/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMContentLoaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;initializeLanguageSelector&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selectedLanguage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSelectedLanguage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectedLanguage&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;updateLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.gh-head-logo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedLanguage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;updateLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;section.copyright a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedLanguage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;updateLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;li.nav-home a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedLanguage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;With this approach, it would even be possible to completely replace the block "lang" approach. Even so, I decided to keep the block "lang" approach as is, because from an SEO perspective it has the advantage that the html lang attribute is rendered server-side, and because I do not think this part is all that important.&lt;/p&gt;

</description>
      <category>webdev</category>
    </item>
    <item>
      <title>Hostname Is Not a Device Identifier</title>
      <dc:creator>Sanghun Kang</dc:creator>
      <pubDate>Sat, 03 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/sanghunkang/hostname-is-not-a-device-identifier-2cka</link>
      <guid>https://dev.to/sanghunkang/hostname-is-not-a-device-identifier-2cka</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk6htfucgo56u7ktj8gel.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk6htfucgo56u7ktj8gel.png" alt="Hostname Is Not a Device Identifier" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Did I really use that many tokens?
&lt;/h2&gt;

&lt;p&gt;I was looking at the Claude Code status line as usual when one number looked wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Last 30 Days            $4038.03 · 5.1B tokens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two days earlier it had been 2.5B. There was no way I had used another 3B tokens in between. If the number had doubled, something was being counted twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The aggregation setup
&lt;/h2&gt;

&lt;p&gt;My &lt;code&gt;~/.claude&lt;/code&gt; directory is synced between a MacBook and a Mac mini through Syncthing. The token usage summary sits on top of that with a simple structure. Each machine writes only to a JSON file named after its own hostname, and the status line sums every JSON file in the directory with a &lt;code&gt;*.json&lt;/code&gt; glob.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;USAGE_DIR="$HOME/.claude/usage-summary"
HOSTNAME_SHORT="$(hostname -s | tr -d '\n\r' | tr -c 'a-zA-Z0-9.-' '_')"
CACHE_FILE="$USAGE_DIR/$HOSTNAME_SHORT.json"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each machine owns its own file. Aggregation happens at the directory level. It seemed simple enough.&lt;/p&gt;

&lt;p&gt;Then I opened the directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mac.json
mini.json
Sanghuns-MacbookPro-M4Max.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three files. But there were only two machines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catching the duplicate with data
&lt;/h2&gt;

&lt;p&gt;The first question was, "Are two of these files coming from the same machine?" &lt;code&gt;ccusage&lt;/code&gt; reads the local &lt;code&gt;~/.claude/projects/**/*.jsonl&lt;/code&gt; session logs on the machine where it runs, so if two files came from the same machine, they could only be two snapshots of the same history at different points in time.&lt;/p&gt;

&lt;p&gt;There were two checks.&lt;/p&gt;

&lt;p&gt;First, the scale of the 30-day totals.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;last30 tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Mac.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2,531,953,934&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Sanghuns-MacbookPro-M4Max.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2,532,361,686&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mini.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;65,207,390&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both &lt;code&gt;Mac.json&lt;/code&gt; and &lt;code&gt;Sanghuns-MacbookPro-M4Max.json&lt;/code&gt; were around 2.5B. &lt;code&gt;mini.json&lt;/code&gt; was 65M. It was not plausible that the Mac mini had suddenly used 40 times more tokens and landed at 2.5B. By scale alone, the two large files were almost certainly from the MacBook.&lt;/p&gt;

&lt;p&gt;Second, exact daily equality. The &lt;code&gt;today&lt;/code&gt; value in &lt;code&gt;Mac.json&lt;/code&gt; for 2026-04-20 and the &lt;code&gt;yesterday&lt;/code&gt; value in &lt;code&gt;Sanghuns-MacbookPro-M4Max.json&lt;/code&gt; for the same date were identical, down to token counts, cost, and model breakdown. Two different machines do not accidentally produce the same breakdown at that level. The data came from the same projects directory.&lt;/p&gt;

&lt;p&gt;So one MacBook was writing two files. The reason was now obvious: the filename depended on hostname, and the hostname was switching between two values.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first fix failed
&lt;/h2&gt;

&lt;p&gt;I deleted &lt;code&gt;Mac.json&lt;/code&gt; as a stale file. That evening the status line was back at 5.1B. &lt;code&gt;ls&lt;/code&gt; showed that &lt;code&gt;Mac.json&lt;/code&gt; had been created again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ hostname -s
Mac
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few hours earlier the same command had returned &lt;code&gt;Sanghuns-MacbookPro-M4Max&lt;/code&gt;. I had not switched machines or changed anything special about the network.&lt;/p&gt;

&lt;p&gt;The idea that &lt;code&gt;hostname&lt;/code&gt; could wobble like that ran against my intuition, so I checked it several times. The Mac mini always returned &lt;code&gt;mini&lt;/code&gt;. Only the MacBook moved between names.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three macOS names
&lt;/h2&gt;

&lt;p&gt;macOS has three names.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ComputerName&lt;/code&gt;: the human-facing name shown in the GUI, such as &lt;code&gt;Sanghun's MacbookPro M4Max&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LocalHostName&lt;/code&gt;: the Bonjour name, derived from &lt;code&gt;ComputerName&lt;/code&gt; and ASCII-normalized, such as &lt;code&gt;Sanghuns-MacbookPro-M4Max&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HostName&lt;/code&gt;: the RFC 1178 Unix hostname. It exists only if explicitly set. Otherwise it is empty.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ scutil --get ComputerName
Sanghun's MacbookPro M4Max
$ scutil --get LocalHostName
Sanghuns-MacbookPro-M4Max
$ scutil --get HostName
HostName: not set
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;HostName&lt;/code&gt; is set, the &lt;code&gt;hostname&lt;/code&gt; command uses it. If it is not set, the command falls back depending on context. The fallback may be derived from &lt;code&gt;LocalHostName&lt;/code&gt;, it may come from a hostname supplied by DHCP, or it may come from some other network stack default. On my MacBook, &lt;code&gt;Mac&lt;/code&gt; was that default fallback. Depending on the network path, the command returned either &lt;code&gt;Mac&lt;/code&gt; or &lt;code&gt;Sanghuns-MacbookPro-M4Max&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Mac mini, on the other hand, had &lt;code&gt;HostName&lt;/code&gt; explicitly set to &lt;code&gt;mini&lt;/code&gt;. No fallback was needed, so it stayed stable.&lt;/p&gt;

&lt;p&gt;In short, &lt;code&gt;hostname -s&lt;/code&gt; is not a reliable way to answer "which machine is this running on?" unless the machine has been configured in a way that makes it reliable. On a machine without &lt;code&gt;HostName&lt;/code&gt;, it can drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing alternatives
&lt;/h2&gt;

&lt;p&gt;There were two broad fixes. One was to stabilize hostname. The other was to stop using hostname and pick a more stable device identifier.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Upside&lt;/th&gt;
&lt;th&gt;Downside&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sudo scutil --set HostName ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fixes the root cause. The whole system benefits.&lt;/td&gt;
&lt;td&gt;Requires sudo once. Some IT tooling may overwrite it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sysctl -n hw.model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kernel sysctl, stable, only a couple of script lines.&lt;/td&gt;
&lt;td&gt;Less readable filenames, such as &lt;code&gt;Mac16,5.json&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ioreg&lt;/code&gt; IOPlatformUUID&lt;/td&gt;
&lt;td&gt;Truly unique.&lt;/td&gt;
&lt;td&gt;UUID filenames are ugly.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environment variable override&lt;/td&gt;
&lt;td&gt;Human-chosen names.&lt;/td&gt;
&lt;td&gt;Manual setup on every machine.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-device mapping table&lt;/td&gt;
&lt;td&gt;Pretty names.&lt;/td&gt;
&lt;td&gt;The table has to be maintained as devices change.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I chose &lt;code&gt;sysctl -n hw.model&lt;/code&gt; for two reasons. First, the change was three lines in &lt;code&gt;ccusage-cache.sh&lt;/code&gt;. Second, it did not require a system-wide change or &lt;code&gt;sudo&lt;/code&gt;. A collision would require me to own two Macs with exactly the same model identifier and run Claude Code on both. That is not true now, and it is a rare future case.&lt;/p&gt;

&lt;h2&gt;
  
  
  The change
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;~/.claude/ccusage-cache.sh&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Before
HOSTNAME_SHORT="$(hostname -s 2&amp;gt;/dev/null | tr -d '\n\r' | tr -c 'a-zA-Z0-9.-' '_')"
[ -z "$HOSTNAME_SHORT" ] &amp;amp;&amp;amp; HOSTNAME_SHORT="unknown"
CACHE_FILE="$USAGE_DIR/$HOSTNAME_SHORT.json"

# After
HOST_ID="$(sysctl -n hw.model 2&amp;gt;/dev/null | tr -d '\n\r')"
[ -z "$HOST_ID" ] &amp;amp;&amp;amp; HOST_ID="unknown"
CACHE_FILE="$USAGE_DIR/$HOST_ID.json"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;statusline-command.sh&lt;/code&gt; did not need any changes. Its aggregation logic already reads every &lt;code&gt;*.json&lt;/code&gt; file in the directory, so the filename rule can change without touching the summing logic.&lt;/p&gt;

&lt;p&gt;After deleting the stale file and running the script once, the MacBook produced the new file. Because &lt;code&gt;~/.claude&lt;/code&gt; is synced through Syncthing, the script change also reached the Mac mini automatically. On the Mac mini I only had to delete the old &lt;code&gt;mini.json&lt;/code&gt; and run the script once.&lt;/p&gt;

&lt;p&gt;The final state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mac16,5.json    # MacBook  (last30 = 2.54B)
Mac16,11.json   # Mac mini (last30 = 66M)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Total: 2.6B. The number I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side note: Apple changed model naming starting with M4
&lt;/h2&gt;

&lt;p&gt;When I first saw &lt;code&gt;Mac16,11&lt;/code&gt;, I was not sure it was the Mac mini. I was used to model identifiers that started with the product family, such as &lt;code&gt;Macmini&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It turns out Apple removed the product-family prefix from model identifiers starting with the M4 generation. Older identifiers were readable, like &lt;code&gt;Macmini9,1&lt;/code&gt; or &lt;code&gt;MacBookPro18,3&lt;/code&gt;. Now they are all &lt;code&gt;MacN,X&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Mac16,5&lt;/code&gt; = MacBook Pro 14-inch M4 Max&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Mac16,11&lt;/code&gt; = Mac mini M4 Pro&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You cannot tell which file belongs to which machine just from the filename. Readability went down, but the trustworthiness of &lt;code&gt;hw.model&lt;/code&gt; as a stable identifier went up. The old product-family prefix helped humans understand the category. Apple giving that up may also be a signal that product boundaries inside a generation are getting blurrier. Mac Studio overlaps with Mac Pro. Mac mini overlaps with Mac Studio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Do not use &lt;code&gt;hostname -s&lt;/code&gt; as a device identifier. On a Mac where &lt;code&gt;HostName&lt;/code&gt; is not set, it can move between DHCP and &lt;code&gt;LocalHostName&lt;/code&gt; fallbacks.&lt;/p&gt;

&lt;p&gt;For this script, &lt;code&gt;sysctl -n hw.model&lt;/code&gt; was the lightest replacement. No sudo, just a few lines of shell. The only cost is less readable filenames.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://sanghunkang.com/hostname-is-not-a-device-identifier-en/" rel="noopener noreferrer"&gt;Sanghun Kang’s blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>automation</category>
    </item>
    <item>
      <title>Codex</title>
      <dc:creator>Sanghun Kang</dc:creator>
      <pubDate>Fri, 02 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/sanghunkang/codex-4db7</link>
      <guid>https://dev.to/sanghunkang/codex-4db7</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6nkxis27kpv0u27jk9q8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6nkxis27kpv0u27jk9q8.png" alt="Codex" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I don't write code.&lt;/p&gt;

&lt;p&gt;Instead I tinker with a file called AGENTS.md. Instructions for an agent. When the agent makes a mistake, I add a line so it never makes that mistake again.&lt;/p&gt;

&lt;p&gt;That file is what Codex reads.&lt;/p&gt;

&lt;p&gt;The word codex comes from Latin caudex. A block of wood. Romans coated wooden tablets in wax and scratched words into them. Bound several together as a record. They called it a codex. When wood gave way to parchment, the name stayed.&lt;/p&gt;

&lt;p&gt;In 529, Emperor Justinian compiled every decree scattered across the Roman Empire into one system. Not individual laws, but a law to govern laws. Every edict in one volume, so you only had to look in one place. They called it Codex Justinianus, a legal code.&lt;/p&gt;

&lt;p&gt;In 2026, I'm building my own Codex Justinianus. Rules, lessons from failures, memory, all in one file so the agent only has to look in one place.&lt;/p&gt;

&lt;p&gt;OpenAI shared a number on their blog this February. Three engineers. Five months. A million lines of production code. Lines written by humans: zero. What they wrote wasn't code. It was codex. Markdown wrapped around the agent. They call it harness engineering.&lt;/p&gt;

&lt;p&gt;To use codex (the app), you first write your own codex (legal code). Not on parchment. On markdown. This codex is what codex (the agent) follows. I get a little nervous every time I open the IDE. Code I never wrote is following my codex. It runs fine. I didn't write a single line.&lt;/p&gt;

&lt;p&gt;Legal code, agent, app. One word holds this tool's entire workflow. Whether whoever named it intended all of this, I have no idea.&lt;/p&gt;

&lt;p&gt;Codex. Well named.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://sanghunkang.com/codex-en/" rel="noopener noreferrer"&gt;Sanghun Kang’s blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Multilingual Ghost CMS Blog: 4. Making Tag Pages Multilingual</title>
      <dc:creator>Sanghun Kang</dc:creator>
      <pubDate>Thu, 01 Oct 2026 02:21:10 +0000</pubDate>
      <link>https://dev.to/sanghunkang/multilingual-ghost-cms-blog-4-making-tag-pages-multilingual-5563</link>
      <guid>https://dev.to/sanghunkang/multilingual-ghost-cms-blog-4-making-tag-pages-multilingual-5563</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffi3acpzvwpn9jjt8kz19.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffi3acpzvwpn9jjt8kz19.png" alt="Multilingual Ghost CMS Blog: 4. Making Tag Pages Multilingual" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's go to the news tag page.&lt;/p&gt;

&lt;p&gt;https://{host}/tag/news/&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fns5uzxk719vn2riy5zrm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fns5uzxk719vn2riy5zrm.png" alt="Multilingual Ghost CMS Blog: 4. Making Tag Pages Multilingual" width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you open that tag page, it shows everything: Korean posts (#ko: ko news1, ko news2), English posts (#en: en news1, en news2), and posts without a language tag assigned (Comming Soon).&lt;/p&gt;

&lt;p&gt;Let's look at routes.yaml again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;collections&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;/&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tag:en"&lt;/span&gt;
    &lt;span class="na"&gt;permalink&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/{slug}/&lt;/span&gt;
    &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;index-en&lt;/span&gt;
  &lt;span class="na"&gt;/ko/&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tag:ko"&lt;/span&gt;
    &lt;span class="na"&gt;permalink&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/ko/{slug}/&lt;/span&gt;
    &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;index-ko&lt;/span&gt;

&lt;span class="na"&gt;taxonomies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tag/{slug}/&lt;/span&gt;
  &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/author/{slug}/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;routes.yaml&lt;/p&gt;

&lt;p&gt;I set the English and Korean post paths completely differently. (/, /ko/)&lt;/p&gt;

&lt;p&gt;But you cannot do that for tags. A tag cannot have multiple paths, and it is currently set to /tag/{slug}/. In other words, multilingual handling has to be done on a single page.&lt;/p&gt;

&lt;p&gt;Let's implement the tag page like this to support multiple languages.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Show only posts with the language tag selected in the Language selector&lt;/li&gt;
&lt;li&gt;Change the visible posts when the language is changed in the Language selector&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://github.com/sanghunka/ghost-multilingual-theme/commit/d6030585a2b1a0ece111f6dbb7b434a147ba4005?ref=sanghunkang.com" rel="noopener noreferrer"&gt;0.0.4 · sanghunka/ghost-multilingual-theme@d603058 - Multilingual tag page&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Change how the Language selector works
&lt;/h2&gt;

&lt;p&gt;Right now, changing the Language selector value redirects to / or /ko/.&lt;/p&gt;

&lt;p&gt;On the tag page, when the Language selector value changes, I will change it so that it &lt;strong&gt;does not redirect&lt;/strong&gt; and instead shows only posts with the corresponding language tag.&lt;/p&gt;

&lt;p&gt;Let's modify default.hbs so that the Language selector works differently depending on whether the page is a tag page or not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;ghost_foot&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;initializeLanguageSelector&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;selector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;languageSelector&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;currentUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;currentUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tag/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;urlLanguage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ko/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ko&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;selectedLanguage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;urlLanguage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;urlLanguage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;selectedLanguage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;selectedLanguage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;currentUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tag/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ko&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ko/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;initializeLanguageSelector&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;On lines 8 and 20, it checks whether the page is a tag page.

&lt;ul&gt;
&lt;li&gt;if (!currentUrl.includes('/tag/'))&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Line 13

&lt;ul&gt;
&lt;li&gt;If it is a tag page, it does not check the URL and instead takes the selectedLanguage value currently stored in localStorage as is and sets that as the Language selector value. If that value does not exist, for example when accessing the page directly by URL, it sets the default value en.&lt;/li&gt;
&lt;li&gt;selector.value = localStorage.getItem('selectedLanguage') || 'en';&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Line 24

&lt;ul&gt;
&lt;li&gt;If it is a tag page, it does not redirect to /ko/ or / and simply refreshes the page.&lt;/li&gt;
&lt;li&gt;window.location = window.location.href;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reason for refreshing is to show posts that match that language. Up to this point, applying this means that changing the Language selector value on the tag page will not redirect to /ko or /. However, it still will not show posts for that language. To do that, tag.hbs must be modified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modify tag.hbs
&lt;/h2&gt;

&lt;p&gt;You can check the modified tag.hbs code at the link below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sanghunka/ghost-multilingual-theme/blob/d6030585a2b1a0ece111f6dbb7b434a147ba4005/multi/tag.hbs?ref=sanghunkang.com" rel="noopener noreferrer"&gt;ghost-multilingual-theme/multi/tag.hbs at d6030585a2b1a0ece111f6dbb7b434a147ba4005 · sanghunka/ghost-multilingual-theme - Contribute to sanghunka/ghost-multilingual-theme development by creating an account on GitHub.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The idea is as follows.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Templates are processed on the server side. You cannot use the localStorage value held on the frontend to fetch only posts in a specific language.&lt;/li&gt;
&lt;li&gt;You have to use a method where you fetch everything first and then filter it.&lt;/li&gt;
&lt;li&gt;On the tag page, a post has the form &lt;code&gt;&amp;lt;div class="post-card"&amp;gt;&lt;/code&gt;. Let's also include language information in that div and then filter it on the frontend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the problem is that there is no way in the template layer to know which language tag that post has. To solve this, there is a rule in this tutorial that was established in the first post.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡Let's use the second tag as the position for the language tag&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;foreach&lt;/span&gt; &lt;span class="nv"&gt;posts&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"post-card"&lt;/span&gt; &lt;span class="na"&gt;data-language=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;{{!-- The tag below includes the markup for each post - partials/post-card.hbs --}}&lt;/span&gt;
        &lt;span class="k"&gt;{{&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"post-card"&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;foreach&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you get the second tag (tags.[1].name), you can pass that post's language through the template layer.&lt;/p&gt;

&lt;p&gt;Now let's look at the frontend part. First, all post-cards need to be hidden. Set opacity to 0. I added line 5 to create a smooth fade-in effect for posts when the language is changed in the language selector.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class="nc"&gt;.post-card-excerpt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="nc"&gt;.post-card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* Initially set all post cards to be transparent */&lt;/span&gt;
        &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;opacity&lt;/span&gt; &lt;span class="m"&gt;0.5s&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* Transition for smooth display */&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For posts that match the condition, set opacity to 1 so they become visible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMContentLoaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selectedLanguage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;selectedLanguage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Read language setting from local storage or use default 'en'&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;postCards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div.post-card[data-language]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;visibleCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;postCards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Remove '#' character and update language setting&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;language&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-language&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cleanedLanguage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-language&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cleanedLanguage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Check if it matches the selected language, then show or hide&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cleanedLanguage&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;selectedLanguage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;visibleCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Count the number of posts that match the selected language&lt;/span&gt;
            &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  &lt;span class="c1"&gt;// Use setTimeout for a smooth display&lt;/span&gt;
                &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Set opacity to 1 to make the element visible&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="c1"&gt;// Dynamically update text&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;collectionText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.post-card-excerpt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;collectionText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;visibleCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zero posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="nx"&gt;visibleCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1 post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;visibleCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; posts`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Set text based on the count&lt;/span&gt;
        &lt;span class="nx"&gt;collectionText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`A collection of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Set text in HTML&lt;/span&gt;
        &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  &lt;span class="c1"&gt;// Use setTimeout for a smooth display&lt;/span&gt;
            &lt;span class="nx"&gt;collectionText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Set opacity to 1 to make the element visible&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;It gets the selectedLanguage value stored in localStorage, and if that value does not exist, it uses the default value 'en'.&lt;/li&gt;
&lt;li&gt;The language tags are #en or #ko. The selectedLanguage value is en or ko. For direct comparison, let's remove all # characters from the data-language attribute.&lt;/li&gt;
&lt;li&gt;If it does not match the condition, set display = 'none'. If it does match, change opacity = 1 to show the post.&lt;/li&gt;
&lt;li&gt;While processing this, it counts the visibleCount value and displays on the screen how many posts there are.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgclj09kt7i0um1j5oiap.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgclj09kt7i0um1j5oiap.png" alt="Multilingual Ghost CMS Blog: 4. Making Tag Pages Multilingual" width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It was implemented just as I wanted. I think there may be a more concise way to handle the CSS, but with my knowledge, this seems to be the best I can do for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going forward
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Multilingual handling for the About and Author pages&lt;/li&gt;
&lt;li&gt;Clicking the logo in the top left always redirects to /&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is still a lot left to handle. However, these are not areas that have a major impact on usability, so I plan to fix them gradually.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sanghunkang.com/multilingual-ghost-cms-5-handling-multilingual-home-link-en/" rel="noopener noreferrer"&gt;Multilingual Ghost CMS Blog: 5. Making Home Links Language Aware - This explains how to make the home links in the navigation bar, footer, and error page point to the correct language-specific homepage.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://sanghunkang.com/multilingual-ghost-cms-4-tag-en/" rel="noopener noreferrer"&gt;Sanghun Kang’s blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>webdev</category>
      <category>software</category>
    </item>
  </channel>
</rss>
