HN 日本語サマリー

← 一覧へ戻る
Web開発

Waldi: 書くための、そして読まれるための静かな場所

Waldi: A quiet place to write, and to be read (github.com)

45 pointsby waaldev21 コメント

要約

Waldiは、かつてのブログ文化を再現することを目指した、静かでプライベートな執筆・読書体験を提供するマルチテナント型ブログプラットフォームです。公開された「いいね」数やコメントはなく、代わりにプライベートな手紙でのやり取りを重視し、新しい書き手が沈黙の中に投稿することのないよう、アルゴリズムによる評価前に一定数の読者を保証します。Go言語とPostgreSQLで構築されており、シンプルなアーキテクチャとミニマルな機能セットが特徴です。

全文翻訳

Waldi 静かに書き、静かに読まれる場所。 waldi.blog で公開中 — 誰でも読むことができます。執筆は、創設メンバーが場所を形作る間は招待制です。 Waldi は、ブログを懐かしく思う人々のためのマルチテナント型ブログプラットフォームです。単一のGoバイナリとPostgreSQL、サーバーレンダリングされたHTML/テンプレートページ、そして単一のTiptapエディターアイランドで構成されています。各ライターには username.<base-domain> (または独自のドメイン) が与えられます。同じバイナリが、リクエストのHostヘッダーをランタイムで読み取ることで、すべてのブログを提供するため、テナントごとのデプロイ管理は不要です。 その中心となるループは、公開し、百人の見知らぬ人がそれを読み、フォローやプライベートな手紙が返ってくることです。公開されたいいね数、コメント、リーダーボードはありません。返信はプライベートな手紙であり、あらゆる投稿は、アルゴリズムが判断する前に、保証された最低限の読者数を確保します。そのため、新しい書き手が沈黙の中に投稿することはありません。 私が2016年に自身のブログdissolveを始めた理由 — 終わろうとしているパーティーに遅れて参加したのです。ペルシャ語のブログ圏は、初期のウェブで最大かつ最も活気のある一角であり、そこで最も親しい友人たちの何人かを見つけました。その後、それが生きていたプラットフォームは劣化または消滅し、時には10年分の人々の執筆を道連れにし、誰もがTelegram、Instagram、Twitterに散り散りになりました — これらは執筆に、そのプライベートで急がない空間を二度と与えませんでした。 Waldi は、2つの確信に基づいて構築された、その歴史への私の答えです。第一に、ブログが死んだ理由は執筆ではなかった — それは配信でした:新しいブログは沈黙の中に公開され、ソーシャルフィードはノイズに聴衆を保証しました。だからWaldiは代わりに聴衆を保証します:あらゆる投稿は、それが判断される前に見知らぬ人によって読まれます。第二に、読者の1日は完了可能であるべきです — フォローしているライターからの数件の投稿、見知らぬ人からの1つの驚き、そしてそれだけです。それはペルシャ語で最初にローンチされました。なぜなら、私がそれに恩義を感じているブログ圏だからです — しかし、それは構築上バイリンガルであり(Multi-language / i18nを参照)、英語は翻訳対象ではなく、第一級市民です。 製品のルール — コメントの代わりに手紙、どこにも公開された指標なし、決して構築されない機能の厳格なリスト — はROADMAP.mdとdocs/DESIGN.mdに文書化されています。 アーキテクチャ リクエストフロー / マルチテナンシー すべてリクエストは internal/web/server.go: ServeHTTP を通ります。これはまず、Hostヘッダーがブログにマッピングされているかを確認します (internal/web/host.go: isBlogHost): 安価なサブドメインマッチ: username.<base-domain> (開発中は .localhost / .waldi.test)。そうでなければ、Postgresに対する検証済みのカスタムドメインルックアップで、リクエストごとのDBヒットを避けるためにインプロセスでキャッシュされます (customDomainCache、正/負のTTL付き)。ハンドラーは、解決されたホストがブログかメインアプリドメインかによって分岐します (internal/web/blog.go, handlers_read.go)。セッションクッキーは cookieDomain でスコープされているため、メインドメインでのログインは username.<base-domain> サブドメインに引き継がれますが、カスタムドメインには引き継がれません。 レイヤリング internal/web — HTTPレイヤー: ルーティング (server.go)、ハンドラー (handlers_*.go、機能領域ごとに1つのファイル)、テンプレートデータ構造とレンダリング (render.go)、セッション/ロケール/ホスト解決。 internal/store — すべてのSQL、*pgxpool.Pool をラップします (store.go)。エンティティごとに1つのファイル (users.go, posts.go, follows.go, letters.go, events.go, invitations.go, settings.go, stats.go)。ハンドラーはリポジトリ/サービスインターフェースレイヤーなしで、ストアメソッドを直接呼び出します。 internal/post — Tiptapドキュメントモデルの純粋なドメインロジック: JSONスキーマ検証、サニタイズされたHTMLレンダリング、画像URLの修正、埋め込み、脚注。DBやHTTPなし — 最も集中的に単体テストされたパッケージです。 internal/jobs — 毎日のバックグラウンドジョブ (ダイジェストメール、ワイルドカード選択)、キュー/ワーカープロセスではなく、CLIサブコマンドとして呼び出されます。 internal/mail — 小さなMailerインターフェースの背後にあるSMTP送信。SMTPが設定されていない場合 (開発時) は NoopMailer。 internal/storage — 画像アップロードストレージ: ローカルディスクまたはS3互換 (本番ではCloudflare R2)。アップロード時にWebPへのリサイズを行います。 internal/cdn — 公開時のCloudflareキャッシュパージ (オプション; ゾーンID/トークンが必要)。 internal/i18n — Goとテンプレートの両方で使用される翻訳カタログ。 internal/telegrambot — 管理ボット (下記参照)。 internal/importcommon — すべてのインポーターが使用する共有HTMLからドキュメントへの変換。 internal/importblogir — blog.ir インポーター。他のプラットフォームのサポートを追加するための参照実装です (CONTRIBUTING.md参照)。 internal/migrate — migrations/ 内の番号付き、追記形式のSQLファイルを実行します。 cmd/waldi — CLIエントリーポイント。main.go でサブコマンドがディスパッチされます (serve, migrate, digest, reader-digest, wildcard, weekly-stats, invite, user, import, posts)。 フロントエンド web/templates/*.html — サーバーレンダリングされたページ。template.ParseFS を介してロードされます (開発時はディスク、デプロイ時は embed.FS)。データは、ページタイプごとに1つのオプションサブビューを持つ単一の web.PageData 構造体です。 web/static/css/main.css — 手書き、フレームワークなし。 web/editor/ — Tiptapアイランド (TypeScript)。esbuild によって web/static/js/editor.js に直接バンドルされます。エディターのドキュメントスキーマは、サーバーサイドの internal/post/doc.go で強制されます — ライターが無効なドキュメントを作成するのを止めるのは、クライアントではなくサーバーです。 データモデル Postgres、migrations/00N_*.sql。 コアテーブル: users, sessions, posts (doc jsonb は Tiptap の真実の源、html は公開時に事前にレンダリング/サニタイズされ、そのまま提供されます)、follows (follow-attribution 用の source_post_id 付き)、letters, impressions/readings (追記形式のイベントロギング)、wildcards, digests、カスタムドメイン、invitations、admin settings。 インフラストラクチャ 懸念事項 | 使用されているもの データベース | PostgreSQL オブジェクトストレージ | Cloudflare R2 (S3互換; 任意のS3互換ストアで動作) — 画像はアップロード時にリサイズされ、WebPに変換されます エッジキャッシュ | Cloudflare — 匿名HTMLページは Cache-Control: public で提供され、エッジでキャッシュされ、公開/設定/ドメイン変更時に Cloudflare API を介してパージされます TLS + カスタムドメイン | Caddy — *.<base-domain> 用のワイルドカード証明書 (Cloudflare DNSチャレンジ経由)、ドメインごとのオンデマンド Let's Encrypt 証明書 (検証済みユーザーカスタムドメイン用、内部 /internal/caddy-ask エンドポイントでゲートされています) メール | 任意のSMTPプロバイダー — トランザクショナル (検証、パスワードリセット) とバルクダイジェストメールで別々の送信元アドレスを使用します デプロイ | 単一VM上のDocker Compose (参照ターゲット: DigitalOcean Droplet)。マネージドプラットフォームデプロイ (例: Heroku) はロードマップにありますが、アプリが独自のTLS/Caddyレイヤーを所有しているため、まだサポートされていません。 CI/CD | GitHub Actions — lint → test → build をすべてのプッシュ/PRで実行し、main へのマージ時に SSH 経由でデプロイします。 バックアップ | 毎日のcronでPostgresをダンプし、gzip圧縮されたアーカイブをR2にアップロードします。 管理 | Web管理パネルの代わりにTelegramボット (下記参照) DEPL0Y.md に完全な本番環境セットアップが記載されています。 セルフホスティング はい、自分で実行できます — ライセンスはMITで、DEPLOY.md には Cloudflare/R2/Caddy を前提とする部分やそれらを置き換える方法を含む、完全な本番環境セットアップが文書化されています。 2つの正直な注意点: waldi.blog は公式のコミュニティであり、私が運営またはサポートする唯一のインスタンスです。そのため、セルフホストされたインスタンスはそれ自身の静かな島となります — 保証された読者ループは、あなたのインスタンスが持つ読者と同じくらい現実的です。そして、ロードマップは waldi.blog のニーズを最優先します。セルフホスティングユーザーからのイシューは歓迎されますが、ホスト専用の懸念事項の修正が優先されます。 マルチ言語 / i18n 翻訳文字列はフラットなJSONカタログに格納され、バイナリに埋め込まれます (internal/i18n/locales/en.json, fa.json)。ユーザーインターフェースのすべての文字列は T(lang, key, args...) (internal/i18n/catalog.go) を介してルックアップされ、テンプレートは言語間で共有されます — 言語ごとのテンプレートの重複はなく、web/templates/*.html のすべての文字列は PageData.T を通してルーティングされます。 リクエストごとの言語解決の優先順位: ログイン中のユーザーの保存されたロケール → ロケールクッキー → CF-IPCountry ヘッダー (i18n.LangFromCountry 経由) → デフォルト (fa)。ユーザーはアカウント設定から言語を変更でき、クッキーが更新され、アカウントに永続化され、キャッシュされたページがパージされます。新しい言語を追加するには: internal/i18n/locales/<code>.json に、en.json のすべてのキーをミラーリングして追加します。