<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ja"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://blog.zunda.ninja/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.zunda.ninja/" rel="alternate" type="text/html" hreflang="ja" /><updated>2026-07-22T00:46:04+00:00</updated><id>https://blog.zunda.ninja/feed.xml</id><title type="html">zundaの個人サイト</title><subtitle>ヤングもすなる個人サイトといふものを、おっさんもしてみむとするなり。</subtitle><author><name>zunda</name></author><entry><title type="html">静的サイトをCloudflare Workersで公開する</title><link href="https://blog.zunda.ninja/vitepress/cloudflare/cloudflareworkers/2026/07/21/cloudflare-workers.html" rel="alternate" type="text/html" title="静的サイトをCloudflare Workersで公開する" /><published>2026-07-21T01:00:00+00:00</published><updated>2026-07-22T00:45:00+00:00</updated><id>https://blog.zunda.ninja/vitepress/cloudflare/cloudflareworkers/2026/07/21/cloudflare-workers</id><content type="html" xml:base="https://blog.zunda.ninja/vitepress/cloudflare/cloudflareworkers/2026/07/21/cloudflare-workers.html"><![CDATA[<p>いろいろあって、VitePressに生成してもらっている静的サイトmitome.inのトップレベルドメインを変更して<a href="https://mitome.ink">mitome.ink</a>に引っ越すことにしました。ついでに、ビルドと公開にCloudflare Workersを試してみることにしました。</p>

<h2 id="作業前の状態">作業前の状態</h2>
<p>Cloudflareのアカウントに支払い手段を設定して引っ越し先のドメインmitome.inkを購入しました。</p>

<p>静的サイトのコードはGitHubに<a href="https://github.com/zunda/mitome.in">zunda/mitome.in</a>として管理していただいていて、Netlifyから<a href="https://mitome.in">mitome.in</a>に公開しています。</p>

<h2 id="cloudflare-workers">Cloudflare Workers</h2>
<p>Cloudflareによるドキュメントを眺めてみると、Workersでは<a href="https://developers.cloudflare.com/workers/ci-cd/">CI/CD機能</a>としてWorkers Buildsやその他のプロバイダによるビルドシステムを利用できるようです。ここでは、<a href="https://developers.cloudflare.com/workers/ci-cd/builds/">Workers Builds</a>のGitHub Integrationを利用してみます。</p>

<p>Workers Buildsの<a href="https://developers.cloudflare.com/workers/ci-cd/builds/#get-started">Get started文書</a>に従って作業を進めてみます。</p>

<h3 id="workerの作成">Workerの作成</h3>
<p>CloudflareとGitHubにログインした状態のブラウザを利用します。</p>

<ol>
  <li>Cloudflareのダッシュボードにログインします</li>
  <li>GitHubにログインしておきます。</li>
  <li><a href="https://dash.cloudflare.com/?to=/:account/workers-and-pages">Workers &amp; Pages</a>を閲覧します</li>
  <li>右上のCreate applicationボタンを押します</li>
  <li>Ship something newという表題の下記の選択肢からConnect GitHubを選択します
    <ul>
      <li>Connect GitHub</li>
      <li>Connect GitLab</li>
      <li>Start with Hello World!</li>
      <li>Select a template</li>
      <li>Upload your static files</li>
    </ul>
  </li>
  <li>GitHubのウインドウが開くので、Cloudflare Workers and Pagesをインストールする対象のGitHub アカウント/組織とレポジトリを選択します。今回は、自アカウントの、Only selected repositoriesとして、zunda/mitome.inを選択しました。これによって、下記のパーミションが有効になるとのことでした。書き込み権限が広いようにも思うけれどもしかたないのかな。
    <ul>
      <li>Read access to metadata</li>
      <li>Read and write access to administration, checks, code, deployments, and pull requests</li>
    </ul>
  </li>
  <li>元のウインドウで、Continuw with GitHub→を選択します</li>
  <li>GitHubレポジトリを選択します</li>
  <li>Set up your applicationという表題のステップで、下記を設定します
    <ul>
      <li>Project name: mitomein (小文字のアルファベットと数字とダッシュのみが利用できるとのことです)</li>
      <li>Build command: <code class="language-plaintext highlighter-rouge">yarn docs:build</code> (デフォルトは空白)</li>
      <li>Deploy command: デフォルトの<code class="language-plaintext highlighter-rouge">npx wrangler deploy</code>のまま</li>
      <li>Builds for non-production branchesを有効に (デフォルトは有効)<sup id="fnref:branches"><a href="#fn:branches" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></li>
    </ul>
  </li>
  <li>Deployボタンを押します。生成されたサイトのパス (<code class="language-plaintext highlighter-rouge">./docs/.vuepress/dist/</code>) を設定する項目はありませんでしたがとりあえず進めます</li>
</ol>

<p>ここまでの作業で、ブラウザにビルドの進行状況が表示されました。今回のビルドは下記のようなログを残して失敗したようです。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2026-07-21T01:16:38.145Z	Initializing build environment...
2026-07-21T01:16:38.145Z	Initializing build environment...
2026-07-21T01:16:39.870Z	Success: Finished initializing build environment
2026-07-21T01:16:41.275Z	Cloning repository...
2026-07-21T01:16:42.703Z	Restoring from dependencies cache
2026-07-21T01:16:42.705Z	Restoring from build output cache
2026-07-21T01:16:42.708Z	Detected the following tools from environment: yarn@4.9.1, nodejs@22.16.0
2026-07-21T01:16:42.845Z	Installing project dependencies: yarn
2026-07-21T01:16:44.128Z	➤ YN0087: Migrated your project to the latest Yarn version 🚀
2026-07-21T01:16:44.129Z
2026-07-21T01:16:44.130Z	➤ YN0000: · Yarn 4.5.0
2026-07-21T01:16:44.149Z	➤ YN0000: ┌ Resolution step
2026-07-21T01:16:46.945Z	➤ YN0085: │ + @fortawesome/fontawesome-svg-core@npm:6.7.2, @fortawesome/free-solid-svg-icons@npm:6.7.2, @fortawesome/vue-fontawesome@npm:3.0.8, and 197 more.
2026-07-21T01:16:46.952Z	➤ YN0000: └ Completed in 2s 804ms
2026-07-21T01:16:46.953Z	➤ YN0000: ┌ Post-resolution validation
2026-07-21T01:16:46.953Z	➤ YN0002: │ mitome.in@workspace:. doesn't provide vue (pdbc23), requested by @fortawesome/vue-fontawesome and other dependencies.
2026-07-21T01:16:46.954Z	➤ YN0086: │ Some peer dependencies are incorrectly met by your project; run yarn explain peer-requirements &lt;hash&gt; for details, where &lt;hash&gt; is the six-letter p-prefixed code.
2026-07-21T01:16:46.954Z	➤ YN0086: │ Some peer dependencies are incorrectly met by dependencies; run yarn explain peer-requirements for details.
2026-07-21T01:16:46.976Z	➤ YN0028: │ The lockfile would have been modified by this install, which is explicitly forbidden.
2026-07-21T01:16:46.977Z	➤ YN0000: └ Completed
2026-07-21T01:16:46.982Z	➤ YN0000: · Failed with errors in 2s 845ms
2026-07-21T01:16:47.026Z	Failed: error occurred while installing tools or dependencies
</code></pre></div></div>

<h2 id="ビルドの問題への対応">ビルドの問題への対応</h2>
<p>ローカルで問題を再現して対応していきます。</p>

<h3 id="問題の再現">問題の再現</h3>
<p>これまでは古いYarnコマンドで<code class="language-plaintext highlighter-rouge">yarn install</code>に成功していました。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ yarn install --pure-lockfile
yarn install v1.22.22
[1/5] Validating package.json...
[2/5] Resolving packages...
[3/5] Fetching packages...
[4/5] Linking dependencies...
warning " &gt; @fortawesome/vue-fontawesome@3.0.8" has unmet peer dependency "vue@&gt;= 3.0.0 &lt; 4".
warning "vitepress &gt; @docsearch/js &gt; @docsearch/react &gt; @algolia/autocomplete-preset-algolia@1.17.7" has unmet peer dependency "@algolia/client-search@&gt;= 4.9.1 &lt; 6".
warning "vitepress &gt; @docsearch/js &gt; @docsearch/react &gt; @algolia/autocomplete-core &gt; @algolia/autocomplete-plugin-algolia-insights@1.17.7" has unmet peer dependency "search-insights@&gt;= 1 &lt; 3".
warning "vitepress &gt; @docsearch/js &gt; @docsearch/react &gt; @algolia/autocomplete-core &gt; @algolia/autocomplete-shared@1.17.7" has unmet peer dependency "@algolia/client-search@&gt;= 4.9.1 &lt; 6".
warning " &gt; vue-toast-notification@3.1.3" has unmet peer dependency "vue@^3.0".
[5/5] Building fresh packages...
Done in 1.44s.
</code></pre></div></div>

<p>YarnコマンドをCloudflare Workersと同じバージョンにすることで、無事に<code class="language-plaintext highlighter-rouge">yarn install --immutable</code>を失敗させることができました。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ yarn set version 4.5.0
$ yarn install --immutable
➤ YN0000: · Yarn 4.5.0
➤ YN0000: ┌ Resolution step
➤ YN0085: │ + @fortawesome/fontawesome-svg-core@npm:6.7.2, and 199 more.
➤ YN0000: └ Completed in 1s 289ms
➤ YN0000: ┌ Post-resolution validation
➤ YN0002: │ mitome.in@workspace:. doesn't provide vue (pdbc23), requested by @fortawesome/vue-fontawesome and other dependencies.
➤ YN0086: │ Some peer dependencies are incorrectly met by your project; run yarn explain peer-requirements &lt;hash&gt; for details, where &lt;hash&gt; is the six-letter p-prefixed code.
➤ YN0086: │ Some peer dependencies are incorrectly met by dependencies; run yarn explain peer-requirements for details.
➤ YN0028: │ The lockfile would have been modified by this install, which is explicitly forbidden.
➤ YN0000: └ Completed
➤ YN0000: · Failed with errors in 1s 329ms
</code></pre></div></div>

<h3 id="足りないモジュールの追加">足りないモジュールの追加</h3>
<p>ブランチで作業します。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git switch -c build-on-cloudflare
$ yarn add -D vue
➤ YN0000: · Yarn 4.5.0
➤ YN0000: ┌ Resolution step
➤ YN0085: │ + vue@npm:3.5.40, and 19 more.
➤ YN0000: └ Completed in 0s 679ms
➤ YN0000: ┌ Post-resolution validation
➤ YN0086: │ Some peer dependencies are incorrectly met by dependencies; run yarn explain peer-requirements for details.
➤ YN0000: └ Completed
➤ YN0000: ┌ Fetch step
➤ YN0013: │ 20 packages were added to the project (+ 16.01 MiB).
➤ YN0000: └ Completed in 1s 141ms
➤ YN0000: ┌ Link step
➤ YN0000: └ Completed in 0s 356ms
➤ YN0000: · Done with warnings in 2s 221ms
</code></pre></div></div>

<p>ここまでの作業で下記のような差分が得られました。<code class="language-plaintext highlighter-rouge">vue</code>モジュールの追加のみをライセンスの追加と一緒にcommitしてGitHubにpushします。</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh">diff --git a/package.json b/package.json
index 65f32b5..5451c22 100644
</span><span class="gd">--- a/package.json
</span><span class="gi">+++ b/package.json
</span><span class="p">@@ -18,11 +18,13 @@</span>
     "moment": "^2.30.1",
     "openpgp": "^6.1.1",
     "vitepress": "^1.5.0",
<span class="gi">+    "vue": "^3.5.40",
</span>     "vue-toast-notification": "^3"
   },
   "scripts": {
     "docs:dev": "vitepress dev docs",
     "docs:build": "vitepress build docs",
     "docs:preview": "vitepress preview docs"
<span class="gd">-  }
</span><span class="gi">+  },
+  "packageManager": "yarn@4.5.0"
</span> }
</code></pre></div></div>

<p>この状態で、Yarn 1.22.15で<code class="language-plaintext highlighter-rouge">yarn install --pure-lockfile</code>も完走することを確認できました。</p>

<h3 id="cloudflare-workersでのビルド">Cloudflare Workersでのビルド</h3>
<p>Cloudflareのダッシュボードに戻り、左のペインから、Build - Workers &amp; Pagesを選択し、先ほど作成したWorkerを選択します。下方のVersionペインの、先ほどGitHubにpushしたハッシュをクリックすると、ビルド済みのページを閲覧することができました。右の方のブランチ名をクリックすると、ビルドログを閲覧できるようです。ホスト名は、<code class="language-plaintext highlighter-rouge">&lt;Cloudflareが付与したversion ID&gt;-&lt;Workerのproject name&gt;.&lt;Cloudflareのaccount name&gt;-cloudflare.workers.dev</code>になるようです。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2026-07-21T02:32:15.629Z	Initializing build environment...
2026-07-21T02:32:17.922Z	Success: Finished initializing build environment
2026-07-21T02:32:18.646Z	Cloning repository...
2026-07-21T02:32:20.773Z	Restoring from dependencies cache
2026-07-21T02:32:20.774Z	Restoring from build output cache
2026-07-21T02:32:20.776Z	Detected the following tools from environment: yarn@4.9.1, nodejs@22.16.0
2026-07-21T02:32:20.914Z	Installing project dependencies: yarn
  :
2026-07-21T02:32:27.984Z	Executing user build command: yarn docs:build
  :
2026-07-21T02:32:39.458Z	Executing user deploy command: npx wrangler deploy
  :
2026-07-21T02:32:48.543Z	Detected Project Settings:
2026-07-21T02:32:48.543Z	 - Worker Name: mitomein
2026-07-21T02:32:48.543Z	 - Framework: Static
2026-07-21T02:32:48.543Z	 - Build Command: yarn run docs:build
2026-07-21T02:32:48.544Z	 - Output Directory: docs/.vitepress/dist
  :
2026-07-21T02:32:48.549Z	📄 Create wrangler.jsonc:
2026-07-21T02:32:48.549Z	  {
2026-07-21T02:32:48.549Z	    "$schema": "node_modules/wrangler/config-schema.json",
2026-07-21T02:32:48.549Z	    "name": "mitomein",
2026-07-21T02:32:48.549Z	    "compatibility_date": "2026-07-21",
2026-07-21T02:32:48.549Z	    "observability": {
2026-07-21T02:32:48.549Z	      "enabled": true
2026-07-21T02:32:48.549Z	    },
2026-07-21T02:32:48.549Z	    "assets": {
2026-07-21T02:32:48.554Z	      "directory": "docs/.vitepress/dist"
2026-07-21T02:32:48.554Z	    },
2026-07-21T02:32:48.554Z	    "compatibility_flags": [
2026-07-21T02:32:48.554Z	      "nodejs_compat"
2026-07-21T02:32:48.554Z	    ]
2026-07-21T02:32:48.555Z	  }
  :
2026-07-21T02:33:01.495Z	[build] Running: yarn run docs:build
  :
2026-07-21T02:33:13.732Z	🌀 Building list of assets...
2026-07-21T02:33:13.735Z	✨ Read 194 files from the assets directory /opt/buildhome/repo/docs/.vitepress/dist
2026-07-21T02:33:13.817Z	🌀 Starting asset upload...
2026-07-21T02:33:15.577Z	🌀 Found 182 new or modified static assets to upload. Proceeding with upload...
  :
2026-07-21T02:33:17.530Z	✨ Success! Uploaded 182 files (1 already uploaded) (1.95 sec)
  :
2026-07-21T02:33:20.063Z	Deployed mitomein triggers (1.06 sec)
2026-07-21T02:33:20.064Z	  https://mitomein.zundan-cloudflare.workers.dev
  :
2026-07-21T02:33:24.555Z	✨ Success! Build completed.
  :
</code></pre></div></div>

<p>Cloudflareによるドキュメント<a href="https://developers.cloudflare.com/workers/ci-cd/builds/build-branches/#configure-non-production-branch-builds">Build branches · Cloudflare Workers docs</a>によると、Builds for non-production branchesが有効になっている場合は、上記で観察したように、Production branch (今回は<code class="language-plaintext highlighter-rouge">master</code>)以外のブランチへのpushでも、Cloudflare Workersはビルドとデプロイを始めるようです。GitHubで作成されたプルリクエストに関する設定を見つけることはできませんでした。</p>

<h2 id="カスタムドメインの設定">カスタムドメインの設定</h2>
<p>Workerに<a href="https://developers.cloudflare.com/workers/configuration/routing/custom-domains/">custom domain</a>を追加することで、そのドメインでWorkerによるビルドの結果を閲覧できるようです。</p>

<p>Cloudflareのダッシュボードに戻り、左のペインから、Build - Workers &amp; Pagesを選択し、先ほど作成したWorkerを選択します。上の方に並んでいるメニューからDomains
を選択し、</p>

<ol>
  <li>Custom Domains and RoutesセクションのAdd Domainボタンを押します</li>
  <li>Connect domainダイアログで希望のドメインを選択します</li>
  <li>Subdomainを設定します。今回はAPEX (root)ドメインで公開するので空白のままにします</li>
  <li>Add domainボタンを押します</li>
</ol>

<p>この状態でCloudflareのDNSレコードを確認すると下記のようなレコードが生成されていました。また、<a href="https://mitome.ink">https://mitome.ink</a>でWorkerによるビルドの結果を閲覧できました。</p>

<table>
  <thead>
    <tr>
      <th>Content</th>
      <th>Type</th>
      <th>Target</th>
      <th>Proxy status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>mitome.ink</td>
      <td>Worker</td>
      <td>mitoemein</td>
      <td>Proxied</td>
    </tr>
  </tbody>
</table>

<h3 id="builds-for-non-production-branchesを無効にする">Builds for non-production branchesを無効にする</h3>
<p>Builds for non-production branchesが有効な場合は連携されたレポジトリへの全てのpushがカスタムドメインにもデプロイされるようです。</p>

<p>今回はプルリクエストをマージしてから変更点を公開したいので、WorkerのSettingsメニューからBuildセクションのBranch controlでBuilds for non-production branchesを無効にしました。</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:branches">
      <p>Builds for non-production branchesが有効な場合は連携されたレポジトリへの全てのpushがカスタムドメインにもデプロイされるようです <a href="#fnref:branches" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>zunda</name></author><category term="vitepress" /><category term="cloudflare" /><category term="cloudflareworkers" /><summary type="html"><![CDATA[いろいろあって、VitePressに生成してもらっている静的サイトmitome.inのトップレベルドメインを変更してmitome.inkに引っ越すことにしました。ついでに、ビルドと公開にCloudflare Workersを試してみることにしました。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.zunda.ninja/zunda-square.png" /><media:content medium="image" url="https://blog.zunda.ninja/zunda-square.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">kramdownを使っているJekyllの記事の節にリンクする</title><link href="https://blog.zunda.ninja/jekyll/kramdown/fragment/2026/05/04/kramdown-section-id.html" rel="alternate" type="text/html" title="kramdownを使っているJekyllの記事の節にリンクする" /><published>2026-05-04T02:50:00+00:00</published><updated>2026-05-04T02:50:00+00:00</updated><id>https://blog.zunda.ninja/jekyll/kramdown/fragment/2026/05/04/kramdown-section-id</id><content type="html" xml:base="https://blog.zunda.ninja/jekyll/kramdown/fragment/2026/05/04/kramdown-section-id.html"><![CDATA[<h2 id="アブストラクト">アブストラクト</h2>
<p>Jekyllではkramdownの機能によってセクションヘッダの<code class="language-plaintext highlighter-rouge">id</code>アトリビュートに表題と同じ文字列が付与され、URLのフラグメント部(<code class="language-plaintext highlighter-rouge">#</code>で始まる部分)で参照できることがわかりました。</p>

<h2 id="イントロダクション">イントロダクション</h2>
<p>長い友人と会って、</p>

<blockquote>
  <p>印刷が普及したことで、人類は既存の印刷物への参照を示しながら自分の思考を記録することができるようになった</p>
</blockquote>

<p>と知ることができました。</p>

<blockquote>
  <p>写本の時代には、それぞれの写本で内容の配置されているページが異なるため、参照先をページ数で示すことができなかった</p>
</blockquote>

<p>のだそうです。なるほどなあ！現代の印刷物であるウェブページももっと参照可能にする必要がある。</p>

<h2 id="現状">現状</h2>
<p>例えばこの記事のHTMLコードを確認すると、上記の章の冒頭部分は、下記のようになっています。</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;h2</span> <span class="na">id=</span><span class="s">"イントロダクション"</span><span class="nt">&gt;</span>イントロダクション<span class="nt">&lt;/h2&gt;</span>
<span class="nt">&lt;p&gt;</span>長い友人と会って、<span class="nt">&lt;/p&gt;</span>
</code></pre></div></div>

<p>この部分への同じページからのリンクは、下記のように記述できます。</p>

<div class="language-md highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nv">イントロダクションの章</span><span class="p">](</span><span class="sx">#イントロダクション</span><span class="p">)</span>へのリンク
</code></pre></div></div>

<p>この記述は、下記のようなHTMLに変換され、</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"#イントロダクション"</span><span class="nt">&gt;</span>イントロダクションの章<span class="nt">&lt;/a&gt;</span>へのリンク
</code></pre></div></div>

<p>「<a href="#イントロダクション">イントロダクションの章</a>へのリンク」のように表示されます。パーセントエンコーディングは施されないようです。</p>

<h3 id="kramdownによるidアトリビュート">kramdownによるIDアトリビュート</h3>
<p>この個人サイトは、kramdownをレンダラーとしたJekyllによってHTMLを生成しています。kramdownはデフォルトではセクションヘッダの文字列をそのまま<code class="language-plaintext highlighter-rouge">a</code>タグの<code class="language-plaintext highlighter-rouge">id</code>アトリビュートに設定するようです。<sup id="fnref:specifying-id"><a href="#fn:specifying-id" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<h2 id="絶対パスによる参照">絶対パスによる参照</h2>
<p>このように<code class="language-plaintext highlighter-rouge">a</code>タグの<code class="language-plaintext highlighter-rouge">id</code>アトリビュートが設定されているので、ページのURLに<code class="language-plaintext highlighter-rouge">#</code>と章や節の表題を追加すれば、この個人サイトの特定の章や節を参照できそうです。パーセントエンコーディングしたくなった場合には、<a href="https://clean-url.zunda.ninja">URLお掃除くん</a>なども利用できそうです。</p>

<h1 id="まとめ">まとめ</h1>
<p>この個人サイトの章や節への参照を利用できるようにしたいと思ったら、kramdownの標準的な機能で既に利用可能でした。フラグメントとして表題と同じ文字列を指定します。</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:specifying-id">
      <p>必要な場合には<a href="https://kramdown.gettalong.org/syntax.html#specifying-a-header-id">「<code class="language-plaintext highlighter-rouge"># ヘッダ # {#id}</code>」という記述で指定できる</a>ようです <a href="#fnref:specifying-id" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>zunda</name></author><category term="jekyll" /><category term="kramdown" /><category term="fragment" /><summary type="html"><![CDATA[アブストラクト Jekyllではkramdownの機能によってセクションヘッダのidアトリビュートに表題と同じ文字列が付与され、URLのフラグメント部(#で始まる部分)で参照できることがわかりました。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.zunda.ninja/zunda-square.png" /><media:content medium="image" url="https://blog.zunda.ninja/zunda-square.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">コード懇親会 at RubyKaigi 2026でdRubyに触る</title><link href="https://blog.zunda.ninja/rubykaigi/codeparty/2026/04/25/code-party.html" rel="alternate" type="text/html" title="コード懇親会 at RubyKaigi 2026でdRubyに触る" /><published>2026-04-25T13:30:00+00:00</published><updated>2026-04-27T05:15:00+00:00</updated><id>https://blog.zunda.ninja/rubykaigi/codeparty/2026/04/25/code-party</id><content type="html" xml:base="https://blog.zunda.ninja/rubykaigi/codeparty/2026/04/25/code-party.html"><![CDATA[<p><a href="https://rubykaigi.org/2026/">RubyKaigi 2026</a>に参加してきました。現地参加は<a href="https://rubykaigi.org/2015/">RubyKaigi 2015</a>以来です。RubyKaigi本体も楽しかったのですが、関連イベントとして開催された<a href="https://andpad.connpass.com/event/385946/">コード懇親会/Code Party(Social Coding) at RubyKaigi 2026</a>もかなり楽しく、終電の差し迫るなか、勢い余って<a href="https://github.com/ruby/uri/issues/224">ruby/uriへのIssue</a>を提出してしまったのでした。</p>

<h2 id="会場に到着したらrubyをインストールする">会場に到着したらRubyをインストールする</h2>
<p>RubyKaigiでの作業<sup id="fnref:helper"><a href="#fn:helper" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>が終わって会場の開始5分前くらいに会場の会議室に到着すると、多くの参加者がわいわい話をしながら軽食を食べていました。熱気。みなさんコード懇親会を楽しみにしている雰囲気です。やっぱりみんなコードを書くのだいすきよね。僕も軽食(とくにお寿司がおいしかった！)をいただいて、ラップトップを開いたところで、先日Rubyのリリースがあったのを思い出しました。</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>brew update ruby-build
<span class="nv">$ </span>rbenv <span class="nb">install </span>4.0.3
<span class="nv">$ </span><span class="nb">cd</span> ~c/src/local/rk26-code-party
<span class="nv">$ </span>rbenv <span class="nb">local </span>4.0.3
</code></pre></div></div>

<h2 id="drubyでオブジェクトをやりとりする">dRubyでオブジェクトをやりとりする</h2>
<p>僕が参加したのは、<a href="https://github.com/andpad-dev/code-party/blob/main/rubykaigi-2026/README.md#theme-1-druby-by-seki">作者のsekiさんと一緒にdRubyを触ってみる</a>グループで、用意していただいていた<a href="https://www.druby.org/druby-matzyama.pdf">資料</a>に沿ってdRubyを触ります。Ruby 3.0以降ではWEBrickは<a href="https://docs.ruby-lang.org/ja/4.0/library/webrick.html">標準ライブラリではない</a>ので、gemとしてインストールします。最終的に<code class="language-plaintext highlighter-rouge">Gemfile</code>は下記のようになりました。</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">source</span> <span class="s2">"https://rubygems.org"</span>
<span class="n">gem</span> <span class="s2">"drb"</span><span class="p">,</span> <span class="s2">"~&gt; 2.2"</span>
<span class="n">gem</span> <span class="s2">"webrick"</span><span class="p">,</span> <span class="s2">"~&gt; 1.9"</span>
<span class="n">gem</span> <span class="s2">"irb"</span><span class="p">,</span> <span class="s2">"~&gt; 1.18"</span>
<span class="n">gem</span> <span class="s2">"driq"</span><span class="p">,</span> <span class="s2">"~&gt; 0.4.3"</span>
</code></pre></div></div>

<h3 id="macosは自分がipv6に居ると思っている">macOSは自分がIPv6に居ると思っている</h3>
<p>dRubyでは一部のクラスのオブジェクトのやりとりにサーバが必要なので、ホストとポートを指定して起動するサーバとは別にクライアント側のプロセスでもサーバを起動します。</p>

<p>(サーバ)</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">$</span> <span class="n">bundle</span> <span class="nb">exec</span> <span class="n">irb</span> <span class="o">-</span><span class="n">r</span> <span class="n">drb</span>
<span class="o">&gt;</span> <span class="n">h</span> <span class="o">=</span> <span class="no">Hash</span><span class="p">.</span><span class="nf">new</span>
<span class="o">&gt;</span> <span class="no">DRb</span><span class="p">.</span><span class="nf">start_service</span><span class="p">(</span><span class="s1">'druby://localhost:54000'</span><span class="p">,</span> <span class="n">h</span><span class="p">)</span>
  <span class="p">:</span>
 <span class="vi">@exported_uri</span><span class="o">=</span><span class="p">[</span><span class="s2">"druby://localhost:54000"</span><span class="p">],</span>
  <span class="p">:</span>
   <span class="vi">@config</span><span class="o">=</span>
    <span class="p">{</span><span class="ss">tcp_original_host: </span><span class="s2">"localhost"</span><span class="p">,</span>
  <span class="p">:</span>
     <span class="ss">tcp_port: </span><span class="mi">54000</span><span class="p">},</span>
  <span class="p">:</span>
</code></pre></div></div>

<p>(クライアント)</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">$</span> <span class="n">bundle</span> <span class="nb">exec</span> <span class="n">irb</span> <span class="o">-</span><span class="n">r</span> <span class="n">drb</span>
<span class="o">&gt;</span> <span class="no">DRb</span><span class="p">.</span><span class="nf">start_service</span>
  <span class="p">:</span>
 <span class="vi">@exported_uri</span><span class="o">=</span><span class="p">[</span><span class="s2">"druby://::1:49357"</span><span class="p">],</span>
  <span class="p">:</span>
   <span class="vi">@config</span><span class="o">=</span>
    <span class="p">{</span><span class="ss">tcp_original_host: </span><span class="s2">""</span><span class="p">,</span>
  <span class="p">:</span>
     <span class="ss">tcp_port: </span><span class="mi">49357</span><span class="p">},</span>
<span class="o">&gt;</span> <span class="n">h</span> <span class="o">=</span> <span class="no">DRbObject</span><span class="p">.</span><span class="nf">new_with_uri</span><span class="p">(</span><span class="s2">"druby://localhost:54000"</span><span class="p">)</span>
<span class="o">&gt;</span> <span class="n">h</span><span class="p">[</span><span class="ss">:greeting</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"Hello, World!"</span>
</code></pre></div></div>

<p>ここで、<code class="language-plaintext highlighter-rouge">@exported_uri</code>が<code class="language-plaintext highlighter-rouge">druby://::1:49357</code>となっていることが気になりました。</p>

<h3 id="ipv6アドレスのuri">IPv6アドレスのURI</h3>
<p><a href="https://datatracker.ietf.org/doc/html/rfc2732#section-2">RFC 2732</a>によると、ホスト名部がIPv6であるURLではホスト名部を角かっこ<code class="language-plaintext highlighter-rouge">[]</code>で囲う必要があるようですが、上記で<code class="language-plaintext highlighter-rouge"> DRb.start_service</code>が返したオブジェクトでは、<code class="language-plaintext highlighter-rouge">@exported_uri</code>で<code class="language-plaintext highlighter-rouge">::1</code>が角かっこに囲まれていません。</p>

<p>ここでdRubyのコードを追ってみます。MacOS 15.7.5上のRuby 4.0.3のdRuby 2.2.3です。<code class="language-plaintext highlighter-rouge">@exported_uri</code>のホスト名部分は下記のメソッドで得ているようです。</p>

<p>(drb/drb.rb)</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">:</span>
    <span class="c1"># Returns the hostname of this server</span>
    <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">getservername</span>
      <span class="n">host</span> <span class="o">=</span> <span class="no">Socket</span><span class="o">::</span><span class="n">gethostname</span>
      <span class="k">begin</span>
        <span class="no">Socket</span><span class="o">::</span><span class="n">getaddrinfo</span><span class="p">(</span><span class="n">host</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span>
                                  <span class="no">Socket</span><span class="o">::</span><span class="no">AF_UNSPEC</span><span class="p">,</span>
                                  <span class="no">Socket</span><span class="o">::</span><span class="no">SOCK_STREAM</span><span class="p">,</span>
                                  <span class="mi">0</span><span class="p">,</span>
                                  <span class="no">Socket</span><span class="o">::</span><span class="no">AI_PASSIVE</span><span class="p">)[</span><span class="mi">0</span><span class="p">][</span><span class="mi">3</span><span class="p">]</span>
      <span class="k">rescue</span>
        <span class="s1">'localhost'</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="p">:</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">irb</code>で試してみると確かに<code class="language-plaintext highlighter-rouge">::1</code>になるようです。</p>

<p>URIにするコードは下記のようになっていました。たしかに<code class="language-plaintext highlighter-rouge">druby://::1:&lt;ポート番号&gt;</code>となりそうです。</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">:</span>
      <span class="k">if</span> <span class="o">...</span> 
        <span class="n">host</span> <span class="o">=</span> <span class="n">getservername</span>
        <span class="n">soc</span> <span class="o">=</span> <span class="n">open_server_inaddr_any</span><span class="p">(</span><span class="n">host</span><span class="p">,</span> <span class="n">port</span><span class="p">)</span>
      <span class="k">else</span>
  <span class="p">:</span>
      <span class="k">end</span>
      <span class="n">port</span> <span class="o">=</span> <span class="n">soc</span><span class="p">.</span><span class="nf">addr</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">...</span>
  <span class="p">:</span>
      <span class="n">uri</span> <span class="o">=</span> <span class="s2">"druby://</span><span class="si">#{</span><span class="n">host</span><span class="si">}</span><span class="s2">:</span><span class="si">#{</span><span class="n">port</span><span class="si">}</span><span class="s2">"</span>
  <span class="p">:</span>
</code></pre></div></div>

<p>このインスタンス変数はdRubyが動作する際に参照されることはなさそうだけれど、なんとなく気持ち悪いのでurl標準ライブラリなどでRFCどおりに整形してもらえばいいだろう。</p>

<h2 id="標準ライブラリでurlを整形してもらう">標準ライブラリでURLを整形してもらう</h2>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ irb

⢀⡴⠊⢉⡟⢿  IRB v1.18.0 - Ruby 4.0.3
⣎⣀⣴⡋⡟⣻  Type "help" for commands, "help &lt;cmd&gt;" for details
⣟⣼⣱⣽⣟⣾  ~/c/src/local/rk26-code-party

&gt; require 'uri'
&gt; s = URI::Generic.new('https', nil, '::1', 443, nil, '/', nil, nil, nil).to_s
=&gt; "https://::1:443/"
</code></pre></div></div>

<p>ありゃ。角かっこで囲まれていない。パースはできるのかな？</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; URI.parse(s)
.../4.0.3/lib/ruby/4.0.0/uri/rfc3986_parser.rb:130:in 'URI::RFC3986_Parser#split': bad URI (is not URI?): "https://::1:443/" (URI::InvalidURIError)
</code></pre></div></div>

<p>やっぱりできないかー。と思って、やっぱりできないかー。という姿勢をしていたら、だいじょうぶですか？とお声がけいただきました。ありがたいありがたい！これは絶望ではなく希望の姿勢なのです。</p>

<h2 id="一緒の部屋に居るのすごい">一緒の部屋に居るのすごい</h2>
<p>そんなこんなで、気づいたことを口頭でご報告して、uriライブラリについては報告先を推薦していただいて、20分間ほどで<a href="https://github.com/ruby/uri/issues/224">新規のIssue</a>の提出までさせていただいてしまったのでした。</p>

<p>プルリクエストにできるといいな。</p>

<p>(追記)</p>

<p><code class="language-plaintext highlighter-rouge">URI::Generic.new()</code>を呼び出す時に<code class="language-plaintext highlighter-rouge">arg_check</code>引数<sup id="fnref:arg"><a href="#fn:arg" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>に<code class="language-plaintext highlighter-rouge">true</code>を渡すと期待通りに角かっこが付くことがわかりました。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; URI::Generic.new('https', nil, '::1', 443, nil, '/', nil, nil, nil, URI::Generic::DEFAULT_PARSER, true)
=&gt; #&lt;URI::Generic https://[::1]:443/&gt;
</code></pre></div></div>

<p><a href="https://github.com/ruby/uri">ruby/uri</a>をforkして<code class="language-plaintext highlighter-rouge">test/uri/test_generic.rb</code>を眺めていたら<a href="https://github.com/ruby/uri/blob/52077e9/test/uri/test_generic.rb#L844-L853">ちゃんと角かっこを確認している</a>のを見つけました。</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:helper">
      <p>今回は<a href="https://x.com/rubykaigi/status/2023355565007839375">ヘルパーへの応募</a>が当たったので、裏方としてかなり楽しませていただいたのでした。 <a href="#fnref:helper" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:arg">
      <p><a href="https://docs.ruby-lang.org/en/4.0/URI/Generic.html#method-c-new">URI::Genericクラスのマニュアル</a>にあるように11番目の引数です。 <a href="#fnref:arg" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>zunda</name></author><category term="rubykaigi" /><category term="codeparty" /><summary type="html"><![CDATA[RubyKaigi 2026に参加してきました。現地参加はRubyKaigi 2015以来です。RubyKaigi本体も楽しかったのですが、関連イベントとして開催されたコード懇親会/Code Party(Social Coding) at RubyKaigi 2026もかなり楽しく、終電の差し迫るなか、勢い余ってruby/uriへのIssueを提出してしまったのでした。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.zunda.ninja/zunda-square.png" /><media:content medium="image" url="https://blog.zunda.ninja/zunda-square.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">NFCタグを名刺にする</title><link href="https://blog.zunda.ninja/nfc/2026/03/22/nfc-namecard.html" rel="alternate" type="text/html" title="NFCタグを名刺にする" /><published>2026-03-22T10:00:00+00:00</published><updated>2026-03-22T10:00:00+00:00</updated><id>https://blog.zunda.ninja/nfc/2026/03/22/nfc-namecard</id><content type="html" xml:base="https://blog.zunda.ninja/nfc/2026/03/22/nfc-namecard.html"><![CDATA[<p>僕は名札にアイコン、メールアドレス、OpenGPG鍵の指紋、<a href="https://zunda.ninja">ホームページのURL</a>の他、ホームページのURLをQRコードにしたものを掲載しています。ある日、NFCタグも用意すると携帯電話のカメラで撮影しなくてもホームページを訪問してもらえるのではないかと思い立ちました。</p>

<p>ホームページのURLを書き込んだNFCタグは付属の金属リングで名札の紐に取り付けました。下記の写真では左上に写っています。</p>

<p><img src="/assets/2026-03-22-namecard.jpg" alt="" /></p>

<p><a href="https://www.amazon.com/dp/B0FVFCDWK6">Amazonで売られているNFCタグ</a>を購入します。届いたNFCタグをそのまま携帯電話に近づけても、携帯電話は反応しません。</p>

<p>上記のAmazonのページで紹介されていたアプリケーション<a href="https://play.google.com/store/apps/details?id=com.wakdev.wdnfc">NFC Tools</a>を、手元のAndroidの携帯電話(Pixel 9a)にインストールし起動します。Readタブが選択された状態でNFCタグを携帯電話の背面に近づけると、NFCタグとしての情報が得られるようです。</p>

<p><img src="/assets/2026-03-22-read.png" alt="" /></p>

<table>
  <thead>
    <tr>
      <th>項目</th>
      <th>内容</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Tag type</td>
      <td>ISO 14443-3A, NXP - Mifare Ultralight</td>
    </tr>
    <tr>
      <td>Technologies available</td>
      <td>NfcA, Ndef</td>
    </tr>
    <tr>
      <td>ATQA</td>
      <td>0x0044</td>
    </tr>
    <tr>
      <td>SAK</td>
      <td>0x00</td>
    </tr>
    <tr>
      <td>Data format</td>
      <td>NFC Forum Type 2</td>
    </tr>
    <tr>
      <td>Size</td>
      <td>0 / 492 Bytes</td>
    </tr>
    <tr>
      <td>Writable</td>
      <td>Yes</td>
    </tr>
    <tr>
      <td>Can be made Read-Only</td>
      <td>Yes</td>
    </tr>
  </tbody>
</table>

<p>Writeタブを選択し、Add a recordボタンをタップし、URL / URIメニューをタップし、URLを入力します。Writeボタンをタップし、NFCタグを携帯電話の背面に近づけると、指定したURLがNFCタグに書き込まれます。</p>

<p>URLを書き込んだNFCタグをアンロックされた携帯電話の背面に近づけると、ブラウザが開き、URLの内容が表示されます。</p>]]></content><author><name>zunda</name></author><category term="NFC" /><summary type="html"><![CDATA[僕は名札にアイコン、メールアドレス、OpenGPG鍵の指紋、ホームページのURLの他、ホームページのURLをQRコードにしたものを掲載しています。ある日、NFCタグも用意すると携帯電話のカメラで撮影しなくてもホームページを訪問してもらえるのではないかと思い立ちました。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.zunda.ninja/zunda-square.png" /><media:content medium="image" url="https://blog.zunda.ninja/zunda-square.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Jekyllの記事にMastodonへの共有リンクを表示する</title><link href="https://blog.zunda.ninja/jekyll/mastodon/2026/03/02/share-button.html" rel="alternate" type="text/html" title="Jekyllの記事にMastodonへの共有リンクを表示する" /><published>2026-03-02T20:20:00+00:00</published><updated>2026-03-02T20:20:00+00:00</updated><id>https://blog.zunda.ninja/jekyll/mastodon/2026/03/02/share-button</id><content type="html" xml:base="https://blog.zunda.ninja/jekyll/mastodon/2026/03/02/share-button.html"><![CDATA[<p>ウェブページを<a href="https://blog.joinmastodon.org/2026/03/a-new-share-button/">Mastodonへ共有するボタンがリリースされました</a>。このJekyllサイトの記事にも、下の方にMastodonへ共有するリンクを表示します。</p>

<p><code class="language-plaintext highlighter-rouge">_includes/share.html</code>として下記のような内容のファイルを作ります。</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">abs_url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">absolute_url</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">url_encode</span><span class="w"> </span><span class="cp">%}</span>
&lt;div&gt;
&lt;a href="https://share.joinmastodon.org/#text=<span class="cp">{{</span><span class="w"> </span><span class="nv">page</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">append</span><span class="p">:</span><span class="w"> </span><span class="s2">" | "</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">append</span><span class="p">:</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">url_encode</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">append</span><span class="p">:</span><span class="w"> </span><span class="s2">"%0D"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">append</span><span class="p">:</span><span class="w"> </span><span class="nv">abs_url</span><span class="w"> </span><span class="cp">}}</span>" target="_blank" rel="noopener" aria-label="Share on Mastodon"&gt;Share on &lt;svg role="img" viewBox="0 0 74 79" fill="currentColor" xmlns="http://www.w3.org/2000/svg" width="1em"&gt;&lt;path d="M73.7014 17.4323C72.5616 9.05152 65.1774 2.4469 56.424 1.1671C54.9472 0.950843 49.3518 0.163818 36.3901 0.163818H36.2933C23.3281 0.163818 20.5465 0.950843 19.0697 1.1671C10.56 2.41145 2.78877 8.34604 0.903306 16.826C-0.00357854 21.0022 -0.100361 25.6322 0.068112 29.8793C0.308275 35.9699 0.354874 42.0498 0.91406 48.1156C1.30064 52.1448 1.97502 56.1419 2.93215 60.0769C4.72441 67.3445 11.9795 73.3925 19.0876 75.86C26.6979 78.4332 34.8821 78.8603 42.724 77.0937C43.5866 76.8952 44.4398 76.6647 45.2833 76.4024C47.1867 75.8033 49.4199 75.1332 51.0616 73.9562C51.0841 73.9397 51.1026 73.9184 51.1156 73.8938C51.1286 73.8693 51.1359 73.8421 51.1368 73.8144V67.9366C51.1364 67.9107 51.1302 67.8852 51.1186 67.862C51.1069 67.8388 51.0902 67.8184 51.0695 67.8025C51.0489 67.7865 51.0249 67.7753 50.9994 67.7696C50.9738 67.764 50.9473 67.7641 50.9218 67.7699C45.8976 68.9569 40.7491 69.5519 35.5836 69.5425C26.694 69.5425 24.3031 65.3699 23.6184 63.6327C23.0681 62.1314 22.7186 60.5654 22.5789 58.9744C22.5775 58.9477 22.5825 58.921 22.5934 58.8965C22.6043 58.8721 22.621 58.8505 22.6419 58.8336C22.6629 58.8167 22.6876 58.8049 22.714 58.7992C22.7404 58.7934 22.7678 58.794 22.794 58.8007C27.7345 59.9796 32.799 60.5746 37.8813 60.5733C39.1036 60.5733 40.3223 60.5733 41.5447 60.5414C46.6562 60.3996 52.0437 60.1408 57.0728 59.1694C57.1983 59.1446 57.3237 59.1233 57.4313 59.0914C65.3638 57.5847 72.9128 52.8555 73.6799 40.8799C73.7086 40.4084 73.7803 35.9415 73.7803 35.4523C73.7839 33.7896 74.3216 23.6576 73.7014 17.4323ZM61.4925 47.3144H53.1514V27.107C53.1514 22.8528 51.3591 20.6832 47.7136 20.6832C43.7061 20.6832 41.6988 23.2499 41.6988 28.3194V39.3803H33.4078V28.3194C33.4078 23.2499 31.3969 20.6832 27.3894 20.6832C23.7654 20.6832 21.9552 22.8528 21.9516 27.107V47.3144H13.6176V26.4937C13.6176 22.2395 14.7157 18.8598 16.9118 16.3545C19.1772 13.8552 22.1488 12.5719 25.8373 12.5719C30.1064 12.5719 33.3325 14.1955 35.4832 17.4394L37.5587 20.8853L39.6377 17.4394C41.7884 14.1955 45.0145 12.5719 49.2765 12.5719C52.9614 12.5719 55.9329 13.8552 58.2055 16.3545C60.4017 18.8574 61.4997 22.2371 61.4997 26.4937L61.4925 47.3144Z" fill="inherit"/&gt;&lt;/svg&gt;&lt;/a&gt;
&lt;/div&gt;
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">minima-2.5.2/_layouts/post.html</code>を<code class="language-plaintext highlighter-rouge">_layouts/post.html</code>としてコピーして、最後に下記の行を追加します。</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">{%</span><span class="w"> </span><span class="nt">include</span><span class="w"> </span>share.html<span class="w"> </span><span class="cp">%}</span>
</code></pre></div></div>]]></content><author><name>zunda</name></author><category term="jekyll" /><category term="Mastodon" /><summary type="html"><![CDATA[ウェブページをMastodonへ共有するボタンがリリースされました。このJekyllサイトの記事にも、下の方にMastodonへ共有するリンクを表示します。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.zunda.ninja/zunda-square.png" /><media:content medium="image" url="https://blog.zunda.ninja/zunda-square.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Vueでテストを書く</title><link href="https://blog.zunda.ninja/vue/test/2026/02/13/vue-test.html" rel="alternate" type="text/html" title="Vueでテストを書く" /><published>2026-02-13T01:45:00+00:00</published><updated>2026-02-25T03:00:00+00:00</updated><id>https://blog.zunda.ninja/vue/test/2026/02/13/vue-test</id><content type="html" xml:base="https://blog.zunda.ninja/vue/test/2026/02/13/vue-test.html"><![CDATA[<p>テストと一緒にVueのアプリを書いてみます。レポジトリを<a href="https://github.com/zunda/clean-url">zunda/clean-url</a>で、稼働例を<a href="https://clean-url.zunda.ninja">clean-url.zunda.ninja</a>で参照できます。</p>

<h2 id="雛形のアプリを作る">雛形のアプリを作る</h2>
<p>新しいディレクトリにVueのアプリを作ります。
手元のXubuntu 24.01には<code class="language-plaintext highlighter-rouge">yarn</code>パッケージとしてyarn 1.22.15が入っています。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">mkdir </span>clean-url
<span class="nv">$ </span><span class="nb">cd </span>clean-url
<span class="nv">$ </span>yarn init
<span class="nv">$ </span>yarn add <span class="nt">-D</span> @vue/cli
<span class="nv">$ </span>yarn run vue create <span class="nb">.</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">package.json</code>を眺めると、<code class="language-plaintext highlighter-rouge">yarn serve</code>で開発用のサーバを起動できそうです。</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"serve"</span><span class="p">:</span><span class="w"> </span><span class="s2">"vue-cli-service serve"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>サーバが起動しました。念のためブラウザで閲覧できるのを確認します。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>yarn serve

<span class="o">(</span>中略<span class="o">)</span>

  App running at:
  - Local:   http://localhost:8080/
  - Network: http://192.168.1.141:8080/

  Note that the development build is not optimized.
  To create a production build, run yarn build.

</code></pre></div></div>

<h2 id="ユニットテストを書いて実行する">ユニットテストを書いて実行する</h2>
<p>今回は、入力されたURLを加工するアプリを書いてみます。</p>

<p>まず、URLをパーズするヘルパ関数を書きます。
<code class="language-plaintext highlighter-rouge">src/helpers.js</code>に、とりあえず最小の機能を持った下記の関数を定義します。</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">function</span> <span class="nf">parseUrl</span><span class="p">(</span><span class="nx">url</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">URL</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="nx">url</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Vueによる<a href="https://vuejs.org/guide/scaling-up/testing.html#recommendation">Testing</a>ガイドによると、テストフレームワークには<a href="https://vitest.dev/">Vitest</a>が推奨のようです。インストールします。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>yarn add <span class="nt">-D</span> vitest
</code></pre></div></div>

<p><a href="https://vitest.dev/guide/#writing-tests">Getting Started</a>ガイドに従って、
<code class="language-plaintext highlighter-rouge">package.json</code>に<code class="language-plaintext highlighter-rouge">test</code>スクリプトを追加します。</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"test"</span><span class="p">:</span><span class="w"> </span><span class="s2">"vitest"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>さらに、<code class="language-plaintext highlighter-rouge">src/helpers.spec.js</code>にユニットテストを書きます。</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">describe</span><span class="p">,</span> <span class="nx">expect</span><span class="p">,</span> <span class="nx">test</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">vitest</span><span class="dl">'</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">parseUrl</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./helpers</span><span class="dl">'</span>

<span class="nf">describe</span><span class="p">(</span><span class="dl">'</span><span class="s1">parseUrl</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nf">test</span><span class="p">(</span><span class="dl">'</span><span class="s1">parses a minimal URL</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">parsed</span> <span class="o">=</span> <span class="nf">parseUrl</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://example.com</span><span class="dl">'</span><span class="p">)</span>
    <span class="kd">const</span> <span class="nx">expected</span> <span class="o">=</span> <span class="p">{</span> <span class="na">host</span><span class="p">:</span> <span class="dl">'</span><span class="s1">example.com</span><span class="dl">'</span><span class="p">,</span> <span class="na">protocol</span><span class="p">:</span> <span class="dl">'</span><span class="s1">https:</span><span class="dl">'</span> <span class="p">}</span>
    <span class="nb">Object</span><span class="p">.</span><span class="nf">entries</span><span class="p">(</span><span class="nx">expected</span><span class="p">).</span><span class="nf">forEach</span><span class="p">(([</span><span class="nx">prop</span><span class="p">,</span> <span class="nx">goal</span><span class="p">])</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nf">expect</span><span class="p">(</span><span class="nx">parsed</span><span class="p">[</span><span class="nx">prop</span><span class="p">]).</span><span class="nf">toBe</span><span class="p">(</span><span class="nx">goal</span><span class="p">)</span>
    <span class="p">})</span>
  <span class="p">})</span>
<span class="p">})</span>
</code></pre></div></div>

