エリクサーとフェニックスでブログを構築する

2026/03/27 2:16

エリクサーとフェニックスでブログを構築する

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

このブログでは、Elixir の Phoenix フレームワークと NimblePublisher を使って完全に機能する個人ウェブサイトを構築する方法を紹介しています。
アーキテクチャ:Phoenix はサーバー側でレンダリングを行い、

priv/posts/**/*.md
内の Markdown 投稿はモジュール属性へ事前コンパイルされるため、実行時の処理が不要です。
スタイリングとハイライト:Makeup が構文ハイライトを担当し、Tailwind CSS のクラスは Earmark タグプロセッサーを通じて注入されます。
デプロイメントパイプライン:マルチステージ Dockerfile(Debian trixie 上の Elixir 1.18.4 / Erlang 28.0.2)を GitHub Actions でビルドし、Hetzner の Dokploy を介して自動構築とローリングアップデートで展開します。Dependabot が依存関係を最新に保ちます。
CI/CD:ワークフローは
mix compile
mix format
mix credo
、および
mix test
を実行します。
SEO とフィード:Phoenix コントローラが RSS フィード(
JolaDevWeb.RssXML
)を提供し、サイトマップジェネレーターはすべてのページと投稿を一覧化し、動的に Open Graph/Twitter メタデータを各投稿に挿入します。
レガシー URL サポート
BlogRedirect
プラグインが古い
blog.jola.dev/*
リンクを新しい
/posts/:id
ルートへ書き換えます。
AI‑フレンドリーエンドポイント:追加のプレーンテキストリスト(
llms.txt
llms-full.txt
)が AI がコンテンツを取得できるように投稿を公開します。
呼びかけ:著者は他の Elixir 開発者にこのスタックを採用することを奨励し、Hetzner(紹介リンク付き)を推奨し、貢献を歓迎しています。完全なソースコードは
https://github.com/joladev/jola.dev
で入手できます。

本文

TL;DR
Elixirで構築したPhoenixアプリケーションは、サーバー側でページをレンダリングします。ブログ投稿はMarkdownファイルとして保存され、NimblePublisherによってモジュール属性にコンパイルされます。このサイトは自己ホスト型のDokploy(Hetzner)上で稼働し、bunny.netをフロントエンドに利用しています。


Phoenix を選んだ理由

以前は Hakyll などの静的サイトジェネレーターを使っていましたが、純粋な静的サイトでは実現できないインタラクティブな実験を追加したいと考えました。Phoenix は完全にコントロールできますので、サードパーティツールに依存せず、Ectoやデータベースなしで高速なサーバーサイドレンダリングが可能です。

結果: PageSpeed Insights で確認できるほどの高速ページ(Blazingly fast pages)。


NimblePublisher

ブログの核は NimblePublisher です。設定は

JolaDev.Blog
にあります:

defmodule JolaDev.Blog do
  use NimblePublisher,
    build:      JolaDev.Blog.Post,
    from:       Application.app_dir(:jola_dev, "priv/posts/**/*.md"),
    as:         :posts,
    html_converter: JolaDev.Blog.MarkdownConverter,
    highlighters:   [:makeup_elixir]
end
  • priv/posts
    配下の Markdown ファイルをすべて読み込みます。
  • フロントマターを解析します。
  • Markdown を HTML に変換(カスタムコンバータ経由)。
  • 結果はモジュール属性に保存され、実行時には何も起きません。

投稿ヘルパー

@posts Enum.sort_by(@posts, & &1.date, {:desc, Date})

@tags @posts
       |> Enum.flat_map(& &1.tags)
       |> Enum.uniq()
       |> Enum.sort()

def all_posts, do: @posts
def all_tags,  do: @tags

def posts_by_tag(tag) do
  Enum.filter(all_posts(), fn post -> tag in post.tags end)
end

def find_by_id(id) do
  Enum.find(all_posts(), &(&1.id == id))
end

Markdown 出力に Tailwind クラスを注入

Earmark
を使って Tailwind のクラスを挿入します:

Earmark.Options.make_options!(
  registered_processors: [
    Earmark.TagSpecificProcessors.new([
      {"a",          &Earmark.AstTools.merge_atts_in_node(&1, class: "underline")},
      {"h1",         &Earmark.AstTools.merge_atts_in_node(&1, class: "text-3xl py-4")},
      {"h2",         &Earmark.AstTools.merge_atts_in_node(&1, class: "text-2xl py-4")},
      {"h3",         &Earmark.AstTools.merge_atts_in_node(&1, class: "text-xl py-4")},
      {"p",          &Earmark.AstTools.merge_atts_in_node(&1, class: "text-md pb-4")},
      {"code",       &Earmark.AstTools.merge_atts_in_node(&1, class: "")},
      {"pre",
        &Earmark.AstTools.merge_atts_in_node(
          &1,
          class:
            "mb-4 p-1 py-4 overflow-x-scroll border-y"
        )},
      {"ol",         &Earmark.AstTools.merge_atts_in_node(&1, class: "list-decimal")},
      {"ul",         &Earmark.AstTools.merge_atts_in_node(&1, class: "list-disc pb-4")},
      {"blockquote",
        &Earmark.AstTools.merge_atts_in_node(
          &1,
          class: "pl-4 border-l-2 mb-4 border-purple-700"
        )}
    ])
  ]
)

フロントエンド

  • サーバーサイドの Phoenix テンプレート
  • Tailwind CSS(DaisyUI は不要)
  • 最小限の JavaScript:モバイルメニュー切替と Phoenix topbar
  • ダークモード対応

CI

GitHub Actions が push/PR 毎に実行されます:

mix compile --warnings-as-errors
mix format --check-formatted
mix credo --strict
mix test

Dependabot で依存関係を自動更新。Elixir の小さな依存グラフは npm よりも驚きが少ないです。


デプロイ

  • Dockerfile – 複数ステージ、Phoenix の例に基づく。

    • ビルド時にのみほとんどの依存関係を取得。
      Elixir 1.18.4
      Erlang 28.0.2
      Debian trixie-slim
      を使用。
  • Dokploy – 自己ホスト型 PaaS(Heroku ライク)。
    ビルド・デプロイ・ネットワーキング・ローリングアップデート・ロールバック・プレビュー構築などを自動化。

公開フロー

  1. PR を作成 → CI が走る。
  2. マージ → Dokploy がリポジトリを取得し、Docker イメージをビルド、レプリカを更新。
  3. デプロイ完了は約30秒で、キャッシュ済みレイヤーが活用されます。

Hetzner 上にホストしています。最近の価格上昇後もコストパフォーマンスは圧倒的です。必要なら他サービスへ移行も可能です。


小さなユーティリティ

RSS フィード

Controller (

JolaDevWeb.RssXML
)

defmodule JolaDevWeb.RssXML do
  use JolaDevWeb, :html
  embed_templates "rss_xml/*"

  def format_rfc822(%Date{} = date) do
    DateTime.new!(date, ~T[00:00:00], "Etc/UTC")
    |> format_rfc822()
  end

  def format_rfc822(%DateTime{} = datetime) do
    Calendar.strftime(datetime, "%a, %d %b %Y %H:%M:%S +0000")
  end
end

XML テンプレート (

rss_xml/rss.xml.eex
)

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>jola.dev</title>
    <link><%= url(~p"/") %></link>
    <description>Blog posts from jola.dev</description>
    <language>en-us</language>
    <lastBuildDate><%= JolaDevWeb.RssXML.format_rfc822(DateTime.utc_now()) %></lastBuildDate>
    <atom:link href="<%= url(~p"/rss.xml") %>" rel="self" type="application/rss+xml"/>
    <%= for post <- @posts do %>
      <item>
        <title><%= post.title %></title>
        <link><%= url(~p"/posts/#{post.id}") %></link>
        <description><![CDATA[<%= post.description %>]]></description>
        <content:encoded><![CDATA[<%= post.body %>]]></content:encoded>
        <pubDate><%= JolaDevWeb.RssXML.format_rfc822(post.date) %></pubDate>
        <guid isPermaLink="true"><%= url(~p"/posts/#{post.id}") %></guid>
        <author><%= post.author %></author>
      </item>
    <% end %>
  </channel>
</rss>

サイトマップ

Controller (

JolaDevWeb.SitemapController
)

defmodule JolaDevWeb.SitemapController do
  use JolaDevWeb, :controller

  def index(conn, _params) do
    sitemap = JolaDev.Sitemap.generate()
    conn
    |> put_resp_content_type("text/xml")
    |> send_resp(200, sitemap)
  end
end

ジェネレーター (

JolaDev.Sitemap
)

defmodule JolaDev.Sitemap do
  alias JolaDev.Blog
  @host "https://jola.dev"

  def generate do
    """
    <?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    #{generate_static_pages()}#{generate_tag_pages()}#{generate_blog_posts()}
    </urlset>
    """
  end

  defp generate_static_pages do
    pages = [
      %{loc: @host, changefreq: "monthly", priority: "1.0"},
      %{loc: "#{@host}/about", changefreq: "monthly", priority: "0.8"},
      %{loc: "#{@host}/projects", changefreq: "weekly", priority: "0.9"},
      %{loc: "#{@host}/talks", changefreq: "monthly", priority: "0.7"},
      %{loc: "#{@host}/posts", changefreq: "weekly", priority: "0.9"}
    ]

    Enum.map_join(pages, "\n", &url_entry/1)
  end

  defp generate_tag_pages do
    Blog.all_tags()
    |> Enum.map(fn tag ->
         %{loc: "#{@host}/posts/tag/#{tag}", changefreq: "weekly", priority: "0.6"}
       end)
    |> Enum.map_join("\n", &url_entry/1)
  end

  defp generate_blog_posts do
    Blog.all_posts()
    |> Enum.map(fn post ->
         %{
           loc: "#{@host}/posts/#{post.id}",
           lastmod: Date.to_iso8601(post.date),
           changefreq: "monthly",
           priority: "0.8"
         }
       end)
    |> Enum.map_join("\n", &url_entry/1)
  end

  defp url_entry(params) do
    """
      <url>
        <loc>#{params.loc}</loc>
        #{if params[:lastmod], do: "<lastmod>#{params.lastmod}</lastmod>", else: ""}
        <changefreq>#{params.changefreq}</changefreq>
        <priority>#{params.priority}</priority>
      </url>
    """
  end
end

ブログリダイレクトプラグ

blog.jola.dev
の旧 URL をマイグレーション後も機能させます。

defmodule JolaDevWeb.Plugs.BlogRedirect do
  import Plug.Conn

  def init(_), do: []

  def call(conn, _opts) do
    if conn.host == "blog.jola.dev" do
      ids   = JolaDev.Blog.ids()
      path  = strip_path(conn.request_path)

      path =
        if path in ids do
          "posts/" <> path
        else
          path
        end

      conn
      |> put_resp_header("location", "https://jola.dev/" <> path)
      |> send_resp(:moved_permanently, "")
      |> halt()
    else
      conn
    end
  end

  defp strip_path("/" <> rest), do: rest
  defp strip_path(path),        do: path
end

SEO タグ

レイアウトは

conn.assigns
を参照し、Open Graph / Twitter Card タグを挿入します。ブログ投稿は自動で次のタグを取得します:

<meta property="og:type" content={if @conn.assigns[:post], do: "article", else: "website"} />
<%= if post = @conn.assigns[:post] do %>
  <meta property="article:published_time" content={Date.to_iso8601(post.date)} />
  <meta property="article:author"          content="https://jola.dev/about" />
  <%= for tag <- post.tags do %>
    <meta property="article:tag" content={tag} />
  <% end %>
<% end %>

Twitter Card と説明タグも同様のパターンで設定します。

また

llms.txt
/
llms-full.txt
エンドポイントを追加し、AI システムがサイトを理解しやすいようにしています。これらはサイトマップロジックとほぼ同じです。


まとめ

このプロジェクトは意図的に軽量でありながら強力です:

  • Phoenix + NimblePublisher = 高速・保守性の高いブログ
  • GitHub Actions → Dokploy → Hetzner のゼロエフォートデプロイパイプライン
  • (オプション)bunny.net をフロントに置いた CDN

Elixir 開発者で個人サイトをホストしたい方は、ぜひこのスタックを検討してください。すぐに本番環境で動作し、最小限の摩擦で拡張可能です。


リソース

bunny.net の設定や Hetzner 上の Dokploy に関する詳細投稿を随時配信します。

同じ日のほかのニュース

一覧に戻る →

2026/03/27 5:53

**Deploytarot.com – デプロイメントのタロットカードリーディング** 「デプロイプロジェクトに対する洞察に満ちた指針を、個別化されたタロットカードリーディングで得ましょう。」

## 日本語訳: 記事では、ソフトウェアデリバリーを一連のタスク、リスク、およびステークホルダーの視点として可視化する比喩的な「カードデッキ」を紹介しています。各カードは、A/Bテスト、AI統合、DB移行、サーバーレス移行などの特定のタスクとアイコンおよび簡潔な説明をペアにします。リスクカードでは、「どの指標が重要かについてゼロコンセンサス」や「カードが見ている」という不確実性を列挙しています。役割カードは、CEO、CISO、CTO、清掃係、クライアント、コンサルタント、請負業者、DBA、データサイエンティスト、デザイナー、DevOpsエンジニア、エンジニアリングマネージャー、人事、インターン、ジュニア開発者、オフショア開発者、プロダクトマネージャー、プロジェクトマネージャー、QAエンジニア、受付係、営業、スクラムマスター、セキュリティエンジニア、シニアデベロッパー、SRE、ステークホルダー、テックリード、VP of Engineering など多岐にわたるステークホルダーの各役割がデプロイメント決定をどのように見ているかを示す一文の逸話を提供します。 物語は「アーケナはあなたのスプリント速度を気にしない」と強調し、代わりに「時折その終点で崖がある」という潜在的な落とし穴をハイライトすることに焦点を当てています。 デッキは各引きごとに新たにシャッフルされますが、「戻ってくると覚えている」と説明され、過去の洞察を保持する動的システムであることを示唆しています。 速度よりもリスク認識を前面に押し出すことで、このモデルは多様な役割間で明確なコミュニケーションと整合性を維持し、よりレジリエントなデリバリープロセスを育むことを奨励します。

2026/03/26 0:46

**2025年に多くの制御室がシー・フロムグリーン(海泡色)を採用した理由は?** | 要因 | なぜ重要だったか | |------|-----------------| | **エルゴノミクス研究** | その年に公開された調査では、海泡色が目の疲労を軽減し、長時間の監視作業中に集中力を向上させることが示されました。 | | **省エネルギー動向** | この色は白や黄よりも自然光を反射しやすく、24時間稼働する環境で人工照明の必要性を低減します。 | | **ブランドアイデンティティ** | いくつかのテック大手が「グリーンファースト」サステナビリティイニシアチブを開始し、その理念に合わせて制御室も再設計されました。 | | **心理的影響** | 緑は落ち着きとバランスに結びついており、オペレーターが重要なシステムを管理する際には不可欠な特性です。 | | **規制ガイドライン** | 新たな安全基準では、状態インジケータの可視性を高めるカラースキームが推奨されており、海泡色は赤警報と衝突せずにその要件を満たしました。 | まとめると、エルゴノミクス科学、省エネルギー政策、企業ブランディング、心理研究、そして更新された規制の融合が、2025年の制御室にとって実用的でストレスの少ない選択肢として海泡色を押し上げました。

## Japanese Translation: 記事は、核施設や工業サイトでよく見られる特徴的な海藻緑色を第二次世界大戦時のファーバー・ビレン(Faber Birren)の産業用カラー安全コードに遡ります。2017年夏、著者はオークリッジのX‑10グラファイト炉(「ファットマン」研究のためにロスアラモスへ輸送されたプルトニウムを生成した24フィート四方のブロック)を訪れ、壁と制御パネルでビレンが推奨するライト/ミディアムグリーンが一貫して使用されていることに注目しました。ビレン(1919–1996)は1944年に国立安全協議会によって承認されたカラーコードを開発し、1948年までに世界中で採用されました。このコードは次のような色から構成されています: - **Fire Red** – 火災防止、緊急停止、可燃液 - **Solar Yellow** – 注意、物理的危険 - **Alert Orange** – 危険機械部品 - **Safety Green** – 救急装置、出口、洗眼ステーション - **Caution Blue** – 非安全通知または故障表示 - **Light Green** – 視覚疲労を軽減する壁色 同じスキームがハンフォードのB‑レイザー制御室にも見られます:下部壁にミディアムグリーン、機械にはミディアムグレー、火災防止にファイヤーレッド、低照度エリアにベージュ、床はライトです。ビレンは、このような機能的カラー使用が明るさを制御し、事故を減らし、メンテナンス基準を向上させ、労働士気を高めると主張しました。 ドイツの「ケルン橋緑(Cologne Bridge Green)」は、橋という工業用途で開発された海藻緑色の別例です。著者はまた、古い自動車部品リストに触発されて「Parts List」というフォントをデザインし、オイル交換待合室の雰囲気を呼び起こすことを目的としています。このフォントは彼女のウェブサイトで入手可能です。彼女はこれら歴史的安全色が今日どのように適用できるかを引き続き探求し、海藻緑の使用を現代施設に拡大する可能性や、「Parts List」フォントを産業美学を捉えるデザインツールとして推進する計画です。

2026/03/24 7:06

**クラウドフレアのGen 13サーバー:** コア数とキャッシュ容量を入れ替えて、パフォーマンスを2倍にしています。

## Japanese Translation: Cloudflare は、AMD EPYC 5th‑Gen「Turin」CPU と Rust ベースのリクエストハンドラ FL2 を搭載した新しい Gen 13 エッジサーバーをデプロイ完了しました。Turin はコア数が倍増(最大 192 コア、Gen 12 の 96 コアに対して)し、Zen 5 により IPC が向上、1 コアあたりの電力消費が約32%削減され、DDR5‑6400 メモリバンド幅をサポートします。チップは全コアで 384 MB の L3 キャッシュしか共有せず、1 コアあたり約 2 MB(Gen 12 の 3D V‑Cache を搭載した場合は 12 MB/コア)です。 元の FL1 ハンドラ(NGINX/LuaJIT)は Turin 上で L3 ミス率が高く、ミスサイクルが約350回に対しヒットは約50回と遅延が増大し、スループット向上にもかかわらずレイテンシが悪化しました。プリフェッチャーの調整、ワーカースケーリング、および NUMA コアアフィニティの最適化を行っても、スループットはわずか 5 % 未満にしか改善されませんでした。FL2 の軽量メモリアクセスパターンはこのボトルネックを解消し、Turin 上で Gen 12 に比べ約 50 % 低いレイテンシ、62 % 高いスループット、および FL1 より CPU あたり 2 倍のリクエスト数を実現します。 Gen 13 が Cloudflare のグローバルエッジネットワーク全体に完全展開されたことで、同社はサーバー数を減らしつつより多くのトラフィックを処理できるようになり、SLA に縛られたレイテンシを維持したままで最大 2 倍のスループットを達成します。これにより、パフォーマンス・ペー・ワットが約 50 % 改善され、ラック単位でのスループットは約 60 % 向上します。結果として CDN とクラウド顧客双方の運用コスト削減とカーボンインパクト低減に寄与します。