<?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: Takashi Masuda</title>
    <description>The latest articles on DEV Community by Takashi Masuda (@masutaka).</description>
    <link>https://dev.to/masutaka</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F44998%2Fd9ecea54-0f9a-4fbe-95e6-252288b4fbb1.jpeg</url>
      <title>DEV Community: Takashi Masuda</title>
      <link>https://dev.to/masutaka</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/masutaka"/>
    <language>en</language>
    <item>
      <title>Migrating from asdf and direnv to mise</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Sun, 29 Mar 2026 09:27:47 +0000</pubDate>
      <link>https://dev.to/masutaka/migrating-from-asdf-and-direnv-to-mise-3nhi</link>
      <guid>https://dev.to/masutaka/migrating-from-asdf-and-direnv-to-mise-3nhi</guid>
      <description>&lt;p&gt;For managing versions of development tools like Ruby and Node.js, I had gone through &lt;code&gt;*env&lt;/code&gt; tools like &lt;a href="https://github.com/rbenv/rbenv" rel="noopener noreferrer"&gt;rbenv&lt;/a&gt; and &lt;a href="https://github.com/nodenv/nodenv" rel="noopener noreferrer"&gt;nodenv&lt;/a&gt;, then switched to &lt;a href="https://github.com/asdf-vm/asdf" rel="noopener noreferrer"&gt;asdf&lt;/a&gt; in 2019. For environment variable management, I had been using &lt;a href="https://github.com/direnv/direnv" rel="noopener noreferrer"&gt;direnv&lt;/a&gt; since even earlier—2014.&lt;/p&gt;

&lt;p&gt;Recently, a tool called &lt;a href="https://mise.jdx.dev/" rel="noopener noreferrer"&gt;mise&lt;/a&gt; has been gaining attention. I wasn't particularly having issues, but out of curiosity and the motivation to reduce the number of tools—since I heard mise also has direnv-like functionality—I decided to make the switch. My environment is macOS.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/jdx" rel="noopener noreferrer"&gt;
        jdx
      &lt;/a&gt; / &lt;a href="https://github.com/jdx/mise" rel="noopener noreferrer"&gt;
        mise
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      dev tools, env vars, task runner
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  What is mise?
&lt;/h2&gt;

&lt;p&gt;mise (pronounced "meez") is a tool that handles both development tool version management and environment variable management in one place. It provides asdf-compatible runtime management along with direnv-equivalent environment variable management.&lt;/p&gt;

&lt;p&gt;Here are the notable features I found after using it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can read &lt;code&gt;.tool-versions&lt;/code&gt; (&lt;code&gt;.ruby-version&lt;/code&gt; etc. requires configuration, described later)&lt;/li&gt;
&lt;li&gt;Can define environment variables and task runners in &lt;code&gt;mise.toml&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://github.com/jdx/mise/blob/v2026.3.17/mise.toml" rel="noopener noreferrer"&gt;official mise.toml&lt;/a&gt; may be a good reference&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Supports &lt;code&gt;mise.local.toml&lt;/code&gt; for local settings meant to be gitignored. This allows individual adoption even before a team officially adopts mise&lt;/li&gt;

&lt;li&gt;Well-organized tool management experience

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mise install&lt;/code&gt; installs all tools from &lt;code&gt;.tool-versions&lt;/code&gt; in one command&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mise list&lt;/code&gt; shows all tool versions and their source files at a glance&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I Did for the Migration
&lt;/h2&gt;

&lt;p&gt;The official documentation has a &lt;a href="https://mise.jdx.dev/faq.html#how-do-i-migrate-from-asdf" rel="noopener noreferrer"&gt;migration guide from asdf&lt;/a&gt;, so start there. Below are the specific steps for my environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Homebrew Package Changes
&lt;/h3&gt;

&lt;p&gt;I uninstalled asdf and direnv, and installed mise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;brew uninstall &lt;span class="nt"&gt;--force&lt;/span&gt; asdf
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;brew uninstall &lt;span class="nt"&gt;--force&lt;/span&gt; direnv
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;mise
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. zsh Configuration Changes
&lt;/h3&gt;

&lt;p&gt;I removed all asdf settings from &lt;code&gt;~/.zshenv&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-MY_ASDF_CONFIG_HOME="${XDG_CONFIG_HOME}/asdf"
-export ASDF_CONFIG_FILE="${MY_ASDF_CONFIG_HOME}/asdfrc"
-export ASDF_DATA_DIR="${XDG_DATA_HOME}/asdf"
-export ASDF_GEM_DEFAULT_PACKAGES_FILE="${MY_ASDF_CONFIG_HOME}/default-gems"
-export ASDF_NPM_DEFAULT_PACKAGES_FILE="${MY_ASDF_CONFIG_HOME}/default-npm-packages"
-export ASDF_PERL_DEFAULT_PACKAGES_FILE="${MY_ASDF_CONFIG_HOME}/default-perl-modules"
-export ASDF_PYTHON_DEFAULT_PACKAGES_FILE="${MY_ASDF_CONFIG_HOME}/default-python-packages"
-export ASDF_RUBY_BUILD_VERSION=master
-PATH=$ASDF_DATA_DIR/shims:$PATH
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also removed the direnv settings from &lt;code&gt;~/.zshrc&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-eval "$(direnv hook zsh)"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, I added the mise configuration to &lt;code&gt;~/.zshrc&lt;/code&gt;. Since the activation mechanism enabled by this setting handles PATH management, no configuration in &lt;code&gt;~/.zshenv&lt;/code&gt; is needed.&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="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mise activate zsh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here are the actual files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/masutaka/dotfiles-public/blob/df434ff7b79eb4f61817501a3ca21c6c3e37668d/.zshenv" rel="noopener noreferrer"&gt;&lt;code&gt;~/.zshenv&lt;/code&gt;&lt;/a&gt; (no mise settings)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/masutaka/dotfiles-public/blob/df434ff7b79eb4f61817501a3ca21c6c3e37668d/.zshrc#L238-L252" rel="noopener noreferrer"&gt;&lt;code&gt;~/.zshrc&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the asdf-era environment variables gone, my zsh configuration became cleaner.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Installing Tools
&lt;/h3&gt;

&lt;p&gt;I reinstalled the tools that asdf had been managing using &lt;code&gt;mise install&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.tool-versions
&lt;span class="go"&gt;nodejs 24.14.0
ruby 4.0.2

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mise &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mise list
&lt;span class="go"&gt;Tool       Version   Source            Requested
node       24.14.0   ~/.tool-versions  24.14.0
ruby       4.0.2     ~/.tool-versions  4.0.2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Migrating default_packages_file
&lt;/h3&gt;

&lt;p&gt;With asdf, I managed default_packages_file via environment variables in &lt;code&gt;~/.zshenv&lt;/code&gt;. With mise, I consolidated them in &lt;code&gt;~/.config/mise/config.toml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[settings.node]&lt;/span&gt;
&lt;span class="py"&gt;default_packages_file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"~/.config/mise/default-npm-packages"&lt;/span&gt;

&lt;span class="nn"&gt;[settings.ruby]&lt;/span&gt;
&lt;span class="py"&gt;default_packages_file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"~/.config/mise/default-gems"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During the migration, I discovered a bug where &lt;code&gt;~&lt;/code&gt; in Node.js's &lt;code&gt;default_packages_file&lt;/code&gt; wasn't being expanded, causing default packages not to be installed. I reported it in &lt;a href="https://github.com/jdx/mise/discussions/8606" rel="noopener noreferrer"&gt;Discussion#8606&lt;/a&gt;, and it was fixed in &lt;a href="https://github.com/jdx/mise/pull/8709" rel="noopener noreferrer"&gt;PR #8709&lt;/a&gt; 🙏 This is resolved in &lt;a href="https://github.com/jdx/mise/releases/tag/v2026.3.11" rel="noopener noreferrer"&gt;v2026.3.11&lt;/a&gt; and later.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Configuring .ruby-version and .node-version Support
&lt;/h3&gt;

&lt;p&gt;Neither asdf nor mise reads &lt;code&gt;.ruby-version&lt;/code&gt; or &lt;code&gt;.node-version&lt;/code&gt; by default. With asdf, you set &lt;code&gt;legacy_version_file = yes&lt;/code&gt; in &lt;code&gt;asdfrc&lt;/code&gt;, but with mise, the following configuration is needed. I added it to the repository's &lt;code&gt;mise.toml&lt;/code&gt; or &lt;code&gt;mise.local.toml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[settings]&lt;/span&gt;
&lt;span class="py"&gt;idiomatic_version_file_enable_tools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ruby"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I Didn't Migrate
&lt;/h2&gt;

&lt;p&gt;Some settings were either unnecessary to migrate or I chose not to migrate.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ruby pre-install hook

&lt;ul&gt;
&lt;li&gt;With asdf, I set &lt;code&gt;RUBY_CONFIGURE_OPTS&lt;/code&gt; via &lt;a href="https://github.com/masutaka/dotfiles-public/blob/9aa6ff2e058dc2c87aae77bc8c640399cac34596/.config/asdf/asdfrc#L2" rel="noopener noreferrer"&gt;&lt;code&gt;pre_asdf_install_ruby&lt;/code&gt;&lt;/a&gt;, but this turned out to be unnecessary with mise&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;ASDF_RUBY_BUILD_VERSION=master&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;mise &lt;a href="https://github.com/jdx/mise/blob/v2026.3.17/src/plugins/core/ruby.rs#L87-L125" rel="noopener noreferrer"&gt;automatically fetches the latest version of ruby-build&lt;/a&gt; when installing Ruby, so this setting is unnecessary&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Perl default-perl-modules

&lt;ul&gt;
&lt;li&gt;mise doesn't support &lt;code&gt;default_packages_file&lt;/code&gt; for Perl (see the appendix below for a workaround)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;direnvrc PATH_add&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I had a configuration to automatically add &lt;code&gt;./bin&lt;/code&gt; to PATH for Ruby projects with a Gemfile.lock&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;mise doesn't have an equivalent global feature, but I decided to configure it per-project in &lt;code&gt;mise.toml&lt;/code&gt; when needed&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[env]&lt;/span&gt;
&lt;span class="py"&gt;_.path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"./bin"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I replaced two tools—asdf + direnv—with just mise. The zsh configuration became cleaner, and being able to consolidate tool versions and environment variables in &lt;code&gt;mise.toml&lt;/code&gt; is a nice benefit.&lt;/p&gt;

&lt;p&gt;There were some gotchas like the Emacs compatibility issue described in the appendix below, but overall I'm happy with the migration.&lt;/p&gt;

&lt;p&gt;Also, with direnv no longer needed, I was able to migrate from &lt;code&gt;.envrc&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt;, which later enabled me to set up &lt;code&gt;.env&lt;/code&gt; mounting with &lt;a href="https://developer.1password.com/docs/environments/" rel="noopener noreferrer"&gt;1Password Environments&lt;/a&gt;.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://mise.jdx.dev/getting-started.html" rel="noopener noreferrer"&gt;mise Getting Started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://techracho.bpsinc.jp/hachi8833/2025_09_29/153488" rel="noopener noreferrer"&gt;miseは便利: タスクランナー兼ツールバージョン管理&amp;amp;環境変数管理ツール - TechRacho&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.1password.com/docs/environments/" rel="noopener noreferrer"&gt;1Password Environments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.classmethod.jp/articles/1password-environments-env-management/" rel="noopener noreferrer"&gt;1Password Environmentsで.envファイルを管理できるようになったので試してみた - DevelopersIO&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Appendix
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fixing ruby-lsp Not Working in Emacs
&lt;/h3&gt;

&lt;p&gt;I encountered an issue where &lt;a href="https://github.com/Shopify/ruby-lsp" rel="noopener noreferrer"&gt;ruby-lsp&lt;/a&gt; wouldn't work in Emacs for repositories using a Ruby version different from the global one.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mise activate zsh&lt;/code&gt; takes a hook-based approach, using zsh's &lt;code&gt;chpwd&lt;/code&gt; hook to switch tool versions when changing directories. However, Emacs doesn't execute this hook, so mise's version switching doesn't work.&lt;/p&gt;

&lt;p&gt;mise offers an alternative to hook-based activation called shims.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;activate&lt;/th&gt;
&lt;th&gt;shims&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Interactive shell&lt;/td&gt;
&lt;td&gt;Non-interactive (IDE, Emacs, scripts)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hooks like &lt;code&gt;cd&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Triggered&lt;/td&gt;
&lt;td&gt;Not triggered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;which&lt;/code&gt; result&lt;/td&gt;
&lt;td&gt;Returns the actual tool path&lt;/td&gt;
&lt;td&gt;Returns the shim path&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Initially, I added &lt;code&gt;eval "$(mise activate zsh --shims)"&lt;/code&gt; to &lt;code&gt;~/.zshrc&lt;/code&gt; to enable both, but the &lt;a href="https://mise.jdx.dev/dev-tools/shims.html" rel="noopener noreferrer"&gt;mise documentation&lt;/a&gt; assumes activate and shims are mutually exclusive.&lt;/p&gt;

&lt;p&gt;I ultimately settled on the following setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;eval "$(mise activate zsh)"&lt;/code&gt; in &lt;code&gt;~/.zshrc&lt;/code&gt; (for interactive shell)&lt;/li&gt;
&lt;li&gt;Add the shims directory to PATH only when launching Emacs
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;emacs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"LC_COLLATE=C PATH='&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;XDG_DATA_HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/mise/shims:&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;' emacs"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a separation of concerns: regular zsh sessions use hooks only, while Emacs uses shims.&lt;/p&gt;

&lt;p&gt;Compared to asdf's simpler shims-only design, this setup might look more complex at first glance. However, I understand this is a tradeoff stemming from mise being a replacement for both asdf and direnv. The real-time reflection of environment variable changes in the interactive shell via &lt;code&gt;mise activate&lt;/code&gt; is something shims alone cannot achieve.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workaround for Perl default-packages
&lt;/h3&gt;

&lt;p&gt;mise's default-packages mechanism is a language-specific feature hardcoded in core plugins (Go, Node.js, Python, Ruby). Perl is not a core plugin—it's installed via the Aqua backend—so it's not covered.&lt;/p&gt;

&lt;p&gt;As a workaround, you can run &lt;code&gt;cpanm&lt;/code&gt; in &lt;code&gt;mise.toml&lt;/code&gt;'s &lt;code&gt;[hooks]&lt;/code&gt; postinstall&lt;sup id="fnref1"&gt;1&lt;/sup&gt;. This way, &lt;code&gt;mise install&lt;/code&gt; alone sets up both Perl and CPAN modules.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[settings]&lt;/span&gt;
&lt;span class="py"&gt;experimental&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[hooks]&lt;/span&gt;
&lt;span class="py"&gt;postinstall&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"cpanm CGI HTML::Template"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that hooks is an experimental feature, so &lt;code&gt;experimental = true&lt;/code&gt; is required. Also, postinstall runs on every tool installation, not just Perl, so this hook will be triggered every time you run &lt;code&gt;mise install&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Relationship Between mise and aqua
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/aquaproj/aqua" rel="noopener noreferrer"&gt;aqua&lt;/a&gt; is a tool specialized in version management for CLI tools (terraform, gh, etc.), with robust security through checksum verification. While mise and aqua cover different scopes, mise can use aqua-registry as a backend (via the &lt;code&gt;aqua:&lt;/code&gt; prefix), allowing you to install tools managed by aqua through mise.&lt;/p&gt;

&lt;p&gt;They're not identical, but for personal dotfiles management, I feel mise alone can sufficiently cover aqua-like use cases. If your team uses aqua or you need checksum verification for supply chain security in CI/CD, there's still value in using aqua separately.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;a href="https://mise.jdx.dev/hooks.html" rel="noopener noreferrer"&gt;Hooks | mise-en-place&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>mise</category>
      <category>asdf</category>
      <category>direnv</category>
      <category>emacs</category>
    </item>
    <item>
      <title>Created a GitHub Actions Reusable Workflows Repository for Personal Use</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Mon, 23 Feb 2026 10:00:48 +0000</pubDate>
      <link>https://dev.to/masutaka/created-a-github-reusable-workflows-repository-for-personal-use-28mo</link>
      <guid>https://dev.to/masutaka/created-a-github-reusable-workflows-repository-for-personal-use-28mo</guid>
      <description>&lt;p&gt;I created a GitHub Actions Reusable Workflows repository for my personal use.&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/masutaka" rel="noopener noreferrer"&gt;
        masutaka
      &lt;/a&gt; / &lt;a href="https://github.com/masutaka/actions" rel="noopener noreferrer"&gt;
        actions
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      GitHub Actions reusable workflows
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;




&lt;h2&gt;
  
  
  What Are GitHub Actions Reusable Workflows?
&lt;/h2&gt;

&lt;p&gt;GitHub Actions' &lt;a href="https://docs.github.com/actions/how-tos/reuse-automations/reuse-workflows" rel="noopener noreferrer"&gt;reusable workflows&lt;/a&gt; is a mechanism that allows workflow files to be called from other repositories.&lt;/p&gt;

&lt;p&gt;For example, you can call a workflow from another repository like this:&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;example&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;masutaka/actions/.github/workflows/some-workflow.yml@main&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since you can consolidate common processes in one place, it saves you the trouble of managing the same workflow across multiple repositories.&lt;/p&gt;

&lt;p&gt;There are some limitations and caveats to be aware of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reusable workflows in private repositories cannot be called from public repositories&lt;/li&gt;
&lt;li&gt;Reusable workflows in private repositories can only be called from other repositories within the same org/user (different orgs are not allowed, and the Access policy setting of the called repository must be configured)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;env&lt;/code&gt; context at the calling workflow level is not propagated to the called workflow&lt;/li&gt;
&lt;li&gt;Environment secrets cannot be passed (only regular secrets can be passed via &lt;code&gt;secrets: inherit&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Reusable workflows always run at the job level (they cannot be used as steps). This means a separate runner starts each time they are called, and the filesystem cannot be shared between jobs. For private repositories, this also increases Actions minutes consumption. If you want to reuse at the step level, you need to use a &lt;a href="https://docs.github.com/actions/sharing-automations/creating-actions/creating-a-composite-action" rel="noopener noreferrer"&gt;composite action&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Since &lt;code&gt;masutaka/actions&lt;/code&gt; is a public repository, limitations 1 and 2 do not apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Included Workflows
&lt;/h2&gt;

&lt;p&gt;Currently, I have included the following reusable workflows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/masutaka/actions/blob/e69f6ebf3a73c22a7d963ac0dea51d2cee01a5c2/.github/workflows/add_assignee_to_pr.yml" rel="noopener noreferrer"&gt;add_assignee_to_pr.yml&lt;/a&gt; (&lt;a href="https://github.com/masutaka/actions/blob/e69f6ebf3a73c22a7d963ac0dea51d2cee01a5c2/docs/add_assignee_to_pr.md" rel="noopener noreferrer"&gt;docs&lt;/a&gt;) - Sets the PR creator as the assignee when a PR is created&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/masutaka/actions/blob/e69f6ebf3a73c22a7d963ac0dea51d2cee01a5c2/.github/workflows/codeql.yml" rel="noopener noreferrer"&gt;codeql.yml&lt;/a&gt; (&lt;a href="https://github.com/masutaka/actions/blob/e69f6ebf3a73c22a7d963ac0dea51d2cee01a5c2/docs/codeql.md" rel="noopener noreferrer"&gt;docs&lt;/a&gt;) - Detects languages from changed files and runs CodeQL analysis&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/masutaka/actions/blob/e69f6ebf3a73c22a7d963ac0dea51d2cee01a5c2/.github/workflows/codeql_core.yml" rel="noopener noreferrer"&gt;codeql_core.yml&lt;/a&gt; (&lt;a href="https://github.com/masutaka/actions/blob/e69f6ebf3a73c22a7d963ac0dea51d2cee01a5c2/docs/codeql_core.md" rel="noopener noreferrer"&gt;docs&lt;/a&gt;) - Runs CodeQL analysis for specified languages&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/masutaka/actions/blob/e69f6ebf3a73c22a7d963ac0dea51d2cee01a5c2/.github/workflows/create_gh_issue.yml" rel="noopener noreferrer"&gt;create_gh_issue.yml&lt;/a&gt; (&lt;a href="https://github.com/masutaka/actions/blob/e69f6ebf3a73c22a7d963ac0dea51d2cee01a5c2/docs/create_gh_issue.md" rel="noopener noreferrer"&gt;docs&lt;/a&gt;) - Creates a GitHub Issue from a template&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/masutaka/actions/blob/e69f6ebf3a73c22a7d963ac0dea51d2cee01a5c2/.github/workflows/dependency_review.yml" rel="noopener noreferrer"&gt;dependency_review.yml&lt;/a&gt; (&lt;a href="https://github.com/masutaka/actions/blob/e69f6ebf3a73c22a7d963ac0dea51d2cee01a5c2/docs/dependency_review.md" rel="noopener noreferrer"&gt;docs&lt;/a&gt;) - Reviews PR dependencies&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/masutaka/actions/blob/e69f6ebf3a73c22a7d963ac0dea51d2cee01a5c2/.github/workflows/pushover.yml" rel="noopener noreferrer"&gt;pushover.yml&lt;/a&gt; (&lt;a href="https://github.com/masutaka/actions/blob/e69f6ebf3a73c22a7d963ac0dea51d2cee01a5c2/docs/pushover.md" rel="noopener noreferrer"&gt;docs&lt;/a&gt;) - Sends &lt;a href="https://pushover.net/" rel="noopener noreferrer"&gt;Pushover&lt;/a&gt;&lt;sup id="fnref1"&gt;1&lt;/sup&gt; notifications for workflow failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I referenced &lt;a href="https://github.com/mdn/workflows" rel="noopener noreferrer"&gt;mdn/workflows&lt;/a&gt; for the documentation structure, documenting each workflow's usage in Markdown files under &lt;code&gt;docs/&lt;/code&gt; and linking to them from README.md.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Created This
&lt;/h2&gt;

&lt;p&gt;I had been using &lt;a href="https://github.com/route06/actions" rel="noopener noreferrer"&gt;route06/actions&lt;/a&gt;, a repository where I had been a maintainer at my previous job, even after leaving the company.&lt;/p&gt;

&lt;p&gt;However, there were a few things I wanted to customize for personal use, and creating a new &lt;code&gt;pushover.yml&lt;/code&gt; workflow prompted me to copy the necessary workflow files and create my own repository.&lt;/p&gt;

&lt;p&gt;Up until then, the same &lt;code&gt;pushover.yml&lt;/code&gt; file was duplicated across my personal repositories, but I took this opportunity to consolidate everything into &lt;code&gt;masutaka/actions&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Licenses
&lt;/h2&gt;

&lt;p&gt;Both repositories are under the MIT License. For workflow files copied over, I added attribution to the original repository at the top of each file like this:&lt;/p&gt;

&lt;p&gt;Example: &lt;a href="https://github.com/masutaka/actions/blob/e69f6ebf3a73c22a7d963ac0dea51d2cee01a5c2/.github/workflows/codeql.yml#L1-L3" rel="noopener noreferrer"&gt;codeql.yml&lt;/a&gt;:&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="c1"&gt;# Derived from https://github.com/route06/actions/blob/main/.github/workflows/codeql.yml&lt;/span&gt;
&lt;span class="c1"&gt;# Copyright (c) 2024 ROUTE06, Inc.&lt;/span&gt;
&lt;span class="c1"&gt;# Licensed under the MIT License.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also included both copyrights in the &lt;a href="https://github.com/masutaka/actions/blob/e69f6ebf3a73c22a7d963ac0dea51d2cee01a5c2/LICENSE" rel="noopener noreferrer"&gt;LICENSE&lt;/a&gt; file. I believe this satisfies the requirements of the MIT License.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Copyright (c) Takashi Masuda
Copyright (c) 2024 ROUTE06, Inc.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Until now, similar workflow files were scattered across my personal repositories, and I had to update multiple repositories every time a change was needed. By consolidating them into &lt;code&gt;masutaka/actions&lt;/code&gt;, changes can now be made in one place.&lt;/p&gt;

&lt;p&gt;As my personal repositories continue to grow, I plan to keep consolidating shareable workflows here going forward.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/actions/how-tos/reuse-automations/reuse-workflows" rel="noopener noreferrer"&gt;Reuse workflows - GitHub Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/actions/sharing-automations/creating-actions/creating-a-composite-action" rel="noopener noreferrer"&gt;Creating a composite action - GitHub Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/mdn/workflows" rel="noopener noreferrer"&gt;mdn/workflows&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/route06/actions" rel="noopener noreferrer"&gt;route06/actions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;A push notification service for iOS/Android ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>githubactions</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Created a Rails 8 Learning Repository and Started Recovering from a 6-Year Blank with Rails Guides and Claude Code</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Tue, 30 Dec 2025 05:17:38 +0000</pubDate>
      <link>https://dev.to/masutaka/created-a-rails-8-learning-repository-and-started-recovering-from-a-6-year-blank-with-rails-guides-jcc</link>
      <guid>https://dev.to/masutaka/created-a-rails-8-learning-repository-and-started-recovering-from-a-6-year-blank-with-rails-guides-jcc</guid>
      <description>&lt;p&gt;Last month I changed jobs and returned to Rails development after a long time.&lt;/p&gt;

&lt;p&gt;With a 6-year gap and only one month available for learning, I needed to efficiently grasp Rails 8's new features and refresh my memory of existing ones.&lt;/p&gt;

&lt;p&gt;So I decided to learn by building a working app together with Claude Code while referencing the Rails Guides. I temporarily subscribed to Claude Code's Max plan at &lt;code&gt;USD 100/mo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's the repository I created:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/masutaka" rel="noopener noreferrer"&gt;
        masutaka
      &lt;/a&gt; / &lt;a href="https://github.com/masutaka/trial-rails8" rel="noopener noreferrer"&gt;
        trial-rails8
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Rails ガイドを元にした素振り
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;




&lt;h2&gt;
  
  
  Learning Process
&lt;/h2&gt;

&lt;p&gt;I used the &lt;a href="https://railsguides.jp/v8.0/" rel="noopener noreferrer"&gt;Rails Guides&lt;/a&gt; as my main reference material.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Reviewing the Basics with Rails Guides
&lt;/h3&gt;

&lt;p&gt;Following &lt;a href="https://railsguides.jp/v8.0/getting_started.html" rel="noopener noreferrer"&gt;Getting Started with Rails&lt;/a&gt;, I created &lt;code&gt;/products&lt;/code&gt;. I mainly learned about Active Storage (image uploads) and Action Text (rich text).&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Learning Hotwire and Solid Trifecta
&lt;/h3&gt;

&lt;p&gt;To learn Hotwire and Solid Trifecta, I created &lt;code&gt;/chat&lt;/code&gt; and &lt;code&gt;/posts&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/chat&lt;/code&gt;: Learned Action Cable, Solid Cable, and Stimulus&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/posts&lt;/code&gt;: Learned Turbo Streams/Frames, Active Job, and Solid Queue. Added comment, follow, and notification features to a typical blog functionality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I learned through AI pair programming with Claude Code. The specific workflow was as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a GitHub Issue (e.g., &lt;a href="https://github.com/masutaka/trial-rails8/issues/14" rel="noopener noreferrer"&gt;#14 Follow Feature&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Have Claude Code create a plan document (e.g., &lt;a href="https://github.com/masutaka/trial-rails8/blob/19201432e4806aeadf22b9f27d7febca32691b77/docs/plan-for-issue-14.md" rel="noopener noreferrer"&gt;docs/plan-for-issue-14.md&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Have it create a PR based on the plan while understanding the implementation (e.g., &lt;a href="https://github.com/masutaka/trial-rails8/pull/38" rel="noopener noreferrer"&gt;#38&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I also utilized AI reviews from CodeRabbit. It pointed out N+1 queries and suggested adding tests, allowing me to maintain quality even in solo development. It's great that it's free for OSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rails Features I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Hotwire
&lt;/h3&gt;

&lt;p&gt;Hotwire is a framework introduced in Rails 7 for achieving SPA-like UX. Hotwire consists of Turbo and Stimulus.&lt;/p&gt;

&lt;p&gt;Turbo is further composed of three parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Turbo Drive: Speeds up page transitions (fetches and replaces HTML on link clicks)&lt;/li&gt;
&lt;li&gt;Turbo Frames: Updates parts of a page (inline editing of comments, etc.)&lt;/li&gt;
&lt;li&gt;Turbo Streams: Updates multiple locations simultaneously (updates list and count when adding comments)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stimulus is a JavaScript framework, but it's a very thin layer for adding minimal JS with HTML as the starting point. I used it for UI control in the chat feature.&lt;/p&gt;

&lt;p&gt;I utilized Turbo Streams in the comment feature. When a comment is posted, model callbacks broadcast it to other users' browsers in real-time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/comment.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Comment&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;counter_cache: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;

  &lt;span class="n"&gt;after_create_commit&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# Append to comment list&lt;/span&gt;
    &lt;span class="n"&gt;broadcast_append_to&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="ss"&gt;target: &lt;/span&gt;&lt;span class="s2"&gt;"comments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"comments/comment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;comment: &lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;allow_actions: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;# Update comment count&lt;/span&gt;
    &lt;span class="n"&gt;broadcast_replace_to&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="ss"&gt;target: &lt;/span&gt;&lt;span class="s2"&gt;"comment_count_&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"posts/comment_count"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;post: &lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Solid Trifecta
&lt;/h3&gt;

&lt;p&gt;"Solid Trifecta" introduced in Rails 8 eliminates the need for external middleware like Redis or Memcached.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Solid Queue: Database-based Active Job backend&lt;/li&gt;
&lt;li&gt;Solid Cable: Database-based Action Cable adapter&lt;/li&gt;
&lt;li&gt;Solid Cache: Database-based cache store (not used this time)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used Active Job + Solid Queue for the scheduled post feature. When you set a future datetime for &lt;code&gt;published_at&lt;/code&gt;, a job executes at that time to publish the article.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/jobs/publish_post_job.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PublishPostJob&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationJob&lt;/span&gt;
  &lt;span class="n"&gt;queue_as&lt;/span&gt; &lt;span class="ss"&gt;:default&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scheduled_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="n"&gt;post_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;

    &lt;span class="c1"&gt;# Idempotency: Skip if already published&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;published&lt;/span&gt;

    &lt;span class="c1"&gt;# Skip if publish datetime was changed&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blank?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;scheduled_at&lt;/span&gt;

    &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;published: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can check job execution status with Mission Control Jobs. Access &lt;a href="http://localhost:3000/jobs" rel="noopener noreferrer"&gt;http://localhost:3000/jobs&lt;/a&gt; in development to view job history and queue status in a Web UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Eager Loading: &lt;code&gt;includes&lt;/code&gt;/&lt;code&gt;preload&lt;/code&gt;/&lt;code&gt;eager_load&lt;/code&gt; to prevent N+1 queries&lt;/li&gt;
&lt;li&gt;has_secure_password + rate_limit: User authentication and rate limiting&lt;/li&gt;
&lt;li&gt;Counter Cache: Efficient retrieval of comment counts (&lt;code&gt;belongs_to :post, counter_cache: true&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Foreign key constraint &lt;code&gt;on_delete: :cascade&lt;/code&gt;: Automatically deletes child records when parent is deleted. Used alongside Rails' &lt;code&gt;dependent: :destroy&lt;/code&gt; to maintain DB-level integrity&lt;/li&gt;
&lt;li&gt;Shallow Routing: Specifying &lt;code&gt;shallow: true&lt;/code&gt; for nested resources shortens URLs for member actions (show/edit/update/destroy)&lt;/li&gt;
&lt;li&gt;Polymorphic Association: Used for notification feature. Manages follow and comment notifications uniformly&lt;/li&gt;
&lt;li&gt;Self-Referential Association: Used for follow feature. Expresses many-to-many relationships with a single &lt;code&gt;follows&lt;/code&gt; table&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Despite a 6-year gap, I was able to learn efficiently with Rails Guides and Claude Code.&lt;/p&gt;

&lt;p&gt;Note that I didn't write many tests this time. The goal was to learn Rails features, so I'll save test learning for another time.&lt;/p&gt;

&lt;p&gt;My mental model was stuck around the time of "temporarily disabling Turbolinks...", so I was quite impressed by Hotwire's ability to achieve SPA-like UX with almost no JavaScript. My memory of Rails was that it often served as an API server or adopted React for the frontend, but I now think Hotwire should be considered first.&lt;/p&gt;

&lt;p&gt;Solid Queue and Solid Cable are also nice because you can start with just RDS, reducing the number of components.&lt;/p&gt;

&lt;p&gt;I'll continue working through the open Issues while continuing to learn.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hotwired.dev/" rel="noopener noreferrer"&gt;Hotwire Official Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turbo.hotwired.dev/handbook/introduction" rel="noopener noreferrer"&gt;Turbo Handbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stimulus.hotwired.dev/handbook/introduction" rel="noopener noreferrer"&gt;Stimulus Handbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rails/solid_queue" rel="noopener noreferrer"&gt;Solid Queue (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rails/solid_cable" rel="noopener noreferrer"&gt;Solid Cable (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rails/mission_control-jobs" rel="noopener noreferrer"&gt;Mission Control Jobs (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.coderabbit.ai/" rel="noopener noreferrer"&gt;CodeRabbit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rails</category>
      <category>github</category>
      <category>claudecode</category>
      <category>coderabbit</category>
    </item>
    <item>
      <title>Finally Modernized My Emacs Setup with LSP + Tree-sitter</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Mon, 24 Nov 2025 10:13:19 +0000</pubDate>
      <link>https://dev.to/masutaka/finally-modernized-my-emacs-setup-with-lsp-tree-sitter-125</link>
      <guid>https://dev.to/masutaka/finally-modernized-my-emacs-setup-with-lsp-tree-sitter-125</guid>
      <description>&lt;p&gt;In recent years, I've been mainly writing LookML, SQL, HCL, and other languages, with fewer opportunities to write programming languages like Ruby. However, I've recently returned to Rails development this month.&lt;/p&gt;

&lt;p&gt;So, I finally got around to setting up LSP (&lt;a href="https://en.wikipedia.org/wiki/Language_Server_Protocol" rel="noopener noreferrer"&gt;Language Server Protocol&lt;/a&gt;) in Emacs.&lt;/p&gt;

&lt;p&gt;During this process, I also learned about &lt;a href="https://en.wikipedia.org/wiki/Tree-sitter%20%28parser%20generator%29" rel="noopener noreferrer"&gt;Tree-sitter&lt;/a&gt; and configured it as well.&lt;/p&gt;

&lt;p&gt;※ LSP is an advanced code assistance mechanism provided by Language Servers, while Tree-sitter is a parsing engine that quickly generates &lt;a href="https://en.wikipedia.org/wiki/Abstract%20syntax%20tree" rel="noopener noreferrer"&gt;AST&lt;/a&gt; from code to enable highlighting and structural editing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Emacs Version I'm Using
&lt;/h2&gt;

&lt;p&gt;I'm using Emacs 29.1 instead of the latest 30.2 for the following reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I use &lt;a href="https://github.com/railwaycat/homebrew-emacsmacport" rel="noopener noreferrer"&gt;railwaycat/homebrew-emacsmacport&lt;/a&gt; with &lt;a href="https://bitbucket.org/mituharu/emacs-mac/" rel="noopener noreferrer"&gt;Mac port patch&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/masutaka/dotfiles-public/blob/37ce0f656d6ca344e187de2e15dccac0536d7360/.emacs.d/init.el#L1134-L1210" rel="noopener noreferrer"&gt;I rely on some useful features provided by the Mac port patch&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;The latest Mac port patch is &lt;a href="https://bitbucket.org/mituharu/emacs-mac/src/emacs-29.4-mac-10.1/" rel="noopener noreferrer"&gt;emacs-29.4-mac-10.1&lt;/a&gt; and hasn't caught up to version 30&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://github.com/railwaycat/homebrew-emacsmacport/releases/tag/emacs-29.4-mac-10.1" rel="noopener noreferrer"&gt;railwaycat/&lt;/a&gt;&lt;a href="mailto:homebrew-emacsmacport@emacs-29.4-mac-10.1"&gt;homebrew-emacsmacport@emacs-29.4-mac-10.1&lt;/a&gt; with the patch has issue &lt;a href="https://github.com/railwaycat/homebrew-emacsmacport/issues/389" rel="noopener noreferrer"&gt;#389&lt;/a&gt; on my environment&lt;/li&gt;

&lt;li&gt;Therefore, I'm using &lt;a href="https://github.com/railwaycat/homebrew-emacsmacport/releases/tag/emacs-29.1-mac-10.0" rel="noopener noreferrer"&gt;railwaycat/&lt;/a&gt;&lt;a href="mailto:homebrew-emacsmacport@emacs-29.1-mac-10.0"&gt;homebrew-emacsmacport@emacs-29.1-mac-10.0&lt;/a&gt;
&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introducing LSP
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Adopted lsp-mode
&lt;/h3&gt;

&lt;p&gt;I compared &lt;a href="https://github.com/joaotavora/eglot" rel="noopener noreferrer"&gt;eglot&lt;/a&gt;, which is standard in Emacs 29, with the third-party &lt;a href="https://github.com/emacs-lsp/lsp-mode" rel="noopener noreferrer"&gt;lsp-mode&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I would have preferred to use the standard eglot, but I decided to adopt lsp-mode because there's information suggesting that &lt;a href="https://github.com/castwide/solargraph" rel="noopener noreferrer"&gt;Solargraph&lt;/a&gt;, which eglot requires as the Ruby Language Server, doesn't have good performance. lsp-mode requires &lt;a href="https://github.com/Shopify/ruby-lsp" rel="noopener noreferrer"&gt;ruby-lsp&lt;/a&gt; as the Language Server.&lt;/p&gt;

&lt;p&gt;I haven't actually verified whether Solargraph really has poor performance 😅&lt;/p&gt;

&lt;h3&gt;
  
  
  lsp-mode Configuration Example
&lt;/h3&gt;

&lt;p&gt;First, install the Language Server for each language in a PATH that Emacs recognizes (exec-path).&lt;/p&gt;

&lt;p&gt;The correspondence between each language and Language Server is documented in &lt;a href="https://emacs-lsp.github.io/lsp-mode/page/languages/" rel="noopener noreferrer"&gt;Languages&lt;/a&gt; in the lsp-mode documentation.&lt;/p&gt;

&lt;p&gt;Here are the Language Servers I installed this time:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lang&lt;/th&gt;
&lt;th&gt;Language Server&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dockerfile&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/dockerfile-language-server-nodejs" rel="noopener noreferrer"&gt;dockerfile-language-server-nodejs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/golang/tools/tree/master/gopls" rel="noopener noreferrer"&gt;gopls&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/vscode-json-languageserver" rel="noopener noreferrer"&gt;vscode-json-languageserver&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ruby&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://rubygems.org/gems/ruby-lsp" rel="noopener noreferrer"&gt;ruby-lsp&lt;/a&gt;, &lt;a href="https://rubygems.org/gems/ruby-lsp-rails" rel="noopener noreferrer"&gt;ruby-lsp-rails&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/typescript-language-server" rel="noopener noreferrer"&gt;typescript-language-server&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Next, &lt;a href="https://melpa.org/#/getting-started" rel="noopener noreferrer"&gt;install lsp-mode from Melpa&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Finally, just add the &lt;code&gt;lsp-deferred&lt;/code&gt; function to the hook of the major-mode you want to enable.&lt;/p&gt;

&lt;p&gt;Here's a configuration example for ruby-mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight common_lisp"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;add-hook&lt;/span&gt; &lt;span class="ss"&gt;'ruby-mode-hook&lt;/span&gt; &lt;span class="nf"&gt;#'&lt;/span&gt;&lt;span class="nv"&gt;lsp-deferred&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this, you can jump to definitions with &lt;code&gt;M-.&lt;/code&gt; and return with &lt;code&gt;M-,&lt;/code&gt;, and it also points out indentation issues. For other features, please see the &lt;a href="https://emacs-lsp.github.io/lsp-mode/" rel="noopener noreferrer"&gt;lsp-mode documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I also configured the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight common_lisp"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;setq&lt;/span&gt; &lt;span class="nv"&gt;lsp-keymap-prefix&lt;/span&gt; &lt;span class="s"&gt;"C-c C-l"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;;; Disable rubocop-ls so that ruby-lsp is prioritized even when rubocop is installed.&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;setq&lt;/span&gt; &lt;span class="nv"&gt;lsp-disabled-clients&lt;/span&gt; &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;rubocop-ls&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;;; Format files on save for all buffers where lsp-mode is active.&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;setq-default&lt;/span&gt; &lt;span class="nv"&gt;lsp-format-buffer-on-save&lt;/span&gt; &lt;span class="no"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Discovered xxx-ts-mode Has Been Added to Emacs
&lt;/h2&gt;

&lt;p&gt;When trying to configure lsp-mode for TypeScript, I noticed that xxx-ts-mode exists, which wasn't in previous versions of Emacs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;find /Applications/Emacs.app &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*-ts-mode.elc'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt;
&lt;span class="go"&gt;/Applications/Emacs.app/Contents/Resources/lisp/progmodes/c-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/cmake-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/dockerfile-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/go-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/java-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/json-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/ruby-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/rust-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/typescript-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/textmodes/toml-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/textmodes/yaml-ts-mode.elc
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unlike traditional xxx-mode which parses syntax using regular expressions, xxx-ts-mode parses syntax at the AST level using Tree-sitter. Therefore, syntax highlighting is more accurate, and indentation and structural editing are more stable.&lt;/p&gt;

&lt;p&gt;I was about to skip it, but when I looked at &lt;a href="https://github.com/emacs-typescript/typescript.el/tree/481df3ad2cdf569d8e6697679669ff6206fbd2f9#readme" rel="noopener noreferrer"&gt;typescript-mode's README.md&lt;/a&gt;, I learned that development has stopped:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Essentially all major development of &lt;code&gt;typescript-mode&lt;/code&gt; has come to a halt.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I decided to introduce xxx-ts-mode, starting with typescript-ts-mode. I also felt the benefit of being able to drop third-party xxx-mode packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing typescript-ts-mode
&lt;/h2&gt;

&lt;p&gt;You can install the Tree-sitter for TypeScript by adding the following to init.el:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight common_lisp"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;setq&lt;/span&gt; &lt;span class="nv"&gt;treesit-language-source-alist&lt;/span&gt;
      &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;tsx&lt;/span&gt; &lt;span class="s"&gt;"https://github.com/tree-sitter/tree-sitter-typescript"&lt;/span&gt; &lt;span class="s"&gt;"v0.23.2"&lt;/span&gt; &lt;span class="s"&gt;"tsx/src"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;typescript&lt;/span&gt; &lt;span class="s"&gt;"https://github.com/tree-sitter/tree-sitter-typescript"&lt;/span&gt; &lt;span class="s"&gt;"v0.23.2"&lt;/span&gt; &lt;span class="s"&gt;"typescript/src"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;dolist&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;element&lt;/span&gt; &lt;span class="nv"&gt;treesit-language-source-alist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;lang&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;car&lt;/span&gt; &lt;span class="nv"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;unless&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;treesit-language-available-p&lt;/span&gt; &lt;span class="nv"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;treesit-install-language-grammar&lt;/span&gt; &lt;span class="nv"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When this is evaluated, the &lt;code&gt;*.dylib&lt;/code&gt; files built from source code fetched from GitHub will be installed directly under &lt;code&gt;~/.emacs.d/tree-sitter/&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;libtree-sitter-tsx.dylib&lt;/li&gt;
&lt;li&gt;libtree-sitter-typescript.dylib&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡 If you don't specify a tag, the default branch will be referenced. Since commit hash couldn't be specified, I specified tags from the perspective of security and version pinning.&lt;/p&gt;

&lt;p&gt;Then, simply associate &lt;code&gt;*.ts&lt;/code&gt; and &lt;code&gt;*.tsx&lt;/code&gt; with typescript-ts-mode and tsx-ts-mode respectively. Also enable lsp-mode.&lt;/p&gt;

&lt;p&gt;※ tsx-ts-mode is also defined in typescript-ts-mode.el.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight common_lisp"&gt;&lt;code&gt;&lt;span class="c1"&gt;;; TypeScript&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;add-to-list&lt;/span&gt; &lt;span class="ss"&gt;'auto-mode-alist&lt;/span&gt; &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\\.ts\\'"&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;typescript-ts-mode&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;typescript-ts-mode-hook-func&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;lsp-deferred&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;add-hook&lt;/span&gt; &lt;span class="ss"&gt;'typescript-ts-mode-hook&lt;/span&gt; &lt;span class="nf"&gt;#'&lt;/span&gt;&lt;span class="nv"&gt;typescript-ts-mode-hook-func&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;;; TSX&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;add-to-list&lt;/span&gt; &lt;span class="ss"&gt;'auto-mode-alist&lt;/span&gt; &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\\.tsx\\'"&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;tsx-ts-mode&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;tsx-ts-mode-hook-func&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;lsp-deferred&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;add-hook&lt;/span&gt; &lt;span class="ss"&gt;'tsx-ts-mode-hook&lt;/span&gt; &lt;span class="nf"&gt;#'&lt;/span&gt;&lt;span class="nv"&gt;tsx-ts-mode-hook-func&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Supplement: Why I Didn't Adopt the tree-sitter-langs Package
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://melpa.org/#/tree-sitter-langs" rel="noopener noreferrer"&gt;tree-sitter-langs&lt;/a&gt; package can also install &lt;code&gt;*.dylib&lt;/code&gt;, but I decided not to adopt it for the following reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It installs under &lt;code&gt;~/.emacs.d/elpa/tree-sitter-langs-20251019.1145/bin/&lt;/code&gt; instead of &lt;code&gt;~/.emacs.d/tree-sitter/&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;This directory name will change with version updates of the tree-sitter-langs package&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;The library names are like &lt;code&gt;typescript.dylib&lt;/code&gt; instead of &lt;code&gt;libtree-sitter-typescript.dylib&lt;/code&gt;
&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;It's good for checking the location of Tree-sitter parsers for each language:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/emacs-tree-sitter/tree-sitter-langs/tree/master/repos" rel="noopener noreferrer"&gt;https://github.com/emacs-tree-sitter/tree-sitter-langs/tree/master/repos&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introducing Other xxx-ts-modes
&lt;/h2&gt;

&lt;p&gt;Among the Emacs built-in xxx-ts-modes mentioned earlier, I introduced those with high usage frequency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dockerfile-ts-mode&lt;/li&gt;
&lt;li&gt;go-ts-mode&lt;/li&gt;
&lt;li&gt;json-ts-mode&lt;/li&gt;
&lt;li&gt;ruby-ts-mode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As an exception, I didn't introduce yaml-ts-mode. When saving, it formats with indent 4, and I couldn't figure out how to change it to 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Configuration for LSP mode, Tree-sitter, and xxx-ts-mode
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;LSP mode: &lt;a href="https://github.com/masutaka/dotfiles-public/blob/37ce0f656d6ca344e187de2e15dccac0536d7360/.emacs.d/init.el#L1044-L1063" rel="noopener noreferrer"&gt;init.el#L1044-L1063&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Tree-sitter: &lt;a href="https://github.com/masutaka/dotfiles-public/blob/37ce0f656d6ca344e187de2e15dccac0536d7360/.emacs.d/init.el#L1431-L1450" rel="noopener noreferrer"&gt;init.el#L1431-L1450&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;dockerfile-ts-mode: &lt;a href="https://github.com/masutaka/dotfiles-public/blob/37ce0f656d6ca344e187de2e15dccac0536d7360/.emacs.d/init.el#L801-L810" rel="noopener noreferrer"&gt;init.el#L801-L810&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;go-ts-mode: &lt;a href="https://github.com/masutaka/dotfiles-public/blob/37ce0f656d6ca344e187de2e15dccac0536d7360/.emacs.d/init.el#L905-L917" rel="noopener noreferrer"&gt;init.el#L905-L917&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;json-ts-mode: &lt;a href="https://github.com/masutaka/dotfiles-public/blob/37ce0f656d6ca344e187de2e15dccac0536d7360/.emacs.d/init.el#L980-L989" rel="noopener noreferrer"&gt;init.el#L980-L989&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ruby-ts-mode: &lt;a href="https://github.com/masutaka/dotfiles-public/blob/37ce0f656d6ca344e187de2e15dccac0536d7360/.emacs.d/init.el#L1121-L1132" rel="noopener noreferrer"&gt;init.el#L1121-L1132&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;typescript-ts-mode: &lt;a href="https://github.com/masutaka/dotfiles-public/blob/37ce0f656d6ca344e187de2e15dccac0536d7360/.emacs.d/init.el#L1014-L1028" rel="noopener noreferrer"&gt;init.el#L1014-L1028&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bonus: Migration from auto-complete to company-mode
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/emacs-lsp/lsp-mode/blob/5c4bf238cba48e9b45c6969b22dc6de53b3523bb/lsp-completion.el#L913-L931" rel="noopener noreferrer"&gt;There were usage points for company-mode in the lsp-mode code.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I had only heard the name &lt;a href="https://github.com/company-mode/company-mode" rel="noopener noreferrer"&gt;company-mode&lt;/a&gt;, but apparently it's a completion framework that assists code input.&lt;/p&gt;

&lt;p&gt;I migrated from &lt;a href="https://github.com/auto-complete/auto-complete" rel="noopener noreferrer"&gt;auto-complete&lt;/a&gt;, which I've been using for years, to company-mode.&lt;/p&gt;

&lt;p&gt;I saw some information suggesting that corfu, which is lighter than company-mode, might be better now, but I decided to go with company-mode this time as it seemed easier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight common_lisp"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;setq&lt;/span&gt; &lt;span class="nv"&gt;company-minimum-prefix-length&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;setq&lt;/span&gt; &lt;span class="nv"&gt;company-show-quick-access&lt;/span&gt; &lt;span class="no"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;add-hook&lt;/span&gt; &lt;span class="ss"&gt;'after-init-hook&lt;/span&gt; &lt;span class="nf"&gt;#'&lt;/span&gt;&lt;span class="nv"&gt;global-company-mode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Issues
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;When lsp-format-buffer-on-save is enabled for all buffers where lsp-mode is active, code written by others gets heavily reformatted, which might be awkward. Especially YAML files&lt;/li&gt;
&lt;li&gt;global-company-mode is a bit noisy, so it might be better to configure it for individual major-modes. I might change this later&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Finally, I brought my Emacs setup closer to a modern configuration with LSP + Tree-sitter. However, since there are almost no Emacs users around me, I'm not entirely sure how close I got.&lt;/p&gt;

&lt;p&gt;I was also able to reduce some third-party packages. I want to minimize dependencies as much as possible, so that's good.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Added:

&lt;ul&gt;
&lt;li&gt;company&lt;/li&gt;
&lt;li&gt;lsp-mode&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Removed:

&lt;ul&gt;
&lt;li&gt;auto-complete&lt;/li&gt;
&lt;li&gt;dockerfile-mode&lt;/li&gt;
&lt;li&gt;go-autocomplete&lt;/li&gt;
&lt;li&gt;go-eldoc&lt;/li&gt;
&lt;li&gt;go-mode&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://emacs-lsp.github.io/lsp-mode/" rel="noopener noreferrer"&gt;LSP Mode - LSP support for Emacs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/emacs-tree-sitter/tree-sitter-langs" rel="noopener noreferrer"&gt;tree-sitter-langs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://zenn.dev/eggc/articles/55e8fbde5fab1b" rel="noopener noreferrer"&gt;ruby-lsp による Rubocop 違反の自動修正 in Emacs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://zenn.dev/glassonion1/articles/20752bb8d2cf98" rel="noopener noreferrer"&gt;EmacsでTree-Sitterによる新しいモードを利用する方法&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mugijiru.github.io/.emacs.d/programming/yaml-mode/" rel="noopener noreferrer"&gt;yaml-mode :: 麦汁's Emacs Config&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://joppot.info/posts/c05e989a-e642-4c84-a5b8-a0e0c3178941" rel="noopener noreferrer"&gt;emacs29でビルドインされたtree-sitterとtsx-ts-modeでtypescriptの開発環境を構築する | joppot&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>emacs</category>
      <category>lsp</category>
      <category>treesitter</category>
    </item>
    <item>
      <title>Making this Hugo-built blog multilingual</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Tue, 23 Sep 2025 12:17:05 +0000</pubDate>
      <link>https://dev.to/masutaka/making-this-hugo-built-blog-multilingual-l9b</link>
      <guid>https://dev.to/masutaka/making-this-hugo-built-blog-multilingual-l9b</guid>
      <description>&lt;p&gt;I've made this blog built with Hugo multilingual. For now, it's only available in English.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://masutaka.net/" rel="noopener noreferrer"&gt;https://masutaka.net/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://masutaka.net/en/" rel="noopener noreferrer"&gt;https://masutaka.net/en/&lt;/a&gt; 👈&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I implemented multilingual support
&lt;/h2&gt;

&lt;p&gt;As part of my OSS activities at work, I often cross-post translated articles to platforms like dev.to and Medium. Recently, I've been casually translating and cross-posting articles on my own as well.&lt;/p&gt;

&lt;p&gt;For example, the English translation of &lt;a href="https://masutaka.net/2025-08-15-1/" rel="noopener noreferrer"&gt;2025-08-15-1&lt;/a&gt; was cross-posted to these three sites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/masutaka/migration-from-pocket-and-hatena-bookmark-to-raindropio-and-creating-helm-raindropel-5f76"&gt;dev.to&lt;/a&gt; - This served as the original English version&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://masutaka.hashnode.dev/migration-from-pocket-and-hatena-bookmark-to-raindropio-and-creating-helm-raindropel" rel="noopener noreferrer"&gt;Hashnode&lt;/a&gt; - Set dev.to's article as the canonical version&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@masutaka/migration-from-pocket-and-hatena-bookmark-to-raindrop-io-and-creating-helm-raindrop-el-6e67e22a7f17" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; - Set dev.to's article as the canonical version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, since I'm maintaining this blog masutaka.net, I thought it would be better to store the original English articles here and cross-post them to the above three sites. Setting the canonical URL to my own blog seems like it would also be beneficial for SEO.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hugo's Multilingual Support
&lt;/h2&gt;

&lt;p&gt;Hugo includes built-in multilingual support with simple configuration. All you need to do is add the following settings in your &lt;code&gt;config.toml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;defaultContentLanguage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ja"&lt;/span&gt;

&lt;span class="nn"&gt;[languages.ja]&lt;/span&gt;
  &lt;span class="py"&gt;weight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="py"&gt;languageName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"日本語"&lt;/span&gt;
  &lt;span class="py"&gt;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"マスタカの ChangeLog メモ"&lt;/span&gt;

&lt;span class="nn"&gt;[languages.en]&lt;/span&gt;
  &lt;span class="py"&gt;weight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="py"&gt;languageName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"English"&lt;/span&gt;
  &lt;span class="py"&gt;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Masutaka's ChangeLog Memo"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Articles with &lt;code&gt;.en.md&lt;/code&gt; in their filenames will be the English versions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Japanese version: &lt;code&gt;content/posts/2025-08-15-1.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;English version: &lt;code&gt;content/posts/2025-08-15-1.en.md&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this configuration, articles can be accessed via the following URLs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Japanese version: &lt;code&gt;https://masutaka.net/2025-08-15-1/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;English version: &lt;code&gt;https://masutaka.net/en/2025-08-15-1/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Multilingual Implementation Strategy
&lt;/h2&gt;

&lt;p&gt;When implementing multilingual support, I followed this approach:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Maintain Existing URLs Unchanged
&lt;/h3&gt;

&lt;p&gt;The most important policy was to absolutely preserve all existing URLs. Given that this blog has been operational for over 20 years, any changes to URLs would cause numerous broken links.&lt;/p&gt;

&lt;p&gt;Therefore, I kept the Japanese version as is and added an &lt;code&gt;/en/&lt;/code&gt; prefix only for the English version.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/index.html&lt;/code&gt; - Almost unchanged, with &lt;code&gt;/en/index.html&lt;/code&gt; added&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/index.xml&lt;/code&gt; - Unchanged, with &lt;code&gt;/en/index.xml&lt;/code&gt; added&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/sitemap.xml&lt;/code&gt; - Changed to reference &lt;code&gt;/ja/sitemap.xml&lt;/code&gt; and &lt;code&gt;/en/sitemap.xml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/llms.txt&lt;/code&gt;, &lt;code&gt;/llms-full.txt&lt;/code&gt; - Unchanged, with &lt;code&gt;/en/llms.txt&lt;/code&gt; and &lt;code&gt;/en/llms-full.txt&lt;/code&gt; added

&lt;ul&gt;
&lt;li&gt;See reference: &lt;a href="https://masutaka.net/en/2025-05-18-1/" rel="noopener noreferrer"&gt;2025-05-18-1&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;👉 The behavior described above is achieved through the configuration settings for &lt;code&gt;defaultContentLanguage&lt;/code&gt;, &lt;code&gt;[languages.ja]&lt;/code&gt;, and &lt;code&gt;[languages.en]&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Keep English Menu Options in the Top Right Minimal
&lt;/h3&gt;

&lt;p&gt;The Device, History, and About pages contain content that is primarily Japanese-specific with limited value in translation, so I decided to skip them this time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Japanese Version&lt;/strong&gt;: &lt;a href="https://masutaka.net/archives/" rel="noopener noreferrer"&gt;Archive&lt;/a&gt;, &lt;a href="https://masutaka.net/tags/" rel="noopener noreferrer"&gt;Tags&lt;/a&gt;, &lt;a href="https://masutaka.net/search/" rel="noopener noreferrer"&gt;Search&lt;/a&gt;, &lt;a href="https://masutaka.net/device/" rel="noopener noreferrer"&gt;Device&lt;/a&gt;, &lt;a href="https://masutaka.net/history/" rel="noopener noreferrer"&gt;History&lt;/a&gt;, &lt;a href="https://masutaka.net/about/" rel="noopener noreferrer"&gt;About&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;English Version&lt;/strong&gt;: Only includes &lt;a href="https://masutaka.net/en/archives/" rel="noopener noreferrer"&gt;Archive&lt;/a&gt;, &lt;a href="https://masutaka.net/en/tags/" rel="noopener noreferrer"&gt;Tags&lt;/a&gt;, &lt;a href="https://masutaka.net/en/search/" rel="noopener noreferrer"&gt;Search&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Implementation Details
&lt;/h2&gt;

&lt;p&gt;Here's an overview of the actual changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Modifications to config.toml
&lt;/h3&gt;

&lt;p&gt;Added language configuration settings and corresponding menu settings for each language.&lt;/p&gt;

&lt;p&gt;👉 The diff and the updated config.toml are attached in the Appendix.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Creating English versions of static pages
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;content/archives.en.md&lt;/code&gt; - Archives page&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;content/search.en.md&lt;/code&gt; - Search page&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;content/privacy.en.md&lt;/code&gt; - Privacy Policy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⚠️ Initially, I attempted to handle this by creating symbolic links from &lt;code&gt;*.en.md&lt;/code&gt; to &lt;code&gt;*.md&lt;/code&gt;, but Hugo did not recognize these links.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Modifying custom shortcodes
&lt;/h3&gt;

&lt;p&gt;Updated the custom &lt;code&gt;post&lt;/code&gt; shortcode to accept an &lt;code&gt;lang&lt;/code&gt; argument.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;layouts/shortcodes/post.html:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{{- $id := .Get "id" | default (.Get 0) -}}
{{- $lang := .Get "lang" | default .Page.Lang -}}
{{- $url := relref . (dict "path" $id "lang" $lang) -}}
{{- $title := .Get "title" -}}

&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"{{ $url }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ with $title }}{{ $title }}{{ else }}[{{ $id }}]{{ end }}&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example usage:&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="c"&gt;&amp;lt;!-- If you specify an article ID as an argument, it generates a link in the current language --&amp;gt;&lt;/span&gt;
{{&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nt"&gt;post&lt;/span&gt; &lt;span class="err"&gt;"2025&lt;/span&gt;&lt;span class="na"&gt;-09-23-1&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;}}
&lt;span class="c"&gt;&amp;lt;!-- -&amp;gt; If the current language is English, this will generate a link to /en/2025-09-23-1/ --&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Adding the 'lang' argument creates an explicit link to another language --&amp;gt;&lt;/span&gt;
{{&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nt"&gt;post&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"2025-09-23-1"&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ja"&lt;/span&gt; &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;}}
&lt;span class="c"&gt;&amp;lt;!-- -&amp;gt; If the current language is English, this will generate a link to /2025-09-23-1/ --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Translation Support for Custom Partials
&lt;/h3&gt;

&lt;p&gt;Since some translations were needed in &lt;code&gt;layouts/partials/*.html&lt;/code&gt;, I created &lt;code&gt;i18n/ja.toml&lt;/code&gt; and &lt;code&gt;i18n/en.toml&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;Usage example:&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;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/"&lt;/span&gt; &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"_blank"&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"noopener"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ i18n "sendMessage" }}&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;i18n/ja.toml:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;sendMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"メッセージ送信"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;i18n/en.toml:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;sendMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Send Message"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I successfully added multilingual support to this Hugo-built blog. The implementation was cleaner than I expected 👍&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All existing URLs remained unchanged, limiting the impact to the &lt;code&gt;/en/&lt;/code&gt; directory&lt;/li&gt;
&lt;li&gt;Achieved using only Hugo's standard features with minimal customization&lt;/li&gt;
&lt;li&gt;Reduced maintenance costs by keeping the English menu minimal&lt;/li&gt;
&lt;li&gt;sitemap.xml and llms.txt were automatically multilingualized&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Moving forward, I'll translate new articles as needed and use &lt;a href="https://masutaka.net/en/" rel="noopener noreferrer"&gt;https://masutaka.net/en/&lt;/a&gt; as the original source while cross-posting to dev.to, Hashnode, and Medium.&lt;/p&gt;

&lt;p&gt;While I'm not sure if there's actually an English-speaking audience, I'm satisfied with the result. With recent AI translation being fairly accurate, it's not that difficult anyway 😎&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/content-management/multilingual/" rel="noopener noreferrer"&gt;Multilingual mode - Hugo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Appendix
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Changes to config.toml
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/config.toml b/config.toml
index 0830be8b..a6bae12c 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/config.toml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/config.toml
&lt;/span&gt;&lt;span class="p"&gt;@@ -7,7 +7,6 @@&lt;/span&gt; googleAnalytics = "G-K28CQCC064"
 hasCJKLanguage = true
 languageCode = "ja"
 theme = "papermod"
&lt;span class="gd"&gt;-title = "マスタカの ChangeLog メモ"
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt; [permalinks]
   posts = "/:filename"
&lt;span class="p"&gt;@@ -27,14 +26,12 @@&lt;/span&gt; title = "マスタカの ChangeLog メモ"
   isPlainText = true
   mediaType = "text/plain"
   rel = "alternate"
&lt;span class="gd"&gt;-  root = true
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt; [outputFormats.llmsfull]
   baseName = "llms-full"
   isPlainText = true
   mediaType = "text/plain"
   rel = "alternate"
&lt;span class="gd"&gt;-  root = true
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt; #
 # papermod configuration
&lt;span class="p"&gt;@@ -51,14 +48,9 @@&lt;/span&gt; title = "マスタカの ChangeLog メモ"
   author = "masutaka"
   comments = true
   defaultTheme = "auto"
&lt;span class="gd"&gt;-  description = "マスタカの変更履歴が記録されていくブログです。"
&lt;/span&gt;   showtoc = true
   tocopen = true
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-[params.homeInfoParams]
-  Title = "マスタカネット"
-  Content = "マスタカの変更履歴が記録されていくブログです。"
-
&lt;/span&gt; [params.assets]
   theme_color = "#ffffff"
   msapplication_TileColor = "#da532c"
&lt;span class="p"&gt;@@ -79,46 +71,81 @@&lt;/span&gt; title = "マスタカの ChangeLog メモ"
 [[params.socialIcons]]
   name =  "GitHub"
   url = "https://github.com/masutaka"
&lt;span class="gd"&gt;-[[params.socialIcons]]
-  name =  "Dev"
-  url = "https://dev.to/masutaka"
-[[params.socialIcons]]
-  name =  "Hashnode"
-  url = "https://masutaka.hashnode.dev/"
-[[params.socialIcons]]
-  name =  "Medium"
-  url = "https://medium.com/@masutaka"
&lt;/span&gt; [[params.socialIcons]]
   name =  "RSS"
   url = "/index.xml"
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-[[menu.main]]
&lt;/span&gt;&lt;span class="gi"&gt;+#
+# Multilingual
+#
+
+[languages.ja]
+  weight = 1
+  languageName = "日本語"
+  title = "マスタカの ChangeLog メモ"
+
+[languages.ja.params]
+  description = "マスタカの変更履歴が記録されていくブログです。"
+
+[languages.ja.params.homeInfoParams]
+  Title = "マスタカネット"
+  Content = "マスタカの変更履歴が記録されていくブログです。"
+
+[[languages.ja.menu.main]]
&lt;/span&gt;   identifier = "archives"
   name = "Archive"
   url = "/archives/"
   weight = 1
&lt;span class="gd"&gt;-[[menu.main]]
&lt;/span&gt;&lt;span class="gi"&gt;+[[languages.ja.menu.main]]
&lt;/span&gt;   identifier = "tags"
   name = "Tags"
   url = "/tags/"
   weight = 2
&lt;span class="gd"&gt;-[[menu.main]]
&lt;/span&gt;&lt;span class="gi"&gt;+[[languages.ja.menu.main]]
&lt;/span&gt;   identifier = "search"
   name = "Search"
   url = "/search/"
   weight = 3
&lt;span class="gd"&gt;-[[menu.main]]
&lt;/span&gt;&lt;span class="gi"&gt;+[[languages.ja.menu.main]]
&lt;/span&gt;   identifier = "device"
   name = "Device"
   url = "/device/"
   weight = 4
&lt;span class="gd"&gt;-[[menu.main]]
&lt;/span&gt;&lt;span class="gi"&gt;+[[languages.ja.menu.main]]
&lt;/span&gt;   identifier = "history"
   name = "History"
   url = "/history/"
   weight = 5
&lt;span class="gd"&gt;-[[menu.main]]
&lt;/span&gt;&lt;span class="gi"&gt;+[[languages.ja.menu.main]]
&lt;/span&gt;   identifier = "about"
   name = "About"
   url = "/about/"
   weight = 6
&lt;span class="gi"&gt;+
+[languages.en]
+  weight = 2
+  languageName = "English"
+  title = "Masutaka's ChangeLog Memo"
+
+[languages.en.params]
+  description = "This is a blog that records Masutaka's change history."
+
+[languages.en.params.homeInfoParams]
+  Title = "Masutaka Net"
+  Content = "This is a blog that records Masutaka's change history."
+
+[[languages.en.menu.main]]
+  identifier = "archives"
+  name = "Archive"
+  url = "/en/archives/"
+  weight = 1
+[[languages.en.menu.main]]
+  identifier = "tags"
+  name = "Tags"
+  url = "/en/tags/"
+  weight = 2
+[[languages.en.menu.main]]
+  identifier = "search"
+  name = "Search"
+  url = "/en/search/"
+  weight = 3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  config.toml after multilingualization
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;baseURL&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://masutaka.net/"&lt;/span&gt;
&lt;span class="py"&gt;defaultContentLanguage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ja"&lt;/span&gt;
&lt;span class="py"&gt;disablePathToLower&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;enableEmoji&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;enableRobotsTXT&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;googleAnalytics&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"G-K28CQCC064"&lt;/span&gt;
&lt;span class="py"&gt;hasCJKLanguage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;languageCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ja"&lt;/span&gt;
&lt;span class="py"&gt;theme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"papermod"&lt;/span&gt;

&lt;span class="nn"&gt;[permalinks]&lt;/span&gt;
  &lt;span class="py"&gt;posts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/:filename"&lt;/span&gt;

&lt;span class="nn"&gt;[taxonomies]&lt;/span&gt;
  &lt;span class="py"&gt;tag&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"tags"&lt;/span&gt;

&lt;span class="nn"&gt;[markup.goldmark.renderer]&lt;/span&gt;
  &lt;span class="py"&gt;hardWraps&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="py"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[outputs]&lt;/span&gt;
  &lt;span class="py"&gt;home&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"rss"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"llms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"llmsfull"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[outputFormats.llms]&lt;/span&gt;
  &lt;span class="py"&gt;baseName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"llms"&lt;/span&gt;
  &lt;span class="py"&gt;isPlainText&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="py"&gt;mediaType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"text/plain"&lt;/span&gt;
  &lt;span class="py"&gt;rel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"alternate"&lt;/span&gt;

&lt;span class="nn"&gt;[outputFormats.llmsfull]&lt;/span&gt;
  &lt;span class="py"&gt;baseName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"llms-full"&lt;/span&gt;
  &lt;span class="py"&gt;isPlainText&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="py"&gt;mediaType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"text/plain"&lt;/span&gt;
  &lt;span class="py"&gt;rel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"alternate"&lt;/span&gt;

&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# papermod configuration&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;

&lt;span class="nn"&gt;[params]&lt;/span&gt;
  &lt;span class="py"&gt;AmazonJpAffiliateID&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"masutaka04-22"&lt;/span&gt;
  &lt;span class="py"&gt;DateFormat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2006-01-02 (Mon)"&lt;/span&gt;
  &lt;span class="py"&gt;ShowAllPagesInArchive&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="py"&gt;ShowCodeCopyButtons&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="py"&gt;ShowFullTextinRSS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="py"&gt;ShowPageNums&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="py"&gt;ShowPostNavLinks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="py"&gt;author&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"masutaka"&lt;/span&gt;
  &lt;span class="py"&gt;comments&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="py"&gt;defaultTheme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"auto"&lt;/span&gt;
  &lt;span class="py"&gt;showtoc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="py"&gt;tocopen&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[params.assets]&lt;/span&gt;
  &lt;span class="py"&gt;theme_color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"#ffffff"&lt;/span&gt;
  &lt;span class="py"&gt;msapplication_TileColor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"#da532c"&lt;/span&gt;

&lt;span class="nn"&gt;[params.social]&lt;/span&gt;
  &lt;span class="py"&gt;fediverse_creator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"@masutaka@mstdn.love"&lt;/span&gt;
  &lt;span class="py"&gt;twitter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"@masutaka"&lt;/span&gt;

&lt;span class="nn"&gt;[[params.socialIcons]]&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;  &lt;span class="s"&gt;"Mastodon"&lt;/span&gt;
  &lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://mstdn.love/@masutaka"&lt;/span&gt;
&lt;span class="nn"&gt;[[params.socialIcons]]&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;  &lt;span class="s"&gt;"Bluesky"&lt;/span&gt;
  &lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://bsky.app/profile/masutaka.net"&lt;/span&gt;
&lt;span class="nn"&gt;[[params.socialIcons]]&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;  &lt;span class="s"&gt;"Twitter"&lt;/span&gt;
  &lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://twitter.com/masutaka"&lt;/span&gt;
&lt;span class="nn"&gt;[[params.socialIcons]]&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;  &lt;span class="s"&gt;"GitHub"&lt;/span&gt;
  &lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://github.com/masutaka"&lt;/span&gt;
&lt;span class="nn"&gt;[[params.socialIcons]]&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;  &lt;span class="s"&gt;"RSS"&lt;/span&gt;
  &lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/index.xml"&lt;/span&gt;

&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# Multilingual&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;

&lt;span class="nn"&gt;[languages.ja]&lt;/span&gt;
  &lt;span class="py"&gt;weight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="py"&gt;languageName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"日本語"&lt;/span&gt;
  &lt;span class="py"&gt;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"マスタカの ChangeLog メモ"&lt;/span&gt;

&lt;span class="nn"&gt;[languages.ja.params]&lt;/span&gt;
  &lt;span class="py"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"マスタカの変更履歴が記録されていくブログです。"&lt;/span&gt;

&lt;span class="nn"&gt;[languages.ja.params.homeInfoParams]&lt;/span&gt;
  &lt;span class="py"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"マスタカネット"&lt;/span&gt;
  &lt;span class="py"&gt;Content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"マスタカの変更履歴が記録されていくブログです。"&lt;/span&gt;

&lt;span class="nn"&gt;[[languages.ja.menu.main]]&lt;/span&gt;
  &lt;span class="py"&gt;identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"archives"&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Archive"&lt;/span&gt;
  &lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/archives/"&lt;/span&gt;
  &lt;span class="py"&gt;weight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="nn"&gt;[[languages.ja.menu.main]]&lt;/span&gt;
  &lt;span class="py"&gt;identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"tags"&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Tags"&lt;/span&gt;
  &lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/tags/"&lt;/span&gt;
  &lt;span class="py"&gt;weight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="nn"&gt;[[languages.ja.menu.main]]&lt;/span&gt;
  &lt;span class="py"&gt;identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"search"&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Search"&lt;/span&gt;
  &lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/search/"&lt;/span&gt;
  &lt;span class="py"&gt;weight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="nn"&gt;[[languages.ja.menu.main]]&lt;/span&gt;
  &lt;span class="py"&gt;identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"device"&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Device"&lt;/span&gt;
  &lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/device/"&lt;/span&gt;
  &lt;span class="py"&gt;weight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="nn"&gt;[[languages.ja.menu.main]]&lt;/span&gt;
  &lt;span class="py"&gt;identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"history"&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"History"&lt;/span&gt;
  &lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/history/"&lt;/span&gt;
  &lt;span class="py"&gt;weight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="nn"&gt;[[languages.ja.menu.main]]&lt;/span&gt;
  &lt;span class="py"&gt;identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"about"&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"About"&lt;/span&gt;
  &lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/about/"&lt;/span&gt;
  &lt;span class="py"&gt;weight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;

&lt;span class="nn"&gt;[languages.en]&lt;/span&gt;
  &lt;span class="py"&gt;weight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="py"&gt;languageName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"English"&lt;/span&gt;
  &lt;span class="py"&gt;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Masutaka's ChangeLog Memo"&lt;/span&gt;

&lt;span class="nn"&gt;[languages.en.params]&lt;/span&gt;
  &lt;span class="py"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"This is a blog that records Masutaka's change history."&lt;/span&gt;

&lt;span class="nn"&gt;[languages.en.params.homeInfoParams]&lt;/span&gt;
  &lt;span class="py"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Masutaka Net"&lt;/span&gt;
  &lt;span class="py"&gt;Content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"This is a blog that records Masutaka's change history."&lt;/span&gt;

&lt;span class="nn"&gt;[[languages.en.menu.main]]&lt;/span&gt;
  &lt;span class="py"&gt;identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"archives"&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Archive"&lt;/span&gt;
  &lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/en/archives/"&lt;/span&gt;
  &lt;span class="py"&gt;weight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="nn"&gt;[[languages.en.menu.main]]&lt;/span&gt;
  &lt;span class="py"&gt;identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"tags"&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Tags"&lt;/span&gt;
  &lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/en/tags/"&lt;/span&gt;
  &lt;span class="py"&gt;weight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="nn"&gt;[[languages.en.menu.main]]&lt;/span&gt;
  &lt;span class="py"&gt;identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"search"&lt;/span&gt;
  &lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Search"&lt;/span&gt;
  &lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/en/search/"&lt;/span&gt;
  &lt;span class="py"&gt;weight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>hugo</category>
    </item>
    <item>
      <title>Migration from Pocket and Hatena Bookmark to Raindrop.io (and Creating helm-raindrop.el)</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Fri, 15 Aug 2025 10:39:11 +0000</pubDate>
      <link>https://dev.to/masutaka/migration-from-pocket-and-hatena-bookmark-to-raindropio-and-creating-helm-raindropel-5f76</link>
      <guid>https://dev.to/masutaka/migration-from-pocket-and-hatena-bookmark-to-raindropio-and-creating-helm-raindropel-5f76</guid>
      <description>&lt;p&gt;I've migrated from &lt;a href="https://getpocket.com/" rel="noopener noreferrer"&gt;Pocket&lt;/a&gt; and &lt;a href="https://b.hatena.ne.jp/" rel="noopener noreferrer"&gt;Hatena Bookmark (Hatebu)&lt;/a&gt;, services I had been using for over a decade, to a new bookmarking service: Raindrop.io. I've also created helm-raindrop.el so I can comfortably search from Emacs as before.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Raindrop.io?
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;a href="https://raindrop.io/" rel="noopener noreferrer"&gt;https://raindrop.io/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Raindrop.io is a bookmarking service with a modern design and rich features.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Organize bookmarks with collections&lt;/li&gt;
&lt;li&gt;Tagging and smart filters&lt;/li&gt;
&lt;li&gt;Full-text search (premium plan only)&lt;/li&gt;
&lt;li&gt;Browser extensions, mobile apps&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://raindrop.io/integrations" rel="noopener noreferrer"&gt;Integrations with external services&lt;/a&gt; like IFTTT and Zapier&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.raindrop.io/" rel="noopener noreferrer"&gt;REST API&lt;/a&gt; for developers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The backend is proprietary, but everything else is open-source (OSS) and maintained by a single person, &lt;a href="https://github.com/exentrich" rel="noopener noreferrer"&gt;Rustem Mussabekov&lt;/a&gt;, who lives in Kazakhstan.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/raindropio" rel="noopener noreferrer"&gt;
        raindropio
      &lt;/a&gt; / &lt;a href="https://github.com/raindropio/app" rel="noopener noreferrer"&gt;
        app
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Source code of web app and browser extensions
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/raindropio" rel="noopener noreferrer"&gt;
        raindropio
      &lt;/a&gt; / &lt;a href="https://github.com/raindropio/mobile" rel="noopener noreferrer"&gt;
        mobile
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Official Raindrop.io mobile app for Android
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/raindropio" rel="noopener noreferrer"&gt;
        raindropio
      &lt;/a&gt; / &lt;a href="https://github.com/raindropio/desktop" rel="noopener noreferrer"&gt;
        desktop
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Official Raindrop.io Desktop App
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Migration Pocket and Hatebu to Raindrop.io
&lt;/h2&gt;

&lt;p&gt;On July 8, 2025, Pocket was discontinued. I had used it as a "read-it-later" service for many years, so I had to find an alternative.&lt;/p&gt;

&lt;p&gt;I had also been using Hatebu for a full 16 years since 2009, but since I've been distancing myself from social media in recent years, I decided to switch to a unified service.&lt;/p&gt;

&lt;p&gt;Here's why I chose Raindrop.io:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The free plan is more than enough (unlimited bookmarks)&lt;/li&gt;
&lt;li&gt;I can use it as a "read-it-later" service, just like Pocket&lt;/li&gt;
&lt;li&gt;I can also use it as a persistent bookmarking service, like Hatebu&lt;/li&gt;
&lt;li&gt;The API is public, making it easy to create my own tools&lt;/li&gt;
&lt;li&gt;It has a data export feature, so there's less worry about vendor lock-in&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The good things about Raindrop.io
&lt;/h2&gt;

&lt;p&gt;After using it, I found several convenient features.&lt;/p&gt;

&lt;h3&gt;
  
  
  The free plan is practical
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;URLs can be edited&lt;/strong&gt; - You can correct the URL later even if it changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collections feature&lt;/strong&gt; - You can organize bookmarks like folders and set them to be public or private individually&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nested collections&lt;/strong&gt; - You can set up a hierarchical structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Highlighting feature&lt;/strong&gt; - You can mark important parts of an article&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile app&lt;/strong&gt; - In addition to the web version, browser extensions, and desktop version, there are also apps for iOS and Android&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The paid plan's automated web archiving is convenient
&lt;/h3&gt;

&lt;p&gt;🔗 &lt;a href="https://help.raindrop.io/premium-features" rel="noopener noreferrer"&gt;https://help.raindrop.io/premium-features&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI-powered suggestions&lt;/strong&gt; - It suggests appropriate collections and tags when saving and suggests merging or renaming similar tags&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full-text search&lt;/strong&gt; - It performs a full-text search on the content of saved articles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated web archiving feature&lt;/strong&gt; - It permanently saves content even if the site disappears&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reminder feature&lt;/strong&gt; - It notifies you of a specific bookmark via the app or email at a specified time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Annotation feature&lt;/strong&gt; - You can add notes to highlights&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detection of duplicate and broken links&lt;/strong&gt; - Makes maintenance easier (?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated backup&lt;/strong&gt; - You can manage versions by linking with Google Drive&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extended upload limit&lt;/strong&gt; - You can upload up to 10GB of images, videos, and PDFs per month&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I tried the paid plan and found the automated web archiving to be very convenient.&lt;/p&gt;

&lt;p&gt;I can't help but fix broken links when I find them. I know some might argue that if you can't find them, you don't have to maintain them...&lt;/p&gt;

&lt;p&gt;While fixing them, I realized that many of my past bookmarks were inaccessible. With Raindrop.io's automated web archiving, a new reason is added to "read-it-later" or "bookmark": "it'll be readable forever."&lt;/p&gt;

&lt;p&gt;The current paid plan is $33.04 per year (tax included), which is about $3.54 per month (tax included), so it's not a bad price.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bad things about Raindrop.io
&lt;/h2&gt;

&lt;p&gt;It's not a perfect service, so there are a few points that are difficult to use.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The app icon is plain&lt;/strong&gt; - It's difficult to spot in my smartphone's share menu, and I often have to search for it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The saving process is a bit sluggish&lt;/strong&gt; - It feels like it takes an extra 1-2 seconds to save a bookmark compared to Pocket&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No archiving feature&lt;/strong&gt; - If you're looking for a 'mark as read' feature like Pocket's, you'll need to create a dedicated collection and manually move bookmarks to it (which requires two taps)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, even with these points, it's still a good and very usable service.&lt;/p&gt;

&lt;h2&gt;
  
  
  I created helm-raindrop.el
&lt;/h2&gt;

&lt;p&gt;For this migration, I created helm-raindrop.el, which allows you to search and browse Raindrop.io bookmarks from Emacs.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/masutaka" rel="noopener noreferrer"&gt;
        masutaka
      &lt;/a&gt; / &lt;a href="https://github.com/masutaka/emacs-helm-raindrop" rel="noopener noreferrer"&gt;
        emacs-helm-raindrop
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Helm interface for Raindrop.io
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;I'm not following what's next after Helm, but since the main function of helm-raindrop.el is to create a cache file &lt;code&gt;~/.emacs.d/helm-raindrop&lt;/code&gt; with the REST API, I don't think it will be difficult to migrate later.&lt;/p&gt;

&lt;p&gt;Actually, I had a similar tool for Hatebu, and its extreme convenience was one of the reasons I was hesitant to migrate.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/masutaka" rel="noopener noreferrer"&gt;
        masutaka
      &lt;/a&gt; / &lt;a href="https://github.com/masutaka/emacs-helm-hatena-bookmark" rel="noopener noreferrer"&gt;
        emacs-helm-hatena-bookmark
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Helm interface for Hatena::Bookmark
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Being able to search bookmarks quickly from Emacs is quite comfortable. For example, if I think, "where was that article?", I just press &lt;code&gt;⌘-b&lt;/code&gt; and enter a search word. That's what my &lt;a href="https://github.com/masutaka/dotfiles-public/blob/816650d2ea5412ddf9e72925661f65eaed6218f5/.emacs.d/init.el" rel="noopener noreferrer"&gt;init.el&lt;/a&gt; does.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/te8OTzM4V3A"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;I'm not sure if it's a good or bad thing that I can continue to search for bookmarks from Emacs, but for now, I've managed to maintain the status quo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Following the discontinuation of Pocket, I migrated to Raindrop.io, including Hatebu, which I had used for a full 16 years. At first, it was a reluctant migration, but after using it, I found it to be a surprisingly good service.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The free plan is practical, and the paid plan is also reasonably priced at $33.04 per year (tax included)&lt;/li&gt;
&lt;li&gt;The API is public, making it easy to integrate with self-made tools&lt;/li&gt;
&lt;li&gt;It has modern features like AI and automated web archiving&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also created helm-raindrop.el, so I've been able to maintain the ability to search from Emacs.&lt;/p&gt;

&lt;p&gt;I recommend it to anyone looking for a bookmarking service. The free plan is very usable, and migrating from Pocket or Hatebu is easy, so it's a good idea to give it a try.&lt;/p&gt;

&lt;p&gt;I'll continue to use it and will write another article if I discover anything new.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>opensource</category>
      <category>emacs</category>
      <category>emacslisp</category>
    </item>
    <item>
      <title>Self-implemented IFTTT Pro's RSS feed notification feature with AWS serverless architecture</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Sun, 20 Jul 2025 09:49:18 +0000</pubDate>
      <link>https://dev.to/masutaka/self-implemented-ifttt-pros-rss-feed-notification-feature-with-aws-serverless-architecture-2ope</link>
      <guid>https://dev.to/masutaka/self-implemented-ifttt-pros-rss-feed-notification-feature-with-aws-serverless-architecture-2ope</guid>
      <description>&lt;p&gt;For casual information gathering, I've been running a serverless application called masutaka-feed since 2020.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/masutaka" rel="noopener noreferrer"&gt;
        masutaka
      &lt;/a&gt; / &lt;a href="https://github.com/masutaka/masutaka-feed" rel="noopener noreferrer"&gt;
        masutaka-feed
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Notify my feeds of github and hatebu to mastodon and pushover
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Post GitHub private feeds&lt;sup id="fnref1"&gt;1&lt;/sup&gt; to Mastodon

&lt;ul&gt;
&lt;li&gt;Star and follow notifications are also sent to &lt;a href="https://pushover.net/" rel="noopener noreferrer"&gt;Pushover&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Post &lt;a href="https://b.hatena.ne.jp/help/entry/favorite" rel="noopener noreferrer"&gt;Hatena Bookmark favorites&lt;/a&gt; feeds&lt;sup id="fnref2"&gt;2&lt;/sup&gt; to Mastodon&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;These are pieces of information that aren't worth subscribing to seriously with a feed reader, but I want to keep them in my field of view.&lt;/p&gt;

&lt;p&gt;※ Mastodon posts are made to the private account &lt;a href="https://mstdn.love/@masutakafeed" rel="noopener noreferrer"&gt;@masutakafeed@mstdn.love&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TOC
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Previous Architecture Diagram&lt;/li&gt;
&lt;li&gt;Current Architecture Diagram&lt;/li&gt;
&lt;li&gt;
Current Architecture

&lt;ul&gt;
&lt;li&gt;Periodic execution with EventBridge Scheduler&lt;/li&gt;
&lt;li&gt;Lambda function responsibility separation&lt;/li&gt;
&lt;li&gt;Duplicate prevention with DynamoDB&lt;/li&gt;
&lt;li&gt;Configuration management with SAM continues&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;After the migration&lt;/li&gt;

&lt;li&gt;Implemented with Claude Code&lt;/li&gt;

&lt;li&gt;Conclusion&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Previous Architecture Diagram
&lt;/h2&gt;

&lt;p&gt;Previously, I used IFTTT's RSS Feed Integration to detect new items and call Lambda functions via API Gateway.&lt;/p&gt;

&lt;p&gt;However, starting in 2024, RSS Feed Integration became available only with paid IFTTT Pro, so I was reluctantly paying the annual fee of &lt;code&gt;$34.99&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&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.amazonaws.com%2Fuploads%2Farticles%2F5qna2xezjmrud2tabtyh.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.amazonaws.com%2Fuploads%2Farticles%2F5qna2xezjmrud2tabtyh.png" alt="Previous GitHub private feed architecture diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hatena Bookmark:&lt;/strong&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.amazonaws.com%2Fuploads%2Farticles%2Fqbn0zptt84c52lm9gowk.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.amazonaws.com%2Fuploads%2Farticles%2Fqbn0zptt84c52lm9gowk.png" alt="Previous Hatena Bookmark favorites feed architecture diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Current Architecture Diagram
&lt;/h2&gt;

&lt;p&gt;Since I wasn't using IFTTT anywhere else, I really didn't want to continue paying the annual &lt;code&gt;$34.99&lt;/code&gt;, so I completely migrated to an AWS serverless architecture this time.&lt;/p&gt;

&lt;p&gt;The architecture uses EventBridge Scheduler to periodically check feeds and DynamoDB for read state management. Dependencies on external services have been eliminated, and processing is now completed entirely within AWS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&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.amazonaws.com%2Fuploads%2Farticles%2Fwuyhddo2lwcleykmuvzi.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.amazonaws.com%2Fuploads%2Farticles%2Fwuyhddo2lwcleykmuvzi.png" alt="Current GitHub private feed architecture diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hatena Bookmark:&lt;/strong&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.amazonaws.com%2Fuploads%2Farticles%2Fg69u0dnv5ed9gt2bkfmv.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.amazonaws.com%2Fuploads%2Farticles%2Fg69u0dnv5ed9gt2bkfmv.png" alt="Current Hatena Bookmark favorites feed architecture diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Current Architecture
&lt;/h2&gt;

&lt;p&gt;In the new architecture, the following components work together:&lt;/p&gt;

&lt;h3&gt;
  
  
  Periodic execution with EventBridge Scheduler
&lt;/h3&gt;

&lt;p&gt;GitHub private feeds are checked every 5 minutes, and Hatena Bookmark favorites feeds every 15 minutes, executing the Lambda Subscriber functions shown below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lambda function responsibility separation
&lt;/h3&gt;

&lt;p&gt;Each feed is separated into 2 Lambda functions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Subscriber function: Feed retrieval and new item detection

&lt;ul&gt;
&lt;li&gt;Retrieve RSS/Atom feeds and parse with &lt;a href="https://www.npmjs.com/package/rss-parser" rel="noopener noreferrer"&gt;rss-parser&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Manage read state with DynamoDB&lt;/li&gt;
&lt;li&gt;Call Notifier function if there are new items&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Notifier function: Execute notification processing

&lt;ul&gt;
&lt;li&gt;Filtering processing (GitHub only notifies specific events)&lt;/li&gt;
&lt;li&gt;Post to Mastodon&lt;/li&gt;
&lt;li&gt;Send notifications to Pushover (GitHub only)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Duplicate prevention with DynamoDB
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create tables for each feed

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;code&gt;masutaka-feed-github-state&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Hatena Bookmark: &lt;code&gt;masutaka-feed-hatebu-state&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Set partition keys

&lt;ul&gt;
&lt;li&gt;GitHub: Atom feed, each &lt;code&gt;entry&lt;/code&gt;'s &lt;code&gt;id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Hatena Bookmark: RSS 1.0 feed, each &lt;code&gt;item&lt;/code&gt;'s &lt;code&gt;rdf:about&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Set TTL to automatically delete old records after 30 days&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuration management with SAM continues
&lt;/h3&gt;

&lt;p&gt;As before, I use AWS SAM to manage the configuration of added components.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/masutaka/masutaka-feed/blob/678a0b03028e85bee0b2396a9ad6059fc336faba/samconfig.toml" rel="noopener noreferrer"&gt;samconfig.toml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/masutaka/masutaka-feed/blob/678a0b03028e85bee0b2396a9ad6059fc336faba/template.yaml" rel="noopener noreferrer"&gt;template.yaml&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Environment variables continue to be managed with GitHub Actions Secrets/Variables, and automatic deployment occurs when commits are added to the main branch.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/masutaka/masutaka-feed/blob/678a0b03028e85bee0b2396a9ad6059fc336faba/.github/workflows/deploy.yml" rel="noopener noreferrer"&gt;.github/workflows/deploy.yml&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  After the migration
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Despite increased responsibilities, the code became unexpectedly cleaner&lt;/li&gt;
&lt;li&gt;rss-parser was more convenient than expected - by just defining feed types like &lt;a href="https://github.com/masutaka/masutaka-feed/blob/678a0b03028e85bee0b2396a9ad6059fc336faba/github/subscriber/index.ts#L6-L20" rel="noopener noreferrer"&gt;GitHubFeedItem&lt;/a&gt; and &lt;a href="https://github.com/masutaka/masutaka-feed/blob/678a0b03028e85bee0b2396a9ad6059fc336faba/hatebu/subscriber/index.ts#L6C11-L25" rel="noopener noreferrer"&gt;HatebuFeedItem&lt;/a&gt;, parsing became simple

&lt;ul&gt;
&lt;li&gt;As a result, I was able to abandon &lt;a href="https://github.com/masutaka/masutaka-feed/blob/da3954210815138d76e9bf413cc344f96c015f40/hatebu/index.js#L72-L87" rel="noopener noreferrer"&gt;HTML extraction using regular expressions&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;No longer need to pay IFTTT Pro's annual &lt;code&gt;$34.99&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;AWS costs stay within the free tier&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Running error-free for about 2 weeks&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Implemented with Claude Code
&lt;/h2&gt;

&lt;p&gt;After starting to pay for IFTTT Pro, I had the concept in my head, but since it was essentially refactoring, I couldn't get motivated and left it alone.&lt;/p&gt;

&lt;p&gt;Claude Code helped me overcome that lack of motivation, and I made great use of it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Migration from JavaScript to TypeScript

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/masutaka/masutaka-feed/pull/74" rel="noopener noreferrer"&gt;https://github.com/masutaka/masutaka-feed/pull/74&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Migration from IFTTT Pro to AWS self-implementation

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/masutaka/masutaka-feed/pull/77" rel="noopener noreferrer"&gt;https://github.com/masutaka/masutaka-feed/pull/77&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;💡 Each commit message in the pull requests contains Claude Code prompts.&lt;/p&gt;

&lt;p&gt;Resource definitions in template.yaml are tedious to research, but Claude Code wrote them in one shot. Of course, I verified them afterwards.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EventBridge Schedulers definition&lt;/li&gt;
&lt;li&gt;DynamoDB Tables definition&lt;/li&gt;
&lt;li&gt;IAM Role settings for each resource&lt;/li&gt;
&lt;li&gt;CloudWatch Alarm monitoring definitions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By self-implementing IFTTT Pro's feed notification feature with AWS serverless architecture, I was able to gain the following benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cost reduction: Annual fixed cost of &lt;code&gt;$34.99&lt;/code&gt; became essentially zero&lt;/li&gt;
&lt;li&gt;Transparency gained: Full control over what items are added to feeds and what items have been marked as read&lt;/li&gt;
&lt;li&gt;Enhanced monitoring: Can set up monitoring for feed subscriptions and detect failures immediately&lt;/li&gt;
&lt;li&gt;Code optimization: Optimized feed parsing using rss-parser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additionally, by utilizing Claude Code, I was able to significantly lower the motivation barrier for tasks like migrating from JavaScript to TypeScript and creating SAM templates.&lt;/p&gt;

&lt;p&gt;I will continue to operate masutaka-feed at a low altitude going forward.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Example: &lt;a href="https://github.com/masutaka.private.atom?token=xxxx" rel="noopener noreferrer"&gt;https://github.com/masutaka.private.atom?token=xxxx&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;Example: &lt;a href="https://b.hatena.ne.jp/masutaka26/favorite.rss?key=xxxx" rel="noopener noreferrer"&gt;https://b.hatena.ne.jp/masutaka26/favorite.rss?key=xxxx&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>rss</category>
      <category>aws</category>
      <category>github</category>
      <category>mastodon</category>
    </item>
    <item>
      <title>Added llms.txt and llms-full.txt to My Hugo-built Website</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Sun, 18 May 2025 12:38:45 +0000</pubDate>
      <link>https://dev.to/masutaka/added-llmstxt-and-llms-fulltxt-to-my-hugo-built-website-343i</link>
      <guid>https://dev.to/masutaka/added-llmstxt-and-llms-fulltxt-to-my-hugo-built-website-343i</guid>
      <description>&lt;h2&gt;
  
  
  What is llms.txt?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;llms.txt&lt;/code&gt; is a Markdown-formatted text file proposed to address the limitation that LLMs have small context windows and cannot process entire websites.&lt;/p&gt;

&lt;p&gt;It was proposed on September 3, 2024, by Jeremy Howard of Answer.AI at &lt;a href="https://llmstxt.org/" rel="noopener noreferrer"&gt;https://llmstxt.org/&lt;/a&gt;. It is not defined in an RFC like robots.txt.&lt;/p&gt;

&lt;p&gt;The format includes certain specifications such as the website name in an H1 section, a brief summary, and a list of links in H2 sections.&lt;br&gt;
Example: &lt;a href="https://llmstxt.org/llms.txt" rel="noopener noreferrer"&gt;https://llmstxt.org/llms.txt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's also &lt;code&gt;llms-full.txt&lt;/code&gt;, which contains all website information.&lt;br&gt;
Example: &lt;a href="https://developers.cloudflare.com/llms-full.txt" rel="noopener noreferrer"&gt;https://developers.cloudflare.com/llms-full.txt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are directory sites for llms.txt as well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://llmstxt.site/" rel="noopener noreferrer"&gt;https://llmstxt.site/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://directory.llmstxt.cloud/" rel="noopener noreferrer"&gt;https://directory.llmstxt.cloud/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://llmstxthub.com/" rel="noopener noreferrer"&gt;https://llmstxthub.com/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The llms.txt and llms-full.txt I Added
&lt;/h2&gt;

&lt;p&gt;I've added both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://masutaka.net/llms.txt" rel="noopener noreferrer"&gt;https://masutaka.net/llms.txt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://masutaka.net/llms-full.txt" rel="noopener noreferrer"&gt;https://masutaka.net/llms-full.txt&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How to Configure in Hugo
&lt;/h2&gt;

&lt;p&gt;The necessary work involves modifying or creating just three files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;config.toml (modify)&lt;/li&gt;
&lt;li&gt;layouts/index.llms.txt (create new)&lt;/li&gt;
&lt;li&gt;layouts/index.llmsfull.txt (create new)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  config.toml
&lt;/h3&gt;

&lt;p&gt;I added the following to config.toml:&lt;/p&gt;

&lt;p&gt;Defining &lt;a href="https://gohugo.io/configuration/output-formats/" rel="noopener noreferrer"&gt;outputFormats&lt;/a&gt; for &lt;code&gt;llms&lt;/code&gt; and &lt;code&gt;llmsfull&lt;/code&gt; while adding them to the default value of &lt;a href="https://gohugo.io/configuration/outputs/" rel="noopener noreferrer"&gt;outputs.home&lt;/a&gt;, which is &lt;code&gt;["html", "rss"]&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[outputs]&lt;/span&gt;
  &lt;span class="py"&gt;home&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"rss"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"llms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"llmsfull"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[outputFormats.llms]&lt;/span&gt;
  &lt;span class="py"&gt;baseName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"llms"&lt;/span&gt;
  &lt;span class="py"&gt;isPlainText&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="py"&gt;mediaType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"text/plain"&lt;/span&gt;
  &lt;span class="py"&gt;rel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"alternate"&lt;/span&gt;
  &lt;span class="py"&gt;root&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[outputFormats.llmsfull]&lt;/span&gt;
  &lt;span class="py"&gt;baseName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"llms-full"&lt;/span&gt;
  &lt;span class="py"&gt;isPlainText&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="py"&gt;mediaType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"text/plain"&lt;/span&gt;
  &lt;span class="py"&gt;rel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"alternate"&lt;/span&gt;
  &lt;span class="py"&gt;root&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  layouts/index.llms.txt
&lt;/h3&gt;

&lt;p&gt;I created &lt;code&gt;layouts/index.llms.txt&lt;/code&gt; as a template for &lt;code&gt;llms.txt&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;# {{ .Site.Title }}

&amp;gt; {{ .Site.Params.Description }}


## Articles
{{ $yearMonthGroups := slice -}}
{{ range where (where (sort (.Site.GetPage "/posts/").Pages "Date" "desc") "Draft" "ne" true) "Sitemap.Disable" "ne" true -}}
{{ $yearMonth := .Date.Format "2006/01" -}}
{{ if not (in $yearMonthGroups $yearMonth) }}
* {{ $yearMonth -}}
{{ $yearMonthGroups = $yearMonthGroups | append $yearMonth -}}
{{ end }}
    * [{{ .Title }}]({{ .Permalink }})
{{- end }}


## Others

{{- $baseURL := .Site.BaseURL | strings.TrimSuffix "/" }}

* [Device]({{ $baseURL }}/device/): 所有デバイス
* [History]({{ $baseURL }}/history/): このサイトの歴史
* [About]({{ $baseURL }}/about/): 自己紹介
* [Privacy Policy]({{ $baseURL }}/privacy/): 当サイトの広告、アフィリエイト、プライバシーポリシー
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since Hugo site structures can vary widely, this is just one example. My blog has both blog posts and other articles, and since other articles don't change much, I hardcoded them in the Others section.&lt;/p&gt;

&lt;h3&gt;
  
  
  layouts/index.llmsfull.txt
&lt;/h3&gt;

&lt;p&gt;I created &lt;code&gt;layouts/index.llmsfull.txt&lt;/code&gt; as a template for &lt;code&gt;llms-full.txt&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;# {{ .Site.Title }}

&amp;gt; {{ .Site.Params.Description }}


{{/* Articles */}}
{{ range where (where (sort (.Site.GetPage "/posts/").Pages "Date" "desc") "Draft" "ne" true) "Sitemap.Disable" "ne" true }}
--------------------------------------------------------------------------------
title: "{{ .Title }}"
date: "{{ .Date.Format "2006-01-02" }}"
--------------------------------------------------------------------------------
{{ replaceRE "{{&amp;lt;\\s*comment\\s*&amp;gt;}}(.|\n)*?{{&amp;lt;\\s*/comment\\s*&amp;gt;}}" "" .RawContent -}}
{{ end -}}


{{/* Others */}}
{{ range slice "device.md" "history.md" "about.md" "privacy.md" -}}
{{ with site.GetPage . -}}
--------------------------------------------------------------------------------
title: "{{ .Title }}"
lastmod: "{{ .Date.Format "2006-01-02" }}"
--------------------------------------------------------------------------------
{{ replaceRE "{{&amp;lt;\\s*comment\\s*&amp;gt;}}(.|\n)*?{{&amp;lt;\\s*/comment\\s*&amp;gt;}}" "" .RawContent -}}
{{ end -}}
{{ end -}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I made the delimiter longer because some articles contained &lt;code&gt;---&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Also, I made sure comments like the following wouldn't be included in llms-full.txt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;{{&lt;span class="nt"&gt;&amp;lt;comment&amp;gt;&lt;/span&gt;}}
This is a Hugo comment. It won't be output as an HTML comment either.
{{&lt;span class="nt"&gt;&amp;lt;/comment&amp;gt;&lt;/span&gt;}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  nginx Configuration
&lt;/h2&gt;

&lt;p&gt;Since llms.txt and llms-full.txt were showing character encoding issues, I added the following to the server directive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;"^/llms(-full)?\.txt"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/usr/share/nginx/html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;charset&lt;/span&gt; &lt;span class="s"&gt;UTF-8&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;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I added llms.txt and llms-full.txt on my Hugo-built blog:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://masutaka.net/llms.txt" rel="noopener noreferrer"&gt;https://masutaka.net/llms.txt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://masutaka.net/llms-full.txt" rel="noopener noreferrer"&gt;https://masutaka.net/llms-full.txt&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Regardless of whether there's demand for it, I'm satisfied with the result.&lt;/p&gt;

&lt;p&gt;As a side note, I initially found a method on &lt;a href="https://discourse.gohugo.io/t/support-for-llms-txt-standard-for-ai-crawlers/53782/3" rel="noopener noreferrer"&gt;Hugo Discourse&lt;/a&gt; that used &lt;code&gt;resources.ExecuteAsTemplate&lt;/code&gt; in &lt;code&gt;layouts/robots.txt&lt;/code&gt; to generate llms.txt. However, I decided not to use this approach since including llms.txt in robots.txt is not yet common practice.&lt;/p&gt;

&lt;p&gt;Another side note: in the past, my blog was created by converting a single ChangeLog-formatted file into HTML using a tool called &lt;a href="https://chalow.org/" rel="noopener noreferrer"&gt;chalow&lt;/a&gt;. Creating llms-full.txt reminded me of those days. It feels like I've come full circle.&lt;/p&gt;

</description>
      <category>hugo</category>
      <category>llm</category>
      <category>llmstxt</category>
    </item>
    <item>
      <title>What We Did to Gain 3,000 GitHub Stars for the Liam Repository</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Mon, 21 Apr 2025 08:30:55 +0000</pubDate>
      <link>https://dev.to/route06/what-we-did-to-gain-3000-github-stars-for-the-liam-repository-54lf</link>
      <guid>https://dev.to/route06/what-we-did-to-gain-3000-github-stars-for-the-liam-repository-54lf</guid>
      <description>&lt;p&gt;Recently, the &lt;a href="https://github.com/liam-hq/liam" rel="noopener noreferrer"&gt;Liam repository&lt;/a&gt; exceeded 3,000 stars just three months after its release. The repository continues to gain stars, alongside increases in traffic, forks, and external contributions.&lt;/p&gt;

&lt;p&gt;These stars didn't accumulate naturally—they were the result of proactive strategies.&lt;/p&gt;

&lt;p&gt;In this article, I'll share our thinking and the strategies we implemented to achieve 3,000 GitHub stars.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Liam ERD?
&lt;/h2&gt;

&lt;p&gt;Liam ERD is the first product under the Liam brand, a tool that easily generates beautiful ER diagrams automatically from database schemas. It's released as open-source software with the goal of growing alongside the community.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://liambx.com/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fliambx.com%2Fimages%2Fliam_erd.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://liambx.com/" rel="noopener noreferrer" class="c-link"&gt;
            Liam ERD
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Automatically generates beautiful and easy-to-read ER diagrams from your database.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fliambx.com%2Ffavicon.ico"&gt;
          liambx.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Setting 3,000 GitHub Stars as a KPI
&lt;/h2&gt;

&lt;p&gt;We set 3,000 GitHub stars as our KPI for the following reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We are currently developing a paid product in the database design space based on Liam ERD&lt;/li&gt;
&lt;li&gt;We wanted to significantly expand recognition for the first product under the Liam brand&lt;/li&gt;
&lt;li&gt;GitHub stars are a public metric that serves as a leading indicator of user trust&lt;sup id="fnref1"&gt;1&lt;/sup&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Strategies Found from Predecessors
&lt;/h2&gt;

&lt;p&gt;Having set GitHub stars as our KPI, what specific actions should we take?&lt;/p&gt;

&lt;p&gt;Throughout my career as a software engineer, I've developed OSS individually and created OSS repositories for companies, but I had never promoted a company's OSS. Also, blatant promotion isn't something I personally prefer.&lt;/p&gt;

&lt;p&gt;I consulted Google and ChatGPT, but ultimately found an article featured on the Star History website.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://www.star-history.com/blog/playbook-for-more-github-stars" rel="noopener noreferrer"&gt;The (Detailed &amp;amp; Creative) Playbook for Getting More GitHub Stars&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"Writing the post I wish I found 6 months ago" was exactly the information I was looking for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategies Implemented to Gain 3,000 GitHub Stars
&lt;/h2&gt;

&lt;p&gt;Basically, I turned everything mentioned in the Star History article into actionable tasks and implemented the ones that were feasible for us.&lt;/p&gt;

&lt;p&gt;Here are the specific strategies we implemented, with links included:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Preparation
&lt;/h3&gt;

&lt;p&gt;Increasing traffic without proper repository preparation would be counterproductive. First, we focused on preparing the repository to welcome interested users.&lt;/p&gt;

&lt;h4&gt;
  
  
  Setting up the GitHub Repository for OSS
&lt;/h4&gt;

&lt;p&gt;We set up the repository following the guidelines in the article below. We tried to include all GitHub recommendations and security measures.&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/route06/sharing-the-oss-publishing-tutorial-created-by-route06-inc-57nl" class="crayons-story__hidden-navigation-link"&gt;Sharing the "OSS Publishing Tutorial" Created by ROUTE06, Inc.&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;
          &lt;a class="crayons-logo crayons-logo--l" href="/route06"&gt;
            &lt;img alt="ROUTE06, Inc. logo" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F9867%2Fc09ac11b-bf4c-44c7-b2bb-470d71c87e5d.jpg" class="crayons-logo__image"&gt;
          &lt;/a&gt;

          &lt;a href="/masutaka" class="crayons-avatar  crayons-avatar--s absolute -right-2 -bottom-2 border-solid border-2 border-base-inverted  "&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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F44998%2Fd9ecea54-0f9a-4fbe-95e6-252288b4fbb1.jpeg" alt="masutaka profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/masutaka" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Takashi Masuda
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Takashi Masuda
                
              
              &lt;div id="story-author-preview-content-2177250" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/masutaka" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F44998%2Fd9ecea54-0f9a-4fbe-95e6-252288b4fbb1.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Takashi Masuda&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;span&gt;
              &lt;span class="crayons-story__tertiary fw-normal"&gt; for &lt;/span&gt;&lt;a href="/route06" class="crayons-story__secondary fw-medium"&gt;ROUTE06, Inc.&lt;/a&gt;
            &lt;/span&gt;
          &lt;/div&gt;
          &lt;a href="https://dev.to/route06/sharing-the-oss-publishing-tutorial-created-by-route06-inc-57nl" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jan 6 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/route06/sharing-the-oss-publishing-tutorial-created-by-route06-inc-57nl" id="article-link-2177250"&gt;
          Sharing the "OSS Publishing Tutorial" Created by ROUTE06, Inc.
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ospo"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ospo&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/github"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;github&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/route06/sharing-the-oss-publishing-tutorial-created-by-route06-inc-57nl" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/route06/sharing-the-oss-publishing-tutorial-created-by-route06-inc-57nl#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            8 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;h4&gt;
  
  
  Organizing the README.md
&lt;/h4&gt;

&lt;p&gt;README.md is the face of the repository.&lt;/p&gt;

&lt;p&gt;To make it intuitively understandable at first glance, we focused on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using attractive animated GIFs to instantly show what the ER diagrams look like&lt;/li&gt;
&lt;li&gt;Creating a clear tagline that concisely summarizes the tool's features&lt;/li&gt;
&lt;li&gt;Using colorful badges created with &lt;a href="https://shields.io/" rel="noopener noreferrer"&gt;Shields.io&lt;/a&gt; to display the tool's status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additionally, to encourage community participation, we included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Links to contribution guidelines and roadmap&lt;/li&gt;
&lt;li&gt;Visualization of contributors using &lt;a href="https://contrib.rocks" rel="noopener noreferrer"&gt;contrib.rocks&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After release, screenshots of the README.md were frequently shared on social media, confirming its role as the repository's face.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Directly Increasing GitHub Stars to 100
&lt;/h3&gt;

&lt;p&gt;Before sharing on social media, we wanted to show that many people were already interested in the repository, so we aimed to reach 100 stars through direct strategies.&lt;/p&gt;

&lt;h4&gt;
  
  
  Sending Kickoff Messages to Friends and Acquaintances
&lt;/h4&gt;

&lt;p&gt;I directly asked friends and acquaintances to star the repository via X (formerly Twitter) DMs and Slack.&lt;/p&gt;

&lt;h4&gt;
  
  
  Making the First Post on X After Exceeding 100 Stars
&lt;/h4&gt;

&lt;p&gt;We had planned to post after getting a few dozen stars, but the response exceeded our expectations, so we posted after surpassing 100 stars.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://x.com/liam_app/status/1881975247613923737" rel="noopener noreferrer"&gt;https://x.com/liam_app/status/1881975247613923737&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what we focused on in this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prepared a video that clearly demonstrates what the tool can do&lt;/li&gt;
&lt;li&gt;Concisely introduced features in bullet points&lt;/li&gt;
&lt;li&gt;Mentioned well-known relevant accounts

&lt;ul&gt;
&lt;li&gt;React Flow quoted and reposted it&lt;sup id="fnref2"&gt;2&lt;/sup&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;As a result, we gained 41 reposts and approximately 30,000 views.&lt;/p&gt;

&lt;p&gt;💡️ Although we are a Japanese team, all our X posts are in English. This is to avoid being perceived as a product only for Japanese users, which could limit its appeal in English-speaking regions. Based on our estimates, the majority of stars seem to come from users outside Japan.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Indirect Strategies After Exceeding 100 GitHub Stars
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Creating Blog Content on the Service Website
&lt;/h4&gt;

&lt;p&gt;We created content for our company blog, including direct or indirect introductions to Liam ERD, list articles, and knowledge-sharing pieces.&lt;/p&gt;

&lt;p&gt;Example: &lt;a href="https://liambx.com/blog/liam-erd-introduction" rel="noopener noreferrer"&gt;Introducing Liam ERD - Liam ERD&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Distributing Blog Content to Various Media
&lt;/h4&gt;

&lt;p&gt;We cross-posted to dev.to, Hashnode, and Medium with canonical URLs set.&lt;/p&gt;

&lt;p&gt;Example: &lt;a href="https://dev.to/route06/generate-beautiful-and-interactive-er-diagrams-with-liam-erd-5d5n"&gt;Generate Beautiful and Interactive ER-Diagrams with Liam ERD - DEV Community&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We posted slightly modified content on Reddit and HackerNoon. As a side note, you can't post to certain subreddits on Reddit without accumulating karma first. It's better to build up karma early.&lt;/p&gt;

&lt;p&gt;Example: &lt;a href="https://hackernoon.com/new-open-source-tool-lets-you-auto-generate-er-diagrams-for-database-visualization" rel="noopener noreferrer"&gt;New Open-Source Tool Lets You Auto-Generate ER-Diagrams for Database Visualization | HackerNoon&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We also posted a translated article on Zenn for the Japanese audience. This article helped generate our first +100 stars, but it resulted in Japan accounting for over 60% of stars by country, raising concerns about our initial hypothesis regarding the potential to be overlooked in English-speaking regions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://zenn.dev/route06/articles/liam-erd-introduction" rel="noopener noreferrer"&gt;Generate Beautiful and Interactive ER Diagrams with Liam ERD - Zenn&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Promoting on Various Sites and Newsletters
&lt;/h4&gt;

&lt;p&gt;In addition to the sites mentioned in the Star History article, we researched other potential promotional sites. Except for Hacker News and Reddit, these were all new to me.&lt;/p&gt;

&lt;p&gt;As of writing, we've been featured on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hacker News

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://news.ycombinator.com/item?id=42789874" rel="noopener noreferrer"&gt;https://news.ycombinator.com/item?id=42789874&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Reddit - r/coolgithubprojects

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.reddit.com/r/coolgithubprojects/comments/1iza4q6/liam_erd_generates_beautiful_and_easytoread_er/" rel="noopener noreferrer"&gt;https://www.reddit.com/r/coolgithubprojects/comments/1iza4q6/liam_erd_generates_beautiful_and_easytoread_er/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;This subreddit doesn't require karma and had a good response with 7 upvotes and 1K views&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;daily.dev

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dly.to/D8CwMwsw2Jb" rel="noopener noreferrer"&gt;https://dly.to/D8CwMwsw2Jb&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Unexpectedly good response with 211 upvotes, 2.5K views, 11 comments, generating our first +100 stars in the Japanese region&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Gitroom

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gitlibrary.club/databases/1" rel="noopener noreferrer"&gt;https://gitlibrary.club/databases/1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;CodeTriage

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.codetriage.com/liam-hq/liam" rel="noopener noreferrer"&gt;https://www.codetriage.com/liam-hq/liam&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Up For Grabs

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://up-for-grabs.net/#/filters?names=563" rel="noopener noreferrer"&gt;https://up-for-grabs.net/#/filters?names=563&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Not many, but generates continuous traffic&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;freestuff.dev

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://freestuff.dev/alternative/liam-erd" rel="noopener noreferrer"&gt;https://freestuff.dev/alternative/liam-erd&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Open Hub

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://openhub.net/p/liam" rel="noopener noreferrer"&gt;https://openhub.net/p/liam&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;G2

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.g2.com/products/liam-erd" rel="noopener noreferrer"&gt;https://www.g2.com/products/liam-erd&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Dev Hunt

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://devhunt.org/tool/liam-erd" rel="noopener noreferrer"&gt;https://devhunt.org/tool/liam-erd&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Paid, but they &lt;a href="https://x.com/search?q=from%3Adevhunt_%20liam&amp;amp;src=typed_query&amp;amp;f=live" rel="noopener noreferrer"&gt;promoted us several times on X&lt;/a&gt; during the featured period&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;It's launched!

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://itslaunched.com/product/liam-erd" rel="noopener noreferrer"&gt;https://itslaunched.com/product/liam-erd&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;To get featured in React Flow's Showcase, we mentioned them several times on X and &lt;a href="https://liambx.com/blog/tuning-edge-animations-reactflow-optimal-performance" rel="noopener noreferrer"&gt;posted about our blog article&lt;/a&gt; &lt;a href="https://x.com/liam_app/status/1885222627817775216" rel="noopener noreferrer"&gt;on X&lt;/a&gt;. Thankfully, they featured us quickly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://reactflow.dev/showcase" rel="noopener noreferrer"&gt;https://reactflow.dev/showcase&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h5&gt;
  
  
  [Column] How We Researched Potential Promotional Sites
&lt;/h5&gt;

&lt;p&gt;While still exploring, here's how we researched:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Used &lt;a href="https://ahrefs.com/" rel="noopener noreferrer"&gt;Ahrefs&lt;/a&gt; to check backlinks of competitors and similar products, adding sites that featured those products to our list of candidates&lt;/li&gt;
&lt;li&gt;Searched directly using ChatGPT and Google&lt;/li&gt;
&lt;li&gt;Sometimes found sites by chance during daily checks of Hacker News, Reddit, etc.

&lt;ul&gt;
&lt;li&gt;For example, we found &lt;a href="https://itslaunched.com" rel="noopener noreferrer"&gt;It's launched!&lt;/a&gt; through &lt;a href="https://news.ycombinator.com/item?id=42712666" rel="noopener noreferrer"&gt;Hacker News' Show HN&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Sometimes found sites through a chain of discoveries:

&lt;ol&gt;
&lt;li&gt;The Star History article mentioned earlier was originally a cross-post from &lt;a href="https://dev.to/livecycle/the-detailed-creative-playbook-for-more-github-stars-5fo5"&gt;this dev.to article&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Read &lt;a href="https://dev.to/livecycle/"&gt;all articles by Livecycle on dev.to&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Learned about &lt;a href="https://devhunt.org/" rel="noopener noreferrer"&gt;Dev Hunt&lt;/a&gt;&lt;sup id="fnref3"&gt;3&lt;/sup&gt; and added it to our candidates&lt;/li&gt;
&lt;li&gt;Dev Hunt founder &lt;a href="https://johnrush.me/" rel="noopener noreferrer"&gt;John Rush&lt;/a&gt; is a serial entrepreneur who creates products solving challenges like ours, and builds more products with his existing ones&lt;/li&gt;
&lt;li&gt;Researched several of his products and added some to our candidates. Among them, &lt;a href="https://osssoftware.org/" rel="noopener noreferrer"&gt;https://osssoftware.org/&lt;/a&gt; was particularly interesting in its small-scale approach&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h4&gt;
  
  
  Introducing at Tech Conferences
&lt;/h4&gt;

&lt;p&gt;So far only in Japan, but team members have introduced Liam ERD at several conferences.&lt;/p&gt;

&lt;p&gt;Example: &lt;a href="https://findy-tools.connpass.com/event/339331/" rel="noopener noreferrer"&gt;Findy Tools 1st Anniversary Celebration&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Adding to GitHub awesome-lists Repositories
&lt;/h4&gt;

&lt;p&gt;We submitted pull requests to relevant 'awesome list' repositories related to Liam ERD. The following repositories merged our requests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/mgramin/awesome-db-tools" rel="noopener noreferrer"&gt;https://github.com/mgramin/awesome-db-tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gramantin/awesome-rails" rel="noopener noreferrer"&gt;https://github.com/gramantin/awesome-rails&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Approaching Influencers
&lt;/h4&gt;

&lt;p&gt;We directly reached out to influencers who introduce OSS projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;@tom_doerr

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://x.com/tom_doerr/status/1888812285491093781" rel="noopener noreferrer"&gt;https://x.com/tom_doerr/status/1888812285491093781&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;@GithubProjects

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://x.com/GithubProjects/status/1907363835910164878" rel="noopener noreferrer"&gt;https://x.com/GithubProjects/status/1907363835910164878&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.instagram.com/p/DH8GbcbvI1N/" rel="noopener noreferrer"&gt;https://www.instagram.com/p/DH8GbcbvI1N/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.threads.net/@githubprojects/post/DH8GdhGTGHm" rel="noopener noreferrer"&gt;https://www.threads.net/@githubprojects/post/DH8GdhGTGHm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h4&gt;
  
  
  Celebrating Milestones
&lt;/h4&gt;

&lt;p&gt;We posted celebratory messages on X when we reached milestone numbers of stars:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;200 stars

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://x.com/liam_app/status/1882291797399101882" rel="noopener noreferrer"&gt;https://x.com/liam_app/status/1882291797399101882&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;500 stars

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://x.com/liam_app/status/1888786665231245525" rel="noopener noreferrer"&gt;https://x.com/liam_app/status/1888786665231245525&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;1,000 stars

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://x.com/liam_app/status/1896390308319080924" rel="noopener noreferrer"&gt;https://x.com/liam_app/status/1896390308319080924&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;3,000 stars

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://x.com/liam_app/status/1910494502873424353" rel="noopener noreferrer"&gt;https://x.com/liam_app/status/1910494502873424353&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Most Effective Strategies
&lt;/h2&gt;

&lt;p&gt;daily.dev and X posts by influencers were particularly effective.&lt;/p&gt;

&lt;p&gt;The most effective was &lt;a href="https://x.com/GithubProjects/status/1907363835910164878" rel="noopener noreferrer"&gt;@GithubProjects' X post in early April&lt;/a&gt;. This resulted in a #2 daily ranking on &lt;a href="https://github.com/trending" rel="noopener noreferrer"&gt;GitHub Trending&lt;/a&gt;, creating the following positive cycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Featured on GitHub Trending&lt;/li&gt;
&lt;li&gt;Massive increase in Liam repository traffic and stars&lt;/li&gt;
&lt;li&gt;Continued featuring on GitHub Trending&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At this stage, we noticed an increase in spontaneous sharing without our involvement.&lt;/p&gt;

&lt;p&gt;It's fair to say that "to increase stars, aim to be featured on GitHub Trending."&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://www.star-history.com/#liam-hq/liam&amp;amp;Date" rel="noopener noreferrer"&gt;https://www.star-history.com/#liam-hq/liam&amp;amp;Date&lt;/a&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.amazonaws.com%2Fuploads%2Farticles%2Fjfuzmd679nweyzzax1x1.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.amazonaws.com%2Fuploads%2Farticles%2Fjfuzmd679nweyzzax1x1.png" alt="Liam ERD - Star History"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I've shared the strategies we implemented to gain 3,000 GitHub stars for the Liam repository.&lt;/p&gt;

&lt;p&gt;As a software engineer, I initially hesitated about actively promoting OSS, but changed my mindset, recognizing that good products don't always spread organically.&lt;/p&gt;

&lt;p&gt;I'm truly grateful for &lt;a href="https://www.star-history.com/blog/playbook-for-more-github-stars" rel="noopener noreferrer"&gt;the Star History article mentioned earlier&lt;/a&gt;. Fortunately, I had the opportunity to directly express my gratitude to the author, Zevi Reinitz&lt;sup id="fnref4"&gt;4&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;This article is just one example, so I can't guarantee that following these exact steps will lead to success. I hope it serves as a useful reference for someone.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;a href="https://runacap.com/ross-index/methodology/" rel="noopener noreferrer"&gt;ROSS Index Methodology - Runa Capital&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;&lt;a href="https://x.com/xyflowdev/status/1882013279998148726" rel="noopener noreferrer"&gt;https://x.com/xyflowdev/status/1882013279998148726&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;&lt;a href="https://dev.to/livecycle/under-the-hood-at-devhunt-94j"&gt;👀 Under the Hood at DevHunt 🚀 - DEV Community&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;&lt;a href="https://dev.to/masutaka/comment/2m6om"&gt;https://dev.to/masutaka/comment/2m6om&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>github</category>
      <category>opensource</category>
      <category>marketing</category>
      <category>liamerd</category>
    </item>
    <item>
      <title>Created an Emacs Lisp function to insert the title of a GitHub Issue/PR/Discussion URL</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Fri, 18 Apr 2025 09:43:02 +0000</pubDate>
      <link>https://dev.to/masutaka/created-an-emacs-lisp-function-to-insert-the-title-of-a-github-issueprdiscussion-url-345d</link>
      <guid>https://dev.to/masutaka/created-an-emacs-lisp-function-to-insert-the-title-of-a-github-issueprdiscussion-url-345d</guid>
      <description>&lt;p&gt;On GitHub, when you paste Issue/PR/Discussion URLs into comments or descriptions, GitHub automatically renders the status, title, and number.&lt;/p&gt;

&lt;p&gt;For example, if you write the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;*&lt;/span&gt; https://github.com/masutaka/sandbox/issues/93
&lt;span class="p"&gt;*&lt;/span&gt; https://github.com/masutaka/sandbox/issues/70
&lt;span class="p"&gt;*&lt;/span&gt; https://github.com/masutaka/sandbox/pull/90
&lt;span class="p"&gt;*&lt;/span&gt; https://github.com/masutaka/sandbox/discussions/91
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It renders like this:&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.amazonaws.com%2Fuploads%2Farticles%2Fvd6x8iiklvsriq82kymx.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.amazonaws.com%2Fuploads%2Farticles%2Fvd6x8iiklvsriq82kymx.png" alt="Rendering example" width="449" height="139"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, when writing in a plain text area or text editor, the URLs aren't automatically expanded, so you can't see the titles or statuses at a glance. This isn't a problem with just a few URLs, but it can become confusing when dealing with many.&lt;/p&gt;

&lt;p&gt;Previously, I added comments manually like this, but it became tedious, so I created an Emacs Lisp function to automate the process.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;*&lt;/span&gt; https://github.com/masutaka/sandbox/issues/93 &lt;span class="c"&gt;&amp;lt;!-- Create test.rb --&amp;gt;&lt;/span&gt; (In Progress)
&lt;span class="p"&gt;*&lt;/span&gt; https://github.com/masutaka/sandbox/issues/70 &lt;span class="c"&gt;&amp;lt;!-- 2024/09 Sample Issue --&amp;gt;&lt;/span&gt; (Done)
&lt;span class="p"&gt;*&lt;/span&gt; https://github.com/masutaka/sandbox/pull/90 &lt;span class="c"&gt;&amp;lt;!-- Give CodeQL no running part2 --&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;*&lt;/span&gt; https://github.com/masutaka/sandbox/discussions/91 &lt;span class="c"&gt;&amp;lt;!-- 2025-02-05 ミーティングレポート --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Function I Created
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;a href="https://github.com/masutaka/dotfiles-public/blob/2ba4f3f9a53405e6de01aaede3ad9676ed346930/.emacs.d/init.el#L109-L152" rel="noopener noreferrer"&gt;~/.emacs.d/init.el#L109-L152&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight common_lisp"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="ss"&gt;'cl-lib&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="ss"&gt;'request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;github-expand-link&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="s"&gt;"Use the GitHub API to get the information and
insert a comment at the end of the current line in the following format:

- Issue URL &amp;lt;!-- Title --&amp;gt; (Done or In Progress)
- PR/Discussion URL &amp;lt;!-- Title --&amp;gt;"&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;interactive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;thing-at-point&lt;/span&gt; &lt;span class="ss"&gt;'url&lt;/span&gt; &lt;span class="ss"&gt;'no-properties&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="nb"&gt;not&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;message&lt;/span&gt; &lt;span class="s"&gt;"[github-expand-link] No URL at point"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let*&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;parsed-url&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url-generic-parse-url&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
             &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url-host&lt;/span&gt; &lt;span class="nv"&gt;parsed-url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
             &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;parts&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;split-string&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url-filename&lt;/span&gt; &lt;span class="nv"&gt;parsed-url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="s"&gt;"/"&lt;/span&gt; &lt;span class="no"&gt;t&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="nb"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string-match-p&lt;/span&gt; &lt;span class="s"&gt;"github\\.com$"&lt;/span&gt; &lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;length&lt;/span&gt; &lt;span class="nv"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;access-token&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;my-lisp-load&lt;/span&gt; &lt;span class="s"&gt;"github-expand-link-token"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;org&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;nth&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="nv"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;repo&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;nth&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="nv"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;nth&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="nv"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;number&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;nth&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="nv"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;type-alist&lt;/span&gt; &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s"&gt;"issues"&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="s"&gt;"issue"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pull"&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="s"&gt;"pullRequest"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"discussions"&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="s"&gt;"discussion"&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
              &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;request&lt;/span&gt;
                &lt;span class="s"&gt;"https://api.github.com/graphql"&lt;/span&gt;
                &lt;span class="ss"&gt;:type&lt;/span&gt; &lt;span class="s"&gt;"POST"&lt;/span&gt;
                &lt;span class="ss"&gt;:headers&lt;/span&gt; &lt;span class="o"&gt;`&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;concat&lt;/span&gt; &lt;span class="s"&gt;"Bearer "&lt;/span&gt; &lt;span class="nv"&gt;access-token&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
                &lt;span class="ss"&gt;:data&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;json-encode&lt;/span&gt; &lt;span class="o"&gt;`&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s"&gt;"query"&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt; &lt;span class="s"&gt;"query { repository(owner: \"%s\", name: \"%s\") { %s(number: %d) { %s } } }"&lt;/span&gt;
                                                         &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url-hexify-string&lt;/span&gt; &lt;span class="nv"&gt;org&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url-hexify-string&lt;/span&gt; &lt;span class="nv"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                                         &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cdr&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;assoc&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nv"&gt;type-alist&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string-to-number&lt;/span&gt; &lt;span class="nc"&gt;number&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="nb"&gt;equal&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="s"&gt;"issues"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="s"&gt;"title state"&lt;/span&gt; &lt;span class="s"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;)))))&lt;/span&gt;
                &lt;span class="ss"&gt;:parser&lt;/span&gt; &lt;span class="ss"&gt;'json-read&lt;/span&gt;
                &lt;span class="ss"&gt;:sync&lt;/span&gt; &lt;span class="no"&gt;t&lt;/span&gt;
                &lt;span class="ss"&gt;:success&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;cl-function&lt;/span&gt;
                          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;&amp;amp;key&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt; &lt;span class="nv"&gt;response&lt;/span&gt; &lt;span class="k"&gt;&amp;amp;allow-other-keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let*&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;body&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;car&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;alist-get&lt;/span&gt; &lt;span class="ss"&gt;'repository&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;alist-get&lt;/span&gt; &lt;span class="ss"&gt;'data&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
                                   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;title&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;alist-get&lt;/span&gt; &lt;span class="ss"&gt;'title&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                                   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;state&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;alist-get&lt;/span&gt; &lt;span class="ss"&gt;'state&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
                              &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;end-of-line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                              &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;insert&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;state&lt;/span&gt;
                                          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt; &lt;span class="s"&gt;" &amp;lt;!-- %s --&amp;gt; (%s)"&lt;/span&gt; &lt;span class="nv"&gt;title&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="nb"&gt;equal&lt;/span&gt; &lt;span class="s"&gt;"CLOSED"&lt;/span&gt; &lt;span class="nv"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="s"&gt;"Done"&lt;/span&gt; &lt;span class="s"&gt;"In Progress"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                                        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt; &lt;span class="s"&gt;" &amp;lt;!-- %s --&amp;gt;"&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;))))))&lt;/span&gt;
                &lt;span class="ss"&gt;:error&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;cl-function&lt;/span&gt;
                        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;&amp;amp;key&lt;/span&gt; &lt;span class="nv"&gt;error-thrown&lt;/span&gt; &lt;span class="nv"&gt;response&lt;/span&gt; &lt;span class="k"&gt;&amp;amp;allow-other-keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;message&lt;/span&gt; &lt;span class="s"&gt;"[github-expand-link] Fail %S to POST %s"&lt;/span&gt;
                                   &lt;span class="nv"&gt;error-thrown&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;request-response-url&lt;/span&gt; &lt;span class="nv"&gt;response&lt;/span&gt;&lt;span class="p"&gt;))))))&lt;/span&gt;
          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;message&lt;/span&gt; &lt;span class="s"&gt;"[github-expand-link] Not a valid GitHub Issue/PR/Discussion URL"&lt;/span&gt;&lt;span class="p"&gt;))))))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since there doesn't seem to be a GitHub REST API for retrieving Discussion information, I'm using the GraphQL API.&lt;/p&gt;

&lt;p&gt;I try not to use many third-party packages, but I do like using &lt;a href="https://github.com/tkf/emacs-request" rel="noopener noreferrer"&gt;request.el&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;my-lisp-load is a function I introduced in &lt;a href="https://masutaka.net/2016-05-06-2/" rel="noopener noreferrer"&gt;2016-05-06-2&lt;/a&gt;. If you create a PAT at &lt;a href="https://github.com/settings/tokens" rel="noopener noreferrer"&gt;https://github.com/settings/tokens&lt;/a&gt; and save it to ~/.emacs.d/spec/github-expand-link-token, you can load it with &lt;code&gt;(my-lisp-load "github-expand-link-token")&lt;/code&gt;. If you want to reference Issues/PRs/Discussions in private repositories, set the PAT scope to &lt;code&gt;repo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I've assigned the keybinding &lt;code&gt;⌘-i&lt;/code&gt; to this function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight common_lisp"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;define-key&lt;/span&gt; &lt;span class="nv"&gt;global-map&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;kbd&lt;/span&gt; &lt;span class="s"&gt;"s-i"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ss"&gt;'github-expand-link&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Being able to automatically insert titles, reminiscent of shell TAB completion, has proven more useful than I expected.&lt;/p&gt;

&lt;p&gt;While writing this article, I decided to add Issue status as well, which required substantial changes.&lt;/p&gt;

&lt;p&gt;I don't write Emacs Lisp frequently, so in the past, it was tedious to look things up via Google or check the official documentation. However, recently it's been convenient to learn about useful functions through ChatGPT and similar tools. This time, I learned about the alist-get function that way.&lt;/p&gt;

</description>
      <category>emacs</category>
      <category>emacslisp</category>
      <category>github</category>
    </item>
    <item>
      <title>Visualizing Mastodon’s Data Schema with Liam ERD</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Wed, 09 Apr 2025 07:27:15 +0000</pubDate>
      <link>https://dev.to/route06/visualizing-mastodons-data-schema-with-liam-erd-9k1</link>
      <guid>https://dev.to/route06/visualizing-mastodons-data-schema-with-liam-erd-9k1</guid>
      <description>&lt;p&gt;&lt;em&gt;This post was originally written by Takafumi Endo, CEO of ROUTE06, Inc., and published on &lt;a href="https://liambx.com/blog/liam-erd-reinventing-database-visualization" rel="noopener noreferrer"&gt;our company blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.joinmastodon.org/" rel="noopener noreferrer"&gt;Mastodon&lt;/a&gt; stands as a prime example of how a decentralized social network can thrive under open-source principles. Rather than replicating the centralized models seen on major platforms, it empowers individuals to create their own servers—known as “instances”—all while remaining interconnected. This design ensures that each community can maintain its autonomy while still sharing a broader network.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://joinmastodon.org/reports/Mastodon%20Annual%20Report%202023.pdf" rel="noopener noreferrer"&gt;The Mastodon Annual Report 2023&lt;/a&gt;, the latest annual report available at the time of writing, offers concrete data to support this growth. Over the past year, registered accounts rose by 38.5% to reach 8.8 million, and monthly active users totaled 1.56 million across 9,800 servers. Donations increased by 65%, thanks in part to community funding drives and a notable $100,000 contribution from Sujitech. On the product side, Mastodon introduced improvements in user onboarding, enhanced search functionality, and new mobile app features—leading to 1.87 million combined downloads on iOS and Android in 2023. &lt;/p&gt;

&lt;p&gt;A key attraction of Mastodon is its “federated” design, which embraces a wide variety of communities. Each instance sets its own rules and fosters a distinct culture, giving users the freedom to join whichever environment resonates most with them—rather than adapting to a one-size-fits-all platform. In an era dominated by large tech companies, Mastodon’s personalized approach feels refreshingly different. Server administrators actively shape their local environment, and users themselves decide how they want to engage with others. This dynamic reflects the best qualities of the open-source mindset, where technology evolves through collaboration and direct user influence.&lt;/p&gt;

&lt;p&gt;Looking ahead, Mastodon offers valuable insights into the future of social media. Its example shows how technological innovation and collective energy can come together to form a platform that is both cutting-edge and inclusive. Instead of entrusting everything to a single tech giant, people can build and sustain their own communities—and in doing so, shape the online spaces that matter to them most.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demystifying Mastodon’s Data with Liam ERD
&lt;/h2&gt;

&lt;p&gt;Our main goal with Liam ERD is to make complex &lt;a href="https://liambx.com/glossary/database" rel="noopener noreferrer"&gt;databases&lt;/a&gt; easier to understand. Relying solely on &lt;a href="https://liambx.com/glossary/sql" rel="noopener noreferrer"&gt;SQL&lt;/a&gt; files and text-based documentation can quickly become confusing, so Liam ERD automatically generates visual diagrams of tables and relationships. This means you don’t have to sift through code line by line to see how everything fits together.&lt;/p&gt;

&lt;p&gt;Using Liam ERD with Mastodon’s extensive database has been particularly rewarding. Mastodon has about a hundred tables that handle everything from user profiles to timelines and notifications. Rather than feeling overwhelmed, you can filter out irrelevant details and focus on what matters most—giving you a clearer picture of how Mastodon’s features interconnect.&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.amazonaws.com%2Fuploads%2Farticles%2F0jkqxb2yug0oghb25xdt.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.amazonaws.com%2Fuploads%2Farticles%2F0jkqxb2yug0oghb25xdt.jpg" alt="A part of Mastodon’s ERD"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We also made Liam ERD interactive. Instead of a static diagram, you can zoom, pan, and click on elements for more information. This hands-on approach turns the schema into a dynamic learning tool, letting you explore &lt;a href="https://liambx.com/blog/postgresql-materialized-views-from-basics-to-practical-examples-in-mastodon" rel="noopener noreferrer"&gt;Mastodon’s architecture&lt;/a&gt; at your own pace.&lt;/p&gt;

&lt;p&gt;While Mastodon’s schema is large, it is built on solid engineering practices: normalization, well-chosen data types, and foreign keys that maintain relational integrity. Seeing these best practices in action—especially at the scale of millions of users—can be far more illuminating than simply reading about them in a textbook.&lt;/p&gt;

&lt;p&gt;Because Mastodon is open source, you can watch its schema evolve as new features are introduced or performance is optimized. For developers aiming to specialize in database engineering, Mastodon offers lessons in iterative design and scalability. Our goal with Liam ERD is to support that exploration, helping you dive deep into Mastodon’s data design without losing your way.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://liambx.com/erd/p/github.com/mastodon/mastodon/blob/main/db/schema.rb" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fapp.liambx.com%2Fassets%2Fliam_erd.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://liambx.com/erd/p/github.com/mastodon/mastodon/blob/main/db/schema.rb" rel="noopener noreferrer" class="c-link"&gt;
            
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Generate ER diagrams effortlessly by entering a schema file URL. Ideal for visualizing, reviewing, and documenting schemas.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fliambx.com%2Ffavicon.ico"&gt;
          liambx.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How Liam ERD Empowers OSS
&lt;/h2&gt;

&lt;p&gt;Mastodon brings open-source values to life. Its code is fully public, contributions are welcomed worldwide, and the development process is transparent. This openness—together with its responsiveness to user feedback—reflects a strong culture of collaboration that aligns perfectly with Liam ERD’s mission to share knowledge and tools for everyone’s benefit.&lt;/p&gt;

&lt;p&gt;Before Liam released their ERD, we asked Mastodon about featuring it, and we were especially pleased when Mastodon agreed to feature their ERD on our site. This underscores Mastodon’s commitment to transparency and its desire to help others learn from its platform. Such a spirit of shared resources and ideas drives supportive, innovative communities.&lt;/p&gt;

&lt;p&gt;Mastodon’s user-centric decision-making also shows how &lt;a href="https://liambx.com/glossary/agility" rel="noopener noreferrer"&gt;agility&lt;/a&gt; and responsiveness can flourish when diverse voices have a say. The project tackles bugs, adds features, and evolves efficiently while remaining true to its vision—showcasing how shared ownership fosters sustained progress.&lt;/p&gt;

&lt;p&gt;I believe many open-source projects have well-designed databases that often go underappreciated due to their lack of visibility. While text-based documentation can be overwhelming for potential contributors, visualizing the structure makes it much easier for maintainers, developers, and newcomers to understand how everything fits together.&lt;/p&gt;

&lt;p&gt;We see Liam ERD as more than a diagramming tool—it’s a way for anyone, including non-developers, to understand a project’s data architecture and see where they can contribute. That approach matches the open-source principle that good ideas can come from anyone.&lt;/p&gt;

&lt;p&gt;Making database structures easy to visualize also boosts inclusivity. When relationships are clearly outlined, anyone can jump in and say, “I know how to help.” Mastodon’s steady growth—fueled by contributors of all skill levels—demonstrates the impact of this openness.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/liam-hq" rel="noopener noreferrer"&gt;
        liam-hq
      &lt;/a&gt; / &lt;a href="https://github.com/liam-hq/liam" rel="noopener noreferrer"&gt;
        liam
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Automatically generates beautiful and easy-to-read ER diagrams from your database.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Open Source and Transparent Schemas
&lt;/h2&gt;

&lt;p&gt;Mastodon’s robust database and Liam ERD’s visual approach complement each other naturally, both driven by the belief that sharing and accessibility accelerate progress. By showcasing Mastodon’s underlying structure, we aim to lower barriers and inspire people to learn, collaborate, and innovate.&lt;/p&gt;

&lt;p&gt;Clarity and transparency in database design amplify a software project’s potential. Developers work more confidently when they can see a clear map, and open-source communities thrive when people learn from one another’s work. This blend of technical excellence and human-centered collaboration continues to propel software forward for anyone ready to join in.&lt;/p&gt;

</description>
      <category>mastodon</category>
      <category>liamerd</category>
      <category>erd</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Tuning Edge Animations in Reactflow for Optimal Performance</title>
      <dc:creator>Takashi Masuda</dc:creator>
      <pubDate>Tue, 08 Apr 2025 04:41:16 +0000</pubDate>
      <link>https://dev.to/route06/tuning-edge-animations-in-reactflow-for-optimal-performance-3g32</link>
      <guid>https://dev.to/route06/tuning-edge-animations-in-reactflow-for-optimal-performance-3g32</guid>
      <description>&lt;p&gt;&lt;em&gt;This post was originally written by Junki Saito, and published on &lt;a href="https://liambx.com/blog/tuning-edge-animations-reactflow-optimal-performance" rel="noopener noreferrer"&gt;our company blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Liam ERD relies on the incredible capabilities of React Flow to visualize complex data models—like tables and relationships—on the web. We’re genuinely grateful for how React Flow enables rapid development and a beautiful user experience right out of the box. That said, when you’re working with truly massive datasets, you need to make sure your app still runs at top speed. In this post, I’ll walk you through how we tackled performance issues related to edge animations and the steps we took to keep everything running smoothly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge: Animated Edges and Performance Bottlenecks
&lt;/h2&gt;

&lt;p&gt;One of the standout features in React Flow is the effortless animation you can apply to edges. By simply toggling &lt;code&gt;animated&lt;/code&gt; to true, you get a dashed-line effect that makes relationships feel alive. We loved this feature—it instantly added a sense of dynamic flow.&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.amazonaws.com%2Fuploads%2Farticles%2Fu6pqy9u93s3013pwpk39.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fu6pqy9u93s3013pwpk39.gif" alt="React Flow Demo for animated edges in React Flow with animated=true."&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * This code is written in Stackblitz
 * @see https://stackblitz.com/edit/sb1-74jfdcqt?file=src%2FApp.tsx
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;initialNodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&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&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Input Node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Default Node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;125&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;output&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Output Node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;initialEdges&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;e1-2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;source&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&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;animated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;e2-3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;animated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;App&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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ReactFlow&lt;/span&gt;
      &lt;span class="na"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;initialNodes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;initialEdges&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;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;The issue arose when we started rendering a huge number of edges for large ERDs, like in &lt;a href="https://docs.joinmastodon.org/" rel="noopener noreferrer"&gt;Mastodon&lt;/a&gt;, which can involve over 100 tables and countless relationships. We noticed a significant dip in responsiveness whenever we hovered over or dragged around nodes—some browsers handled it better than others, but overall, it was clear that the animations were holding back performance.&lt;/p&gt;
&lt;h2&gt;
  
  
  Identifying the Root Cause: The Culprit - stroke-dasharray
&lt;/h2&gt;

&lt;p&gt;After digging in, we discovered that React Flow’s edge animations rely on the CSS property &lt;code&gt;stroke-dasharray&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.amazonaws.com%2Fuploads%2Farticles%2Fbn7rpeoh0d8dgkaiw6ok.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.amazonaws.com%2Fuploads%2Farticles%2Fbn7rpeoh0d8dgkaiw6ok.png" alt="CSS style for animated edges in React Flow with animated=true."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While it does create a neat dashed effect, it can seriously push your CPU when you have lots of SVG elements doing the same thing at once. Multiple user reports and bug threads confirmed that &lt;code&gt;stroke-dasharray&lt;/code&gt; can be a major slowdown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://issues.chromium.org/issues/40958492" rel="noopener noreferrer"&gt;High CPU utilisation when CSS animating stroke-dashoffset - Chromium&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/svgdotjs/svg.js/issues/1003" rel="noopener noreferrer"&gt;Locked fps of animation · Issue #1003 · svgdotjs/svg.js - Github&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In our case, with hundreds of edges animated simultaneously, &lt;code&gt;stroke-dasharray&lt;/code&gt; quickly became the main bottleneck.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Goal: Retain Animation, Regain Smoothness
&lt;/h2&gt;

&lt;p&gt;We still wanted the user experience of visually “flowing” edges. It’s a small detail that helps people see how tables and relationships link together in real time. But we absolutely had to solve the lag problem.&lt;/p&gt;
&lt;h2&gt;
  
  
  Our Solution: A Custom Animated Edge
&lt;/h2&gt;

&lt;p&gt;To tackle this, we sidestepped the default animation approach in React Flow and crafted our own:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bypassing React Flow's animated props&lt;/strong&gt;: We disabled the library’s &lt;code&gt;animated&lt;/code&gt; prop on edges to stop using &lt;code&gt;stroke-dasharray&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eliminating &lt;code&gt;stroke-dasharray&lt;/code&gt; entirely&lt;/strong&gt;: We cut it out for both animations and any default styling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implementing a Custom approach&lt;/strong&gt;: We built a &lt;code&gt;CustomEdge&lt;/code&gt; component that animates an SVG object along the edge path, as outlined in the &lt;a href="https://reactflow.dev/components/edges/animated-svg-edge" rel="noopener noreferrer"&gt;React Flow docs&lt;/a&gt;. This gave us the same visual flair—minus the performance hit.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PARTICLE_COUNT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ANIMATE_DURATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;EdgeProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RelationshipEdgeType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RelationshipEdge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;data&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;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;edgePath&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getBezierPath&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BaseEdge&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;edgePath&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* data.isHighlighted will be true if the edge should be highlighted. */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;isHighlighted&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PARTICLE_COUNT&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;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ellipse&lt;/span&gt;
            &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`particle-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&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;ANIMATE_DURATION&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"5"&lt;/span&gt;
            &lt;span class="na"&gt;ry&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"1.2"&lt;/span&gt;
            &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"url(#myGradient)"&lt;/span&gt;
          &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* The &amp;lt;animateMotion&amp;gt; element defines how an element moves along a motion path.  */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;animateMotion&lt;/span&gt;
              &lt;span class="na"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ANIMATE_DURATION&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;PARTICLE_COUNT&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;s`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
              &lt;span class="na"&gt;dur&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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;ANIMATE_DURATION&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
              &lt;span class="na"&gt;repeatCount&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"indefinite"&lt;/span&gt;
              &lt;span class="na"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"auto"&lt;/span&gt;
              &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;edgePath&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
              &lt;span class="na"&gt;calcMode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"spline"&lt;/span&gt;
              &lt;span class="na"&gt;keySplines&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"0.42, 0, 0.58, 1.0"&lt;/span&gt;
            &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ellipse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;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;By implementing these three approaches, the appearance changed from looking like Before gif image to looking like After gif image.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can see exactly how we did it in our Pull Request:&lt;/p&gt;


&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/liam-hq/liam/pull/367" rel="noopener noreferrer"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        🚸 Add animated particles to highlighted relationship edges
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#367&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/junkisai" rel="noopener noreferrer"&gt;
        &lt;img class="github-liquid-tag-img" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Favatars.githubusercontent.com%2Fu%2F28256336%3Fv%3D4" alt="junkisai avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/junkisai" rel="noopener noreferrer"&gt;junkisai&lt;/a&gt;
        &lt;/strong&gt; posted on &lt;a href="https://github.com/liam-hq/liam/pull/367" rel="noopener noreferrer"&gt;&lt;time&gt;Dec 24, 2024&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      &lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Summary&lt;/h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;
&lt;/div&gt;

&lt;p&gt;Drawing multiple strokes with stroke-dasharray specified causes performance degradation, especially in Safari browser.&lt;/p&gt;
&lt;p&gt;Therefore, I stopped using stroke-dasharray.
Additionally, since stroke-dasharray is also used when 'animated' is set to true in Edge, I made sure that 'animated' is always set to false.&lt;/p&gt;
&lt;p&gt;Furthermore, since setting 'animated' to false made it difficult to visually distinguish the flow of Edges, I used animateMotion instead to express Edge animations.&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/liam-hq/liam/pull/367" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;h2&gt;
  
  
  Results: Measuring the Improvement
&lt;/h2&gt;

&lt;p&gt;We ran before-and-after tests using Chrome DevTools to measure frame drops. Our test steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hover over the “Accounts” table node in Mastodon’s ERD: &lt;a href="https://liambx.com/erd/p/github.com/mastodon/mastodon/blob/e2f085e2b2cec08dc1f5ae825730c2a3bf62e054/db/schema.rb" rel="noopener noreferrer"&gt;Mastodon’s schema.rb&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Wait 3 seconds, then remove the hover.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://liambx.com/images/blogs/tuning-edge-animations-reactflow-optimal-performance/demo.gif" rel="noopener noreferrer"&gt;Demonstration of actions for comparison.&lt;/a&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt;: We saw consistent frame drops of around 5 frames at a time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&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.amazonaws.com%2Fuploads%2Farticles%2Fvur9tu9l86tpocajpath.png" width="100%"&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;After&lt;/strong&gt;: Frame drops went down to 2–3 frames at a time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&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.amazonaws.com%2Fuploads%2Farticles%2F9z24dack12hgr8j2ijlu.png" width="100%"&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Not surprisingly, our changes made the overall experience smoother and more responsive—no more choppy dragging or sluggish hover states.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimizing React Flow for Scalable Products
&lt;/h2&gt;

&lt;p&gt;By digging into React Flow’s edge animation logic and crafting a custom approach, we found a sweet spot between visual polish and raw performance. Disabling &lt;code&gt;stroke-dasharray&lt;/code&gt; and opting for a more controlled animation model let us keep the dynamic look of animated edges while ensuring that our largest ERDs run like butter. We hope this helps anyone else wrestling with performance challenges on React Flow—you can absolutely keep those animations, as long as you give them some thoughtful tweaks.&lt;/p&gt;

&lt;p&gt;Liam ERD required a highly scalable node-based engine to efficiently render large, interconnected schemas. That drove our choice of React Flow, which effortlessly manages 100+ tables and hundreds of edges while allowing the fine-grained control we need for selective highlighting and custom animations. Since Liam ERD relies on type-safe schema transformations, React Flow’s robust TypeScript support proved indispensable. Its performance tuning capabilities, including the custom animation optimizations we described, make it ideally suited for complex database diagrams. If your product demands large-scale interactive data modeling or specialized node behavior, React Flow is an excellent foundation to build upon.&lt;/p&gt;

&lt;p&gt;For more insights into Liam ERD and what we’ve been working on, feel free to check out our introduction post:  &lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://liambx.com/blog/liam-erd-introduction" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fliambx.com%2Fimages%2Fblogs%2Fliam-erd-introduction%2Fcover.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://liambx.com/blog/liam-erd-introduction" rel="noopener noreferrer" class="c-link"&gt;
            Introducing Liam ERD - Liam ERD
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Discover Liam ERD, a modern tool for effortlessly auto-generating interactive, readable ER diagrams from database schemas. Perfect for team collaborat...
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fliambx.com%2Ffavicon.ico"&gt;
          liambx.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>reactflow</category>
      <category>liamerd</category>
      <category>erd</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