<p>ユニットテストを実行します。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>yarn <span class="nb">test
</span>yarn run v1.22.15
<span class="nv">$ </span>vitest

 DEV  v4.0.18 /home/zunda/c/src/local/clean-url

 ✓ src/helpers.spec.js <span class="o">(</span>1 <span class="nb">test</span><span class="o">)</span> 3ms
   ✓ parseUrl <span class="o">(</span>1<span class="o">)</span>
     ✓ parses a minimal URL 1ms

 Test Files  1 passed <span class="o">(</span>1<span class="o">)</span>
      Tests  1 passed <span class="o">(</span>1<span class="o">)</span>
   Start at  15:11:03
   Duration  190ms <span class="o">(</span>transform 29ms, setup 0ms, import 44ms, tests 3ms, environment 0ms<span class="o">)</span>

 PASS  Waiting <span class="k">for </span>file changes...
       press h to show <span class="nb">help</span>, press q to quit
</code></pre></div></div>

<p>成功しました。</p>

<h2 id="vueコンポーネントのふるまいをテストする">Vueコンポーネントのふるまいをテストする</h2>
<p>VitestのComponent Testingガイドによると、<a href="https://vitest.dev/guide/browser/component-testing.html#available-testing-library-packages">Vueのテストには<code class="language-plaintext highlighter-rouge">@testing-library/vue</code>を使うのが一般的</a>なようです。<a href="https://testing-library.com/docs/vue-testing-library/intro/#quickstart">Testing LibraryのQuickstart</a>に従ってテストを進めてみます。</p>

<p>ここでは、<code class="language-plaintext highlighter-rouge">src/components/UrlCleaner.vue</code>に定義した、<code class="language-plaintext highlighter-rouge">UrlCleanerComponent</code>をテストしてみます。1つ目の<code class="language-plaintext highlighter-rouge">input</code>エレメントに任意のURLを入力すると、2つ目の読み取り専用の<code class="language-plaintext highlighter-rouge">input</code>エレメントに<code class="language-plaintext highlighter-rouge">v=…</code>以外のクエリパラメータが削除されたURLが出力されるはずです。</p>

<p>この段階では、<code class="language-plaintext highlighter-rouge">src/UrlCleaner.js</code>から<code class="language-plaintext highlighter-rouge">UrlCleaner</code>クラスがエキスポートされて、このクラスには<code class="language-plaintext highlighter-rouge">parse()</code>スタティックメソッドと<code class="language-plaintext highlighter-rouge">removeQueriesExceptFor()</code>メソッドが定義されていました。</p>

<div class="language-vue highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;</span><span class="k">template</span><span class="nt">&gt;</span>
  <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"urlCleaner"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;input</span> <span class="na">v-model=</span><span class="s">"dirtyUrl"</span>  <span class="na">placeholder=</span><span class="s">"Dirty URL"</span> <span class="na">type=</span><span class="s">"url"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;br/&gt;</span>
    <span class="nt">&lt;input</span> <span class="na">v-model=</span><span class="s">"cleanUrl"</span> <span class="na">placeholder=</span><span class="s">"Clean URL"</span> <span class="na">readonly=</span><span class="s">"readonly"</span> <span class="nt">/&gt;</span>
  <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/</span><span class="k">template</span><span class="nt">&gt;</span>

<span class="nt">&lt;</span><span class="k">script</span><span class="nt">&gt;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">UrlCleaner</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../UrlCleaner</span><span class="dl">'</span>

<span class="k">export</span> <span class="k">default</span> <span class="p">{</span>
  <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">UrlCleanerComponent</span><span class="dl">'</span><span class="p">,</span>
  <span class="nf">data</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span>
      <span class="na">dirtyUrl</span><span class="p">:</span> <span class="dl">""</span>
    <span class="p">}</span>
  <span class="p">},</span>
  <span class="na">computed</span><span class="p">:</span> <span class="p">{</span>
    <span class="nf">cleanUrl</span><span class="p">()</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">x</span> <span class="o">=</span> <span class="nx">UrlCleaner</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">dirtyUrl</span><span class="p">)</span>
      <span class="k">return</span> <span class="nx">x</span> <span class="p">?</span> <span class="nx">x</span><span class="p">.</span><span class="nf">removeQueriesExceptFor</span><span class="p">([</span><span class="dl">"</span><span class="s2">v</span><span class="dl">"</span><span class="p">]).</span><span class="nf">toString</span><span class="p">()</span> <span class="p">:</span> <span class="dl">""</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
<span class="nt">&lt;/</span><span class="k">script</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p>テストでVueコンポーネントを<code class="language-plaintext highlighter-rouge">import</code>するためには、<code class="language-plaintext highlighter-rouge">@vitejs/plugin-vue</code>をインスールして有効にする必要があるようです<sup id="fnref:plugin-vue"><a href="#fn:plugin-vue" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>。NodeからでブラウザのAPIを利用できるようにするため、jsdomもインストールします<sup id="fnref:jsdom"><a href="#fn:jsdom" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>yarn add <span class="nt">--dev</span> @testing-library/vue @vitejs/plugin-vue jsdom
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">vite.config.js</code>として下記のファイルを作成します。</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">vite</span><span class="dl">'</span>
<span class="k">import</span> <span class="nx">vue</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@vitejs/plugin-vue</span><span class="dl">'</span>

<span class="k">export</span> <span class="k">default</span> <span class="nf">defineConfig</span><span class="p">({</span>
  <span class="na">plugins</span><span class="p">:</span> <span class="p">[</span><span class="nf">vue</span><span class="p">()],</span>
  <span class="na">test</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">include</span><span class="p">:</span> <span class="p">[</span>
      <span class="dl">'</span><span class="s1">./src/**/*.spec.js</span><span class="dl">'</span>
    <span class="p">],</span>
    <span class="na">environment</span><span class="p">:</span> <span class="dl">'</span><span class="s1">jsdom</span><span class="dl">'</span>
  <span class="p">}</span>
<span class="p">})</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">src/tests/UrlCleaner.spec.js</code>にテストを書き始めます。<a href="https://testing-library.com/docs/vue-testing-library/examples/#example-using-v-model">Testing Libraryによる例</a>では、<code class="language-plaintext highlighter-rouge">await fireEvent.update(エレメント, 文字列)</code>で入力するようです。</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">describe</span><span class="p">,</span> <span class="nx">expect</span><span class="p">,</span> <span class="nx">test</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">vitest</span><span class="dl">'</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">render</span><span class="p">,</span> <span class="nx">cleanup</span><span class="p">,</span> <span class="nx">fireEvent</span><span class="p">,</span> <span class="nx">screen</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@testing-library/vue</span><span class="dl">'</span>
<span class="k">import</span> <span class="nx">UrlCleanerComponent</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../components/UrlCleaner.vue</span><span class="dl">'</span>

<span class="nf">describe</span><span class="p">(</span><span class="dl">'</span><span class="s1">UrlCleanerComponent</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nf">test</span><span class="p">(</span><span class="dl">'</span><span class="s1">removes queries from a URL</span><span class="dl">'</span><span class="p">,</span> <span class="k">async </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nf">render</span><span class="p">(</span><span class="nx">UrlCleanerComponent</span><span class="p">)</span>
    <span class="kd">const</span> <span class="nx">dirty</span> <span class="o">=</span> <span class="nx">screen</span><span class="p">.</span><span class="nf">getByPlaceholderText</span><span class="p">(</span><span class="dl">'</span><span class="s1">Dirty URL</span><span class="dl">'</span><span class="p">)</span>
    <span class="kd">const</span> <span class="nx">clean</span> <span class="o">=</span> <span class="nx">screen</span><span class="p">.</span><span class="nf">getByPlaceholderText</span><span class="p">(</span><span class="dl">'</span><span class="s1">Clean URL</span><span class="dl">'</span><span class="p">)</span>

    <span class="kd">const</span> <span class="nx">dirtyUrl</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">http://example.com/path/?p=parameter&amp;v=keep</span><span class="dl">'</span>
    <span class="kd">const</span> <span class="nx">cleanUrl</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">http://example.com/path/?v=keep</span><span class="dl">'</span>

    <span class="k">await</span> <span class="nx">fireEvent</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="nx">dirty</span><span class="p">,</span> <span class="nx">dirtyUrl</span><span class="p">)</span>

    <span class="nf">expect</span><span class="p">(</span><span class="nx">dirty</span><span class="p">.</span><span class="nx">value</span><span class="p">).</span><span class="nf">toBe</span><span class="p">(</span><span class="nx">dirtyUrl</span><span class="p">)</span>
    <span class="nf">expect</span><span class="p">(</span><span class="nx">clean</span><span class="p">.</span><span class="nx">value</span><span class="p">).</span><span class="nf">toBe</span><span class="p">(</span><span class="nx">cleanUrl</span><span class="p">)</span>

    <span class="nf">cleanup</span><span class="p">()</span>
  <span class="p">})</span>
<span class="p">})</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--run</code>オプションを付けて実行すると<code class="language-plaintext highlighter-rouge">vitest</code>は一度だけの実行で停止してくれるようです。テストが通りました。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ yarn test --run src/tests/UrlCleaner.spec.js
yarn run v1.22.15
$ vitest --run src/tests/UrlCleaner.spec.js

 RUN  v4.0.18 /home/zunda/c/src/local/clean-url

 ✓ src/tests/UrlCleaner.spec.js (1 test) 36ms
   ✓ UrlCleanerComponent (1)
     ✓ removes queries from a URL 34ms

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  11:31:41
   Duration  1.13s (transform 95ms, setup 0ms, import 300ms, tests 36ms, environment 631ms)

Done in 1.65s.
</code></pre></div></div>

<h2 id="yarnを新しいものにする">Yarnを新しいものにする</h2>
<p>コードを書き続けていると、
手元のyarn 1.22.15では<code class="language-plaintext highlighter-rouge">node_modules</code>ディレクトリ以下にインストールされるモジュールのバージョンが安定せず、
<code class="language-plaintext highlighter-rouge">yarn install</code>の繰り返しによって時折コマンドが失敗することがわかりました。</p>

<p><a href="https://yarnpkg.com/">Yarn 4+のページ</a>に従って<code class="language-plaintext highlighter-rouge">yarn</code>コマンドを4.12.0に更新したところ、
<code class="language-plaintext highlighter-rouge">yarn test</code>コマンドの実行によってインストールされているモジュールのテストも走るようになりました。
<code class="language-plaintext highlighter-rouge">vite.config.js</code>で定義した<code class="language-plaintext highlighter-rouge">defineConfig</code>関数の引数の<code class="language-plaintext highlighter-rouge">test</code>プロパティに<code class="language-plaintext highlighter-rouge">include</code>プロパティとして<code class="language-plaintext highlighter-rouge">['./src/**/*.spec.js']</code>を追加することで、
<code class="language-plaintext highlighter-rouge">src</code>ディレクトリ以下のテストのみを走らせることができました。</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:plugin-vue">
      <p><a href="https://www.issoh.co.jp/tech/details/9465/">Vitest 4登場！パフォーマンス向上やブラウザモード強化など注目の新機能・改良点の全貌とメリットを徹底解説 | 株式会社一創</a> <a href="#fnref:plugin-vue" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:jsdom">
      <p><a href="https://stackoverflow.com/a/70771291/5716182">vue.js - Vue test utils for Vue3 : document is not defined - Stack Overflow</a> <a href="#fnref:jsdom" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>zunda</name></author><category term="vue" /><category term="test" /><summary type="html"><![CDATA[テストと一緒にVueのアプリを書いてみます。レポジトリをzunda/clean-urlで、稼働例をclean-url.zunda.ninjaで参照できます。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.zunda.ninja/zunda-square.png" /><media:content medium="image" url="https://blog.zunda.ninja/zunda-square.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Jekyllの記事にJavaScriptを書く</title><link href="https://blog.zunda.ninja/jekyll/kramdown/2026/02/04/javascript.html" rel="alternate" type="text/html" title="Jekyllの記事にJavaScriptを書く" /><published>2026-02-04T08:00:00+00:00</published><updated>2026-02-04T08:00:00+00:00</updated><id>https://blog.zunda.ninja/jekyll/kramdown/2026/02/04/javascript</id><content type="html" xml:base="https://blog.zunda.ninja/jekyll/kramdown/2026/02/04/javascript.html"><![CDATA[<p>
<form id="form">
<input type="text" id="src" placeholder="元の文字列" />に
<select id="mark">
<option value="full" selected="">濁点</option>
<option value="half">半濁点</option>
</select>を<button>くっつける</button>と
<input type="text" id="dst" placeholder="こうなる" />
</form>
</p>
<script>
const form = document.getElementById("form");
const mark = document.getElementById("mark");
const src = document.getElementById("src");
const dst = document.getElementById("dst");
function convert(event) {
  event.preventDefault();
  var m = "\u3099";
  if (mark.value == "half") { m = "\u309A"; }
  dst.value = Array.from(src.value).map((c) => c + m).join("");
}
form.addEventListener("submit", convert);
</script>

<p>kramdownにはHTMLだけじゃなくてJavaScriptもそのまま書けるんだ。</p>

<p>例えば、下記のようなフォームとコードを原稿に直書きしておくと、上記のように実行できるようです。Jekyllはページをビルドする時に<a href="https://shopify.github.io/liquid/">Liquidテンプレートエンジン</a>を適用するので、HTMLやJavaScript中にLiquidのタグが現れる場合には<a href="https://shopify.dev/docs/api/liquid/tags/raw">rawタグ</a>で囲むなどの注意が必要かもしれません。</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;p&gt;</span>
<span class="nt">&lt;form</span> <span class="na">id=</span><span class="s">"form"</span><span class="nt">&gt;</span>
<span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">id=</span><span class="s">"src"</span> <span class="na">placeholder=</span><span class="s">"元の文字列"</span> <span class="nt">/&gt;</span>に
<span class="nt">&lt;select</span> <span class="na">id=</span><span class="s">"mark"</span><span class="nt">&gt;</span>
<span class="nt">&lt;option</span> <span class="na">value=</span><span class="s">"full"</span> <span class="na">selected</span><span class="nt">&gt;</span>濁点<span class="nt">&lt;/option&gt;</span>
<span class="nt">&lt;option</span> <span class="na">value=</span><span class="s">"half"</span><span class="nt">&gt;</span>半濁点<span class="nt">&lt;/option&gt;</span>
<span class="nt">&lt;/select&gt;</span>を<span class="nt">&lt;button&gt;</span>くっつける<span class="nt">&lt;/button&gt;</span>と
<span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">id=</span><span class="s">"dst"</span> <span class="na">placeholder=</span><span class="s">"こうなる"</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;/form&gt;</span>
<span class="nt">&lt;/p&gt;</span>
<span class="nt">&lt;script&gt;</span>
<span class="kd">const</span> <span class="nx">form</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">getElementById</span><span class="p">(</span><span class="dl">"</span><span class="s2">form</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">mark</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">getElementById</span><span class="p">(</span><span class="dl">"</span><span class="s2">mark</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">src</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">getElementById</span><span class="p">(</span><span class="dl">"</span><span class="s2">src</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">dst</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">getElementById</span><span class="p">(</span><span class="dl">"</span><span class="s2">dst</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">function</span> <span class="nf">convert</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">event</span><span class="p">.</span><span class="nf">preventDefault</span><span class="p">();</span>
  <span class="kd">var</span> <span class="nx">m</span> <span class="o">=</span> <span class="dl">"</span><span class="se">\</span><span class="s2">u3099</span><span class="dl">"</span><span class="p">;</span>
  <span class="k">if </span><span class="p">(</span><span class="nx">mark</span><span class="p">.</span><span class="nx">value</span> <span class="o">==</span> <span class="dl">"</span><span class="s2">half</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span> <span class="nx">m</span> <span class="o">=</span> <span class="dl">"</span><span class="se">\</span><span class="s2">u309A</span><span class="dl">"</span><span class="p">;</span> <span class="p">}</span>
  <span class="nx">dst</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="nb">Array</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">src</span><span class="p">.</span><span class="nx">value</span><span class="p">).</span><span class="nf">map</span><span class="p">((</span><span class="nx">c</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">c</span> <span class="o">+</span> <span class="nx">m</span><span class="p">).</span><span class="nf">join</span><span class="p">(</span><span class="dl">""</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">form</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">submit</span><span class="dl">"</span><span class="p">,</span> <span class="nx">convert</span><span class="p">);</span>
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>]]></content><author><name>zunda</name></author><category term="jekyll" /><category term="kramdown" /><summary type="html"><![CDATA[に 濁点 半濁点 をくっつけると]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.zunda.ninja/zunda-square.png" /><media:content medium="image" url="https://blog.zunda.ninja/zunda-square.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Jekyllの記事に更新日を記す</title><link href="https://blog.zunda.ninja/jekyll/update/2026/01/30/last-modified-at.html" rel="alternate" type="text/html" title="Jekyllの記事に更新日を記す" /><published>2026-01-30T20:30:00+00:00</published><updated>2026-01-31T00:20:00+00:00</updated><id>https://blog.zunda.ninja/jekyll/update/2026/01/30/last-modified-at</id><content type="html" xml:base="https://blog.zunda.ninja/jekyll/update/2026/01/30/last-modified-at.html"><![CDATA[<p>これまでに公開した記事を改訂したくなったので、記事に更新日を明記します。</p>

<h2 id="last_modified_at変数を表示する">last_modified_at変数を表示する</h2>
<p><a href="https://pl.kpherox.dev/objects/7848aa88-ca45-4f09-833d-62c9ecc13270">jekyll-sitemapやjekyll-feedは<code class="language-plaintext highlighter-rouge">page.last_modified_at</code>に既に対応している</a>とのことです。確かに、jekyll-feed-0.17.0の<code class="language-plaintext highlighter-rouge">feed.xml</code>に下記のような行が含まれていて、<code class="language-plaintext highlighter-rouge">published</code>タグに<code class="language-plaintext highlighter-rouge">date</code>変数が、<code class="language-plaintext highlighter-rouge">updated</code>タグに<code class="language-plaintext highlighter-rouge">last_modified_at</code>変数が適用されることがわかります。</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;title</span> <span class="na">type=</span><span class="s">"html"</span><span class="nt">&gt;</span>{{ post_title }}<span class="nt">&lt;/title&gt;</span>
<span class="nt">&lt;link</span> <span class="na">href=</span><span class="s">"{{ post.url | absolute_url }}"</span> <span class="na">rel=</span><span class="s">"alternate"</span> <span class="na">type=</span><span class="s">"text/html"</span> <span class="na">title=</span><span class="s">"{{ post_title }}"</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;published&gt;</span>{{ post.date | date_to_xmlschema }}<span class="nt">&lt;/published&gt;</span>
<span class="nt">&lt;updated&gt;</span>{{ post.last_modified_at | default: post.date | date_to_xmlschema }}<span class="nt">&lt;/updated&gt;</span>
</code></pre></div></div>

<p>minima-2.5.2の<code class="language-plaintext highlighter-rouge">_layouts/posts.html</code>を<code class="language-plaintext highlighter-rouge">./_layouts/</code>以下にコピーしてきて下記のように編集します。</p>

<div class="language-patch highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh">diff --git a/_layouts/post.html b/_layouts/post.html
index abf9696..8abc4a9 100644
</span><span class="gd">--- a/_layouts/post.html
</span><span class="gi">+++ b/_layouts/post.html
</span><span class="p">@@ -6,10 +6,17 @@</span> layout: default
   &lt;header class="post-header"&gt;
     &lt;h1 class="post-title p-name" itemprop="name headline"&gt;{{ page.title | escape }}&lt;/h1&gt;
     &lt;p class="post-meta"&gt;
<span class="gi">+      公開:
</span>       &lt;time class="dt-published" datetime="{{ page.date | date_to_xmlschema }}" itemprop="datePublished"&gt;
         {%- assign date_format = site.minima.date_format | default: "%b %-d, %Y" -%}
         {{ page.date | date: date_format }}
       &lt;/time&gt;
<span class="gi">+      {%- if page.last_modified_at -%}
+        • 更新:
+        &lt;time class="dt-published" datetime="{{ page.last_modified_at | date_to_xmlschema }}" itemprop="dateModified"&gt;
+          {{ page.last_modified_at | date: date_format }}
+        &lt;/time&gt;
+      {%- endif -%}
</span>       {%- if page.author -%}
         • &lt;span itemprop="author" itemscope itemtype="http://schema.org/Person"&gt;&lt;span class="p-author h-card" itemprop="name"&gt;{{ page.author }}&lt;/span&gt;&lt;/span&gt;
       {%- endif -%}&lt;/p&gt;
</code></pre></div></div>

<p>(追記) Schema.orgを眺めていて<a href="https://schema.org/datePublished"><code class="language-plaintext highlighter-rouge">datePublished</code>プロパティ</a>に加えて<a href="https://schema.org/dateModified"><code class="language-plaintext highlighter-rouge">dateModified</code>プロパティ</a>があるのを見つけました。初出時には<code class="language-plaintext highlighter-rouge">&lt;span&gt;</code>に含めていた更新時刻を<code class="language-plaintext highlighter-rouge">&lt;time itemprop="dateModified"&gt;</code>に含めるよう変更しました。</p>

<h2 id="記事に更新日時を設定する">記事に更新日時を設定する</h2>
<p>フロントマターの<code class="language-plaintext highlighter-rouge">last_modified_at</code>変数を設定する。この記事では下記のように設定してみました。</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">layout</span><span class="pi">:</span> <span class="s">post</span>
<span class="na">title</span><span class="pi">:</span>  <span class="s2">"</span><span class="s">Jekyllの記事に更新日を記す"</span>
<span class="na">date</span><span class="pi">:</span>   <span class="s">2026-01-30 10:30:00 -1000</span>
<span class="na">last_modified_at</span><span class="pi">:</span> <span class="s">2026-01-30 14:20:00 -1000</span>
<span class="na">categories</span><span class="pi">:</span> <span class="s">jekyll update</span>
</code></pre></div></div>]]></content><author><name>zunda</name></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[これまでに公開した記事を改訂したくなったので、記事に更新日を明記します。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.zunda.ninja/zunda-square.png" /><media:content medium="image" url="https://blog.zunda.ninja/zunda-square.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">bashスクリプトで子プロセスにSIGTERMを転送する</title><link href="https://blog.zunda.ninja/bash/2026/01/30/bash-sigterm.html" rel="alternate" type="text/html" title="bashスクリプトで子プロセスにSIGTERMを転送する" /><published>2026-01-30T02:30:00+00:00</published><updated>2026-01-30T20:45:00+00:00</updated><id>https://blog.zunda.ninja/bash/2026/01/30/bash-sigterm</id><content type="html" xml:base="https://blog.zunda.ninja/bash/2026/01/30/bash-sigterm.html"><![CDATA[<p>HerokuというPlatform as a Service (PaaS)ではコンテナの停止時に<a href="https://devcenter.heroku.com/articles/dyno-shutdown-behavior#sigterm-signal">コンテナ内のすべてのプロセスにSIGTERMを送ります</a>が、ナウでヤングなDockerやk8sなどでは親プロセスのみにSIGTERM送るのが一般的なのだそうです。このような状況でも子プロセスにSIGTERMを転送してくれるbashスクリプトを書きます。</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
bundle <span class="nb">exec </span>puma <span class="nt">-C</span> config/puma.rb&amp;
bundle <span class="nb">exec </span>bin/jobs&amp;
<span class="nv">cpids</span><span class="o">=</span><span class="sb">`</span>pgrep <span class="nt">-P</span> <span class="nv">$$</span><span class="sb">`</span><span class="p">;</span>
<span class="nb">trap</span> <span class="s1">'for pid in $cpids; do kill -TERM $pid; done'</span> TERM<span class="p">;</span>
<span class="nb">wait</span> <span class="nt">-n</span><span class="p">;</span>
<span class="nb">kill</span> <span class="nt">-TERM</span> <span class="nv">$$</span><span class="p">;</span>
<span class="nb">wait</span>
</code></pre></div></div>

<p>上記のbashスクリプトは、PumaとSolid Queue(<code class="language-plaintext highlighter-rouge">bin/jobs</code>コマンド)を子プロセスとして起動して、</p>

<ul>
  <li>自スクリプトを実行しているbashプロセスがSIGTERMを受けた、</li>
  <li>自スクリプトを実行しているbashプロセスと子プロセスがSIGTERMを受けた、あるいは、</li>
  <li>いずれかの子プロセスが何らかの理由で停止した</li>
</ul>

<p>時に、</p>

<ol>
  <li>すべての子プロセスにSIGTERMを送って、</li>
  <li>すべての子プロセスが停止したら</li>
  <li>自スクリプトを実行しているbashプロセスを停止する</li>
</ol>

<p>ようになっているはずです。</p>

<h2 id="自スクリプトを実行しているbashプロセスのidを得る">自スクリプトを実行しているbashプロセスのIDを得る</h2>
<p>bashスクリプト内の<code class="language-plaintext highlighter-rouge">$$</code>パラメータは、それを実行しているbashプロセスのIDに展開されます。</p>

<h2 id="子プロセスのidを列挙する">子プロセスのIDを列挙する</h2>
<p>シグナルを転送する先のプロセスIDを得るために、<code class="language-plaintext highlighter-rouge">pgrep</code>コマンドを利用します。<code class="language-plaintext highlighter-rouge">-P</code>オプションを渡すと指定したプロセスの子プロセスを列挙してくれます。<code class="language-plaintext highlighter-rouge">man pgrep</code>より:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-P, --parent ppid,...
       Only match processes whose parent process ID is listed.
</code></pre></div></div>

<p>上記のスクリプトでは、得られたプロセスIDの一覧をシェル変数に格納します。</p>

<h2 id="シグナルを受けた時の動作を指定する">シグナルを受けた時の動作を指定する</h2>
<p><code class="language-plaintext highlighter-rouge">trap</code>組み込みコマンドでスクリプトを実行しているbashプロセスがシグナルを受け取った時の動作を指定できます。文字列として渡した実行内容をシグナルを受け取った時に展開するようです。</p>

<p>上記のスクリプトでは、得られたプロセスIDの一覧を展開します。</p>

<h2 id="子プロセスの終了を待つ">子プロセスの終了を待つ</h2>
<p><code class="language-plaintext highlighter-rouge">wait</code>組み込みコマンドで子プロセスの終了を待つことができます。<code class="language-plaintext highlighter-rouge">-n</code>オプションを付けるといずれかの子プロセスが終了するまでブロックします。オプションを付けないと全ての子プロセスが終了するまでブロックします。</p>

<h2 id="残っている子プロセスにシグナルを送る">残っている子プロセスにシグナルを送る</h2>
<p>上記のスクリプトでは、SIGTERM以外のきっかけでいずれかの子プロセスが停止した場合に、自プロセスにSIGTERMを送ることで<code class="language-plaintext highlighter-rouge">trap</code>に設定したコマンドが実行し残っている子プロセスにもSIGTERMを送ります。</p>]]></content><author><name>zunda</name></author><category term="bash" /><summary type="html"><![CDATA[HerokuというPlatform as a Service (PaaS)ではコンテナの停止時にコンテナ内のすべてのプロセスにSIGTERMを送りますが、ナウでヤングなDockerやk8sなどでは親プロセスのみにSIGTERM送るのが一般的なのだそうです。このような状況でも子プロセスにSIGTERMを転送してくれるbashスクリプトを書きます。]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.zunda.ninja/zunda-square.png" /><media:content medium="image" url="https://blog.zunda.ninja/zunda-square.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Jekyllの記事に脚注を書く</title><link href="https://blog.zunda.ninja/jekyll/update/2026/01/27/footnotes.html" rel="alternate" type="text/html" title="Jekyllの記事に脚注を書く" /><published>2026-01-27T01:40:00+00:00</published><updated>2026-01-27T01:40:00+00:00</updated><id>https://blog.zunda.ninja/jekyll/update/2026/01/27/footnotes</id><content type="html" xml:base="https://blog.zunda.ninja/jekyll/update/2026/01/27/footnotes.html"><![CDATA[<h2 id="kramdownによる記述">kramdownによる記述</h2>

<p>kramdownを利用しているJekyllでは、<a href="https://kramdown.gettalong.org/syntax.html#footnotes">kramdownの仕様</a>に従って脚注を書くことができるようです。</p>

<p>下記のような原稿を書いておくと、</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; 吾輩は猫である。名前はまだ無い。[^footnote]
&gt; 
&gt; [^footnote]: 夏目漱石『吾輩は猫である』より
&gt; 
&gt; どこで生れたかとんと見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。
</code></pre></div></div>

<p>下記のように表示されます。</p>

<blockquote>
  <p>吾輩は猫である。名前はまだ無い。<sup id="fnref:footnote"><a href="#fn:footnote" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

  <p>どこで生れたかとんと見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。</p>
</blockquote>

<h2 id="スタイルシート">スタイルシート</h2>
<p>minimaテーマには脚注関連のスタイルは定義されていないようです。とりあえず<sup id="fnref:css"><a href="#fn:css" class="footnote" rel="footnote" role="doc-noteref">2</a></sup><code class="language-plaintext highlighter-rouge">minima/_layout.scss</code>に追加しておきます。</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">/**
 * footernote
 */</span>
<span class="nc">.footnotes</span> <span class="p">{</span>
  <span class="nl">border-top</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="err">$</span><span class="n">grey-color-light</span><span class="p">;</span>
  <span class="nl">padding</span><span class="p">:</span> <span class="err">$</span><span class="n">spacing-unit</span> <span class="m">0</span><span class="p">;</span>
  <span class="nl">font-size</span><span class="p">:</span> <span class="err">$</span><span class="n">small-font-size</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:footnote">
      <p>夏目漱石『吾輩は猫である』より <a href="#fnref:footnote" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:css">
      <p>zundaはCSSのことをよく知らないので見様見真似です <a href="#fnref:css" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>zunda</name></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[kramdownによる記述]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.zunda.ninja/zunda-square.png" /><media:content medium="image" url="https://blog.zunda.ninja/zunda-square.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>