SNSであるSTGYだが、特定の記事を外部に公開し、ログインせずに閲覧できるようにすれば、ブログエンジンとしても成立する。SNSでありながらブログエンジンであるというのは奇妙な建付けだが、両立させるための機能要件と非機能要件について検討した。既に実装も終わっていてデモサイトで利用できるようになっている(初期化したのでアカウント再登録が必要)。

デモ
STGY上のお知らせ記事をいくつか外部公開しておいたので、ご覧いただきたい。STGYにログインせずとも、アカウント登録をしていなくても、記事が読めるようになっている。
実際にユーザ登録してぜひ使ってみてほしいのだが、我ながらかなり使いやすい。サイドバイサイドプレビュー付きのエディタで構造を確認しながら編集できるし、画像のアップロードやレイアウト調整も簡単だ。そして、SNS上での限定公開とブログとしての外部公開が本当にシームレスに行える。本番稼働したらこのブログもそちらに移行したい。

数式やプログラムコードを含む記事もこんなに綺麗に表示される。


SNSの記事の外部公開
前回の記事で述べたが、STGYではMarkdownで記事を投稿でき、その機能性には相当な拘りを持って作り込んでいる。構造化文書でありつつ、サイドバイサイドのリアルタイムプレビューで視覚的レイアウトを確認しながら編集作業ができる。ヘッダや段落やリストが簡単に書け、画像の埋め込みと最適化が簡単にでき、画像の配置も柔軟に制御でき、テーブルや数式やルビやシンタックスハイライトなどの高度な表現方法もサポートしている。手前味噌だが、Web上で構造化文書を編集する機能としては最善に近いものに仕上がっている。
SNSのユーザとしても、よく書けた記事はWWW全体に公開したいと考えるだろう。その際に、記事データを別のブログサービス等にコピーして、レイアウトなどを整える作業をするのは、二度手間だ。Markdownの可搬性が高いとはいえ、面倒なものは面倒だし、知的水準の高い人にとっては屈辱的ですらある。STGYの高効率の編集作業に慣れた後ではなおさらだろう。また、複数のサービスでデータを公開すると、多重管理になるのが問題だ。元記事を修正した場合、同じ修正を別のサービスでもやる必要があり、そんな運用は早晩破綻する。
STGY上で自分が書いた任意の記事を選択し、それが外部公開できれば、上述の作業が5秒で完了する。これができると、外部公開することを前提とする記事を書くために、クリエータやインフルエンサと呼ばれる人々がユーザになってくれるかもしれない。SNS内部に記事を載せた状態をステージングやベータ版公開という位置づけにして、それで記事の品質や第三者の反応を確認してから、広くWWW上に公開するという手順を踏んでもいい。
SNSをブログエンジンとして使えることになるわけだが、それには利点と欠点がある。想定される利点を以下に挙げよう。
- ブログエンジンとして使いたい新規ユーザが獲得できる。
- 良質の記事を複製して外部公開している既存ユーザの手間が大幅に省ける。
- 外部公開した記事にSTGYへの流入経路を設けて、ブランディングと新規ユーザ獲得ができる。
- 外部公開記事に広告を張るなどのビジネスモデルも可能になる。
- ブログエンジンとしてだけでも成立するので、クリティカルマスを達成するまでの過疎状態に耐えられる。
想定される欠点を以下に挙げよう。
- SNSとしてのコンセプトがぶれ、ユーザにとって、どんなサービスなのかが曖昧になる。
- ブログエンジンとしてだけ使うユーザは返信等の内部交流をしないので、コミュニケーションが希薄になる可能性がある。
- 内部向けの情報と外部向けの情報が混在するので、人間の読者もAIユーザも混乱する可能性がある。
- 公開状態を管理するテーブルとその操作が必要で、情報設計とUIが若干複雑化する。
- 外部公開すると負荷の制御が難しくなる。
欠点の上3つは杞憂かもしれず、実際にユーザがどう思ってどう反応するかは、運用してみないと分からない。投稿の外部公開設定があるFacebookでSNSとしてのコンセプトが維持できていることから考えると、STGYでも深刻な問題にはならないと考えられる。欠点の下2つは技術的課題なので、私が頑張れば解決できるし、方法も明確になっている。ゆえに、総合的に考えて、外部公開機能を実装する価値は高いと判断した。
公開機能のUIとテーブル
外部公開機能のUIは、率直なものでよいだろう。投稿一覧画面または投稿詳細画面にある投稿カードに「⋯」というアイコンをつけ、「Copy link to this post」「Copy mention Markdown」などとともに、「Configure external publication」というのを置く。外部公開操作ができるのは、記事の投稿者または管理者ユーザだけである。

外部公開を選択すると、確認ダイアログが表示される。そこでは、外部公開をするか否かのフラグと、公開日時の設定ができるようになっている。公開日時のデフォルトは現在時刻である。ここに未来の時刻を指定すれば、公開予約機能になる。

外部公開された投稿には「published」マークを付けて判別できるようにする。そのマークを押すと外部公開のURLに遷移する。外部公開された投稿でも、内部での機能は他の投稿と一切変わらず。普通に記事の表示ができ、イイネや返信ができる。外部公開を取りやめるには「Configure external publication」を選んでチェックを外せばよい。
DBのテーブルの設計も率直だ。投稿IDを主キーにしているpostsテーブルには、既に公開日時が属性として存在している。ユーザIDと投稿日時にはセカンダリインデックスが貼られていて、ユーザ毎かつ現在時刻以前が公開時刻である投稿の一覧を時系列順に効率的に取得できるようになっている。公開された投稿だけを扱う部分インデックスなので、実運用上の空間効率が高い。
CREATE TABLE posts ( id BIGINT PRIMARY KEY, owned_by BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, published_at TIMESTAMPTZ, ... ); CREATE INDEX idx_posts_public_owned_by_published_at ON posts (owned_by, published_at) WHERE published_at IS NOT NULL;
投稿の作成日時と公開日時は別概念だ。外部からの視点では、公開日時が記事の作成日時であるかのように見える。ブログは公開日時の降順で並べられるのが基本だ。published_atがNULLであれば、非公開ということになる。
ユーザ毎に、サイトタイトルやデザインテーマなどの外部公開の設定ができるようにする。デザインを投稿毎に設定できるようにすることも考えたが、たぶんほとんどのユーザにとっては面倒臭いだろうから、ユーザ毎の設定にした。
CREATE TABLE user_pub_configs ( user_id BIGINT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, site_name VARCHAR(50) NOT NULL DEFAULT "", subtitle VARCHAR(50) NOT NULL DEFAULT "", author VARCHAR(50) NOT NULL DEFAULT "", introduction VARCHAR(1000) NOT NULL DEFAULT "", design_theme VARCHAR(50) NOT NULL DEFAULT "", show_service_header BOOLEAN NOT NULL DEFAULT TRUE, show_site_name BOOLEAN NOT NULL DEFAULT TRUE, show_pagenation BOOLEAN NOT NULL DEFAULT TRUE, show_side_profile BOOLEAN NOT NULL DEFAULT TRUE, show_side_recent BOOLEAN NOT NULL DEFAULT TRUE );
site_nameはサイト名、subtitleは副題、authorは著者名、introductionはサイト紹介文、design_themeはデザインテーマの名前、show_service_headerはSTGYのページヘッダを表示するか、show_site_nameはサイト名のバナーを表示するか、show_pagenationはページめくりのリンクを表示するか、show_side_profileはサイドバーにプロファイルを乗せるか、show_side_recentはサイドバーに最新記事一覧を表示するかを指定する。SNS内部のユーザ名と外部向けの著者名は別であり、SNS内部の自己紹介文と外部向けのサイト紹介文も別である。SNS内で仲間とコミュニケーションするためのペルソナとWWW向けに公開するペルソナは異なるからだ。
フロントエンドの「/pub-settings」というページで外部公開の設定ができるようにする。user_pub_configsに該当のユーザ用のレコードがあればその内容をフォームの初期値にし、なければデフォルト値である空文字列などを初期値にする。この設定の存在を知らなくても公開操作が行えるが重要だ。極小の学習コストでブログが始められる。ことになる。

全ての投稿をデフォルトで外部公開する機能がFacebookにはあるが、ここではつけない。STGYでは返信も含む全ての投稿がMarkdownの記事であるため、返信や個人的なつぶやきが自動的に外部公開されるのは望ましくない。内部公開している情報も登録ユーザなら誰でも見られるので、外部公開も内部公開も情報の秘匿性の観点では大して変わらない。しかし、外部公開すると検索エンジンに登録され、他のSNSでもシェアされ、潜在的な露出範囲は桁違いになる。人の口に戸は立てられないが、給湯室で喋るのと街中で叫ぶのは違う。秘匿性とは別に、情報管理の観点でも、公開範囲は意識的に設定してもらった方がよい。外部公開すべき整理かつ検証された記事と、外部公開しない未整理かつ未検証の記事を明確に分けることで、後者の「生」の情報をより気軽に内部に書き込めるようになる。
公開記事のレンダリング
公開された記事は、「/pub/12344567890」のようなURLでアクセスできるようにする。つまり、「/pub/」の後ろに投稿IDをつけたものである。実装は単純だ。投稿IDでデータベースを調べ、それが公開状態であれば、内容のMarkdownをHTMLに変換して送信するだけだ。SNS内部での投稿詳細画面と全く同じロジックだが、ログイン処理の代わりに公開判定をするところと、SNS内部用のボイラープレートの代わりに外部公開専用のボイラープレートを使うところが異なる。単純化すると、以下の構造のHTMLがレンダリングされる。
<html lang="mul"> <head lang="ja" prefix="og: http://ogp.me/ns#"> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="generator" content="STGY"> <meta name="author" content="田中一郎"> <meta name="robots" content="all"> <meta name="twitter:card" content="summary_large_image"> <meta name="description" content="今日は鎌倉にサイクリングに言った。とっても天気が良かったので100kmも走って…"> <meta property="og:url" content="https://stgy.jp/pub/12344567890"> <meta property="og:title" content="鎌倉サイクリング"> <meta property="og:site_name" content="自転車日記"> <meta property="og:type" content="website"> <meta property="og:description" content="今日は鎌倉にサイクリングに言った。とっても天気が良かったので100kmも走って…"> <meta property="og:locale" content="ja_JP"> <meta property="og:image" content="https://s3.stgy.jp/stgy-images/12345/98765/45678.jpg"/> <title>自転車日記: 鎌倉サイクリング</title> <link rel="stylesheet" href="/data/pubished.css"/> <script src="/data/pub-article.js"></script> </head> <body lang="en"> <div class="main-container pub-theme-default"> <header> <aside class="headerColumn"> <a href="https://stgy.jp/">STGY</a> </aside> </header> <main> <h1 class="site-name">自転車日記</h1> <div class="subtitle">ゆるーく、またーり</div> <aside class="postMeta"> <span class="publishedAt">2025-09-11 21:22:23</span> </aside> <article lang="ja" class="postBody"> <h1>鎌倉サイクリング</h1> <p>今日は鎌倉にサイクリングに言った。とっても天気が良かったので100kmも走ってしまった。</p> </article> </main> <aside> <section class="sidebarColumn sidebarProfile"> <article lang="ja"> <h2><a href="/sites/12345678">自転車日記</a></h2> <div class="author">田中一郎</div> <div class="introduction">各地にサイクリングに行った際の記録です。</div> </article> </section> <section class="sidebarColumn sidebarRecent"> <h2>Recent articles</h2> <article lang="ja"> <h2><a href="https://stgy.jp/pub/12344567890">世田谷サイクリング</a></h2> </article> <article lang="ja"> <h2><a href="https://stgy.jp/pub/12344567891">吉祥寺サイクリング</a></h2> </article> </section> </aside> </div> </body> </html>
スキーマ上では、投稿にタイトルはないが、本文のMarkdownの中にH1の要素があれば、その最初のものをタイトルとして扱う。H1が一つもない場合には、H2を探す。それもない場合は、投稿日時を使って「POST@2025-09-23」などという文字列を生成する。HTMLのtitle要素には、設定のサイト名と記事のタイトルを連結して、"{siteName}: {postTitle}" の形式で値を入れる。
外部公開する場合、TwitterやFacebookなどでシェアしてもらいたいだろう。その際に格好良く表示されるためには、OGP(Open Graph Protocol)の仕様に則ったメタデータを書く必要がある。スニペットは内部SNSでも作っていて、アイキャッチ用の画像を指定する機能も既にMarkdownの中にあるので、そのロジックをそのまま流用する。ページのdescription情報は、MarkdownのASTを走査して、タイトルを取り除いたテキストをそのまま並べている。ruby要素の振り仮名はdescriptionからは除去する。Google等の検索エンジンは読みによる検索もシノニムで対応できると期待できるからだ。なお、descriptionを直接指定する拡張構文を作ろうとも思ったが、煩雑になりすぎるので止めた。一般的な文書の作法に則るなら、トピックセンテンスは文書の冒頭に来るはずで、同じものをわざわざ二重に管理する手間を取る人はそんなに居ないだろう。
SEO(Search Engine Optimization)の要請から、外部公開用のデータは素のHTMLとして転送しなければならない。OGPスニペットの内容も、そのHTMLに含める必要がある。つまり、CSR(クライアントサイドレンダリング)のJavaScriptでDOMを生成することはできない。Next.jsの場合、SSR(サーバサイドレンダリング)のページを作ることでそれをなす。STGYのフロントエンドの各種コンポーネントはSSRでも動くように実装しているので、これは簡単だ。実装が一段落したら、curl等で外部公開URLにアクセスしてみて、本当にSSRになっているかを確認するべきだ。外部公開URLのレスポンスに記事の全文と各種メタデータが含まれていることが要件である。
各投稿にはロケール属性が付いているが、各投稿を表現するarticle要素のlang属性として指定される。各ユーザにもロケール属性がついているが、それはサイトプロファイルのarticle要素のlang属性として指定される。内部用のユーザプロファイルのロケールと外部公開のサイトプロファイルのロケールを別途設定できる方が柔軟なのだが、既に複雑なロケールの概念がこれ以上複雑化するのは避けたいので、内部ロケールを流用することにした。
記事の中でSTGY内部の別の記事にリンクを貼ると、「/posts/」で始まる相対URLを置くことになるが、これをそのまま外部公開すると使い勝手が悪いので、「/pub/」で始まる外部公開URLに自動的に置き換える。同様に、「/users/」で始まるユーザプロファイルへのリンクは、「/sites/」で始まる外部公開のサイトプロファイルへのリンクに置き換える。画像などのメディアデータは元々全て外部公開かつ絶対URLなので、特に置き換えは必要ない。
aside要素の中にサイトプロファイルと最新記事一覧を入れている。これはモバイル端末などの幅が狭い画面では記事の下のフッタとして表示されるが、幅が一定(1100px)以上広い画面では、サイドバーとして表示される。これは定番の情報設計だ。記事毎のページでは主たる情報はその記事の方なので、その他の雑多な情報は最後でいい。画面の幅に余裕があるときだけ、その雑多な情報もファーストビューで確認できる位置に移動する。ページ冒頭のサイト名やフッタのサイトプロファイルをクリックすると、後述の公開記事の一覧ページに遷移するようになっている。
最新記事の一覧としては、スニペットを縮小したものを5件表示することにしている。タイトルと本文だけを表示することも考えたが、アイキャッチ画像付きのスニペットをそのまま縮小表示している。こうすることで、別途アイキャッチ画像を生成しなくても、弁別性が高く、かつ可読性をギリギリ維持した一覧表示になっている。半構造であるMarkdownの記事であって、必ずしもタイトルが存在しないので、この方式の方が統一感のある表示になる。実は、サイトプロファイルの紹介文はMarkdown形式であり、最新記事一覧と同様に、Markdownの構造を保ったままスニペットが作られて表示される。アイキャッチ画像も付けられるので、それをアバターとして表示させることができる。
記事の内容をmain要素に入れ、それ以外の内容をaside要素に入れるのは、SEOとアクセシビリティの観点で重要だ。Google等の検索エンジンはmainの中身のテキスト内のフレーズの相対的スコアをasideのそれよりも上げることが期待できる。検索結果のスニペットを表示する際にも、mainの中身を優先的に使うことが期待できる。AIが記事を処理する際にも、mainの内容を重視することが期待できる。スクリーンリーダでは、mainの要素にジャンプすることで、余計なボイラープレートの読み上げを省略できる。スマートウォッチやスマートグラスなどの表示枠が限られたデバイスが要約を生成する際にも、main要素を抽出することが容易になる。
HTMLとして書き出す際に、mainをasideよりも先に置くのも重要だ。asideのデータ量は意外に多いので、もしmainが後だと、遅いネットワーク環境では、記事の中身の描画が待たされることになる。冒頭にmainが来てそれを上から表示していくならば、表示すべきデータが無い時間を極小にできる。日本に住んでいると動画でもない限り通信の遅さに苛つくことはないが、世界には通信が遅い地域がたくさんある。
STGYのセッションを持っている場合、サービスヘッダにその記事のイイネ数と返信数が表示される。外部公開記事を中心に閲覧するユーザであっても、各々の記事にイイネや返信があることに気づくことになる。サービスヘッダ左側のボタンを押せば一撃で該当記事の内部の詳細表示画面に飛び、そこで自分もイイネや返信ができる。この機能は、内部のコミュニケーションが希薄化する懸念への対策である。

公開記事の一覧
「/sites/2234567890」のようなURLでアクセスできるようにする。つまり「/sites/」の後ろにユーザIDをつけたものである。内部的にはユーザIDで管理されているのだが、外部からはユーザIDではなくサイトIDとして認識されるという建付けになっている。サイトという別の粒度の層を作って、各ユーザが複数のサイトを持てる構造にすることも考えたのだが、あまりに複雑になるので止めた。
一覧ページの冒頭には、サイトプロファイルの紹介文のMarkdownが投稿記事と同じレンダリング方式で表示される。よって、挨拶文や紹介文だけではなく、画像を貼ったり、人気記事のリンク集を置いたりすることもできる。任意のHTMLを埋め込む危険なカスタマイズを許さなくても、Markdownのおかげで情報学的な柔軟性はかなり高くなっている。
デフォルトでは最新の投稿20件のスニペットを降順で表示する。「?page=2」とかやってページネーションできるようにもする。この実装はSNS内部のユーザ詳細画面と似ている。当然、外部用のボイラープレートを用いる。単純化すると、以下の構造のHTMLがレンダリングされる。
<html lang="mul"> <head lang="ja" prefix="og: http://ogp.me/ns#"> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="generator" content="STGY"> <meta name="author" content="田中一郎"> <meta name="robots" content="all"> <meta name="twitter:card" content="summary_large_image"> <meta name="description" content="各地にサイクリングに行った際の記録です。"> <meta property="og:url" content="https://stgy.jp/pub/12344567890"> <meta property="og:title" content="自転車日記"> <meta property="og:site_name" content="自転車日記"> <meta property="og:type" content="website"> <meta property="og:description" content="各地にサイクリングに行った際の記録です。"> <meta property="og:locale" content="ja_JP"> <title>自転車日記</title> <link rel="stylesheet" href="/data/pubished.css"/> <script src="/data/pub-article.js"></script> </head> <body lang="en"> <div class="main-container pub-theme-default"> <header> <aside class="headerColumn"> <a href="https://stgy.jp/">STGY</a> </aside> </header> <main> <h1 class="site-name">自転車日記</h1> <div class="subtitle">ゆるーく、またーり</div> <article lang="ja"> <p>各地にサイクリングに行った際の記録です。</p> </article> <article lang="ja"> <h1><a href="https://stgy.jp/pub/12344567890">世田谷サイクリング</a></h1> <p>今日は世田谷にサイクリングに行った。</p> </article> <article lang="ja"> <h1><a href="https://stgy.jp/pub/12344567890">吉祥寺サイクリング</a></h1> <p>今日は吉祥寺にサイクリングに行った。</p> </article> <nav> <a href="?page=2">NEXT</a> </nav> </main> </div> </body> </html>
STGYの全てのページは多言語対応前提であるため、html要素のlang属性には、BCP47で規定された多言語を表す「mul」を指定している。それでいて、body要素のlang属性には、UI言語である「en」を指定している。ローカリゼーションに対応したら、そこには「ja」等も設定され得る。既に述べたように、ページ内に含まれるユーザプロファイルや各記事を示すarticle要素のlang属性には、そのユーザまたは投稿につけられたロケールが設定され、bodyのlang指定を上書きする形になる。そうすると、残る言語指定空白地帯は、head要素である。head要素はtitle要素やmeta要素を含み、検索エンジンはそれらを見るし、OGPで利用されるメタデータもmeta要素で記述される。記事一覧ページのメタデータの記述言語はユーザのロケールに一致すべきだし、記事詳細ページのメタデータの記述言語は投稿のロケールに一致すべきだ。しかし、残念ながら、Next.jsでは、head要素に属性を指定できない。これは仕様の欠陥と言わざるを得ない。HTMLとして言語が指定されていない以上、それを処理する検索エンジン等やシェア先のSNS等は、ヒューリスティックで言語判定をすることになる。検索エンジンは記事全体から適当に言語を予測するだろうし、OGPの処理系はog:localeを読むだろう。しかし、スクリーンリーダがtitle要素やmeta要素の内容を読み上げる場合には、言語判定に失敗してうまくいかない可能性がある。これは困るので、ページロード時のJavaScript処理で動的にhead要素のlang属性を記事やユーザのロケールと合わせて設定するようにした。VoiceOver等のブラウザのDOMベースで動く機能であれば、直近のlang属性を動的に読むので、うまくいくだろう。JavaScriptを実行しない処理系では対応できないが、Next.jsの改善を待つしかない。
記事一覧の機能には、細かい工夫がある。典型的なユースケースでは、ユーザは、記事一覧にあるスニペットを見ながら興味のある記事を選んでクリックして、その記事の全文を読む。記事を読んだら、ブラウザの「戻る」機能を使って記事一覧に戻ってきて、別の記事を読む。このブラウザバック操作をした際に、ページの冒頭にスクロールが戻ってしまうと、鬱陶しい。再びスクロールしながら、一覧の中の記事のどこまで目を通していたかを見つけなければいけないからだ。全部の記事を読むと、表示数Nに対してスクロール操作の時間計算量がO(N^2)になってしまう。一覧の中の記事数は10とか20とかなので死ぬほど大変というわけではないが、苛つくのは間違いない。そこで、ブラウザバックした際には、さっき閲覧していたページが画面の上から40%の位置に来るように自動スクロールする機能をつけた。そうすれば、今読んだ記事がすぐ見つけられるし、その次のスニペットから探索を始めるべきことがすぐ分かる。ブラウザバックの自動スクロール機能はSTGY内部のユーザ一覧や投稿一覧でも当然実装している。これが有ると無いとでは多読の際の心理的負荷が全然違う。

記事一覧にはタブ方式で表示方法を変える機能もある。デフォルトの「Rich」モードは、最新10件の構造化スニペットを表示する。「Plain」モードにすると、最新100件のプレーンスニペットを表示する。プレーンスニペットとは、タイトルと著者名とそれ以外の文字列を1行で表現したものだ。それなら100件表示してもスクロール量が現実的な範囲に収まる。過去記事が増えてくると、このプレーンスニペットはかなり便利だ。また、「Oldest」というスイッチもついていて、順序を降順(新しい順)から昇順(古い順)に変更できる。全ての表示状態はURLで直リンクできるようになっている。

学者や芸術家のポートフォリオとして使うなら、プレーンスニペットの昇順表示が最適だろう。そのURLを伝えても良いし、そこから抜粋して履歴書などを書いても良い。書き溜めた記事が忘却されないという「ストック性」「ポートフォリオ性」とでも呼べる性質は、多数の記事を書く人間にとっては非常に重要だ。最新記事ばかりが表示されるブログのビューでは、書けば書くほど、過去に書いた力作が忘れ去られるリスクが高まる。しかし、プレーンスニペットで100件単位で見られるようになり、昇順に並べられるようになると、全ての記事がポートフォリオの一部になる。多作であることが視覚化されるため、書けば書くほど、それなりの達成感が得られる。SNS内部の記事を全部並べるのではなく、外部公開に足る品質であると判断されたもののみがポートフォリオに加わるので、外向けの顔を自在に維持して向上していける。多作のクリエータを繋ぎ止めるには、この性質はかなり重要だ。
デザインテーマ
各ユーザは、自分が好きなデザインテーマを選択できるようになっている。それはbody要素直下のdiv要素のクラス名として反映され、CSSのセレクタでデザインが切り替えられるようになっている。さしあたっては、デフォルトモードとダークモードだけを用意する。両者とも、余計な装飾は一切つけないながらも、2020年代のレスポンシブデザインのデザイン言語は守る。文書で重要なのは情報であり構造であると以前述べたが、見た目のデザインを変更できるのはその考え方と矛盾するだろうか。いや、しない。情報や構造を直感的に把握するには、情報や構造に応じた装飾をすることは効果的だ。構造が定まっていないと適切な装飾ができないので、構造の方が重要なのは確かだが、構造に応じた装飾も、記事の有用性を左右する重要な要素だ。そして、自分の記事の装飾を柔軟に変更したいというユーザの自然な要求には応えるべきだ。そして、読者が自分で容易したCSSでスタイルを上書きできる機能を備えるUAもある。視覚表現のCSSだけではなく音声表現のCSSもある。構造を固定して表現スタイルを柔軟に切り替える慣習はアクセシビリティを向上させる。

さしあたっては、以下のデザインテーマを用意した。他にも、パステルモードやらポップモードやらナチュラルモードやらモノクロモードを作っても良いが、きりがないので後回しにする。
- デフォルト : SNS内部の見え方とあまり変わらない見た目になる。私が最も読みやすいと思うもの。
- whiteboard : 学校で使うホワイトボードを模したもの。
- newspaper : 紙の新聞を模したもの。
- dark : 黒背景に白文字にして、巷のダークモードの見え方を模したもの。
- blackboard : 学校で使う黒板を模したもの。
- tategaki : 縦書き用の組版を施したもの。
- shimbun : 縦書きで紙の新聞を模したもの。
- hakuban : 縦書きでホワイトボードを模したもの。
- kokuban : 縦書きで黒板を模したもの。
選択の自由は大きい方が概して効用が高い。しかし、限定合理性により、選択すること自体が負荷になり、また最適な選択ができないと効用が下がるという選択のパラドックスが生じる。Webにおいては、取得したデータをどのように表示して利用するかは最終的に読者が決めるべき問題だが、どのように表示すればよると見やすいかを事前に知っているとは限らない。それを知っているのは著者もしくは編集者なので、文書の提供意図に適したデザインテーマを設定するのは彼らの仕事だ。それに基づいてデフォルトの表示がなされる。その上で、各ユーザは「?design=dark」などとパラメータを指定して任意のページのデザインテーマが上書きできるようになっている。選択の負荷を乗り越えても効用を増す余地があるのなら、そうすればいい。
さらに進んで、ユーザが任意にCSSを指定できるようにすると、こちらでCSSを書いてテストする手間が省けるのだが、スクリプティング攻撃の脆弱性が生まれるので許容できない。任意のCSSが書けるというのは選択の自由としては最大限のものだが、適切なデザインを施すための学習コストは非常に高い。そのコストをシステム運営側が請け負ったのがデザインテーマである。既存のデザインテーマをちょっとカスタマイズできるくらいなら、少ない追加コストでより高い効用を得られる可能性はある。フォントと色くらいはパラメータ化してもいいかもしれない。バナーや背景画像をカスタマイズできる機能も、いずれは欲しくなるだろうが、後回しにする。オブジェクトストレージと連携することになるので、実装が多少面倒だ。デザインテーマとの相性問題や、画像転送量が増大することによる副作用について検討する必要もある。任意の画像を利用可能にするよりは、最適化された画像を使ったファンシーなデザインテーマをいくつか用意した方が運用しやすそうだ。
縦書きのテーマにはかなり力を入れている。縦書きでブログが読めるサイトはほぼ無いので、差別化要因になるからだ。日本において、小説のほとんどと実用書の多くは縦書きで組版したものが出版されている。文書構造としては縦書きでも横書きでも変わらないので、だからこそデザインテーマを割り当てるだけで相互に変換ができるのだが、読書の際の「印象」「格調」「リズム」「没入感」といった感性的な要素が大きく違うと言われている。よって、特に小説を縦書きで提供できることは、クリエイタ層の意図を汲む上で重要だ。実現にあたっては、CSSのハックにかなり苦労した。横書きの文書では、文書は上から下にスクロールして読むわけだが、縦書きの文書では右から左にスクロールして読むことになる。しかし、ブラウザのレンダリングモデルは上下スクロールを前提としたものが多いため、flexレイアウトや絶対位置指定を多用して無理やり横スクロールで表現できるように頑張った。段落を字下げするとか、ヘッダのマージンを縦書き用に調整するとか、行の文字数を文庫と同様に最大38文字に制限するとか、サイドバーのために縦二段組を導入するとかいった細かい工夫もしている。その甲斐あって、特にスマートフォンなどの縦長の画面で読みやすいように仕上がっている。それでも、ページめくりのある紙の読書感とはまた異なるのだが、縦書きでの読書感を一般的なブラウザで提供できるというのは進歩と言えるだろう。縦書きのミームも、表意文字のミームと同様に、先細りして死滅することが予測されるが、その最後の輝きをここで微力ながら支えたいと思っている。個人的な趣向を抜いても、CSSを書くだけという限定されたコストで残存者利益を追求できるのは理がある。

余談だが、私はダークモードの利便性については懐疑的な立場だ。確かに黒背景に白文字と白背景の黒文字の視認性はほとんど同じだ。そして、黒背景の方が光束も照度も下げられ、OLEDの消費電力も抑えられる。しかし、本質はそこじゃない。色認知について考える必要がある。人間は、輝度が高いと色の弁別がしやすいが、輝度が低いと色の弁別がしにくい。色を弁別する錐体細胞の感度が明るさのみを弁別する桿体細胞の感度よりも低いからだ。例えば、#FFF(白)と#FEF(超薄桃白色)の違いは一目で分かるが、#000(黒)と#010(超薄緑黒色)の違いはほぼ弁別不可能だ。したがって、ライトモードでは背景色の違いに敏感になり、ダークモードでは文字色の違いに敏感になる。そして、Webデザインにおいては、backgroundやborderなどの背景的な装飾で情報の分類をほのめかすことが多く、文字色の違いを積極的に使うことは少ない。文字色が頻繁に変わると長文の文章は読みづらいからだろう。STGYのデザインも、背景色の違いをデザインに活用しているが、文字色で情報を区別させることはない。ゆえに、単純に明度を反転してダークモード化すると、ライトモードで直感的に認識できていた情報が認識できなくなる。もちろん、それは黒基調のデザインに構造的な欠点があることを意味しない。私はプログラミングの際には黒背景に白文字にして、シンタックスハイライトで文字色を変更することで快適に作業できている。全文を読むのではなく、偏った箇所に視線を泳がせるべき内容の場合、文字色で区別できる方が都合が良いのかもしれない。SNSやブログでも、文字色で情報の種別が区別できるようにデザインし直せば、その恩恵に与れる。しかし、それは単にモード切り替えで明度を反転させる以上の作り込みを要求する。今回作ったdarkテーマは明度の反転だけでなく彩度の強調を始めとした視認性の微調整も行って、ダークモードに一般的に求められる要件は満たしたつもりだ。whiteboardとblackboardは、それぞれ白基調と黒基調で見やすさを追求したものだ。
印刷用のスタイルも用意している。デザインテーマで文字色を変えたまま紙に印刷すると見づらいので、印刷の際には全てのテーマの色を初期化する。文字色は黒一色で、背景は白一色だ。文字色に違いがなくても各要素が弁別できるデザインにしているがゆえにデザインテーマで自由に色が変えられるわけだが、それが印刷時にも長所になる。また、サイトヘッダやページナビゲーションやサイドバーなどの要素は印刷する意味がないので、印刷時には表示されないようにした。

キャッシュとサムネイルとスロットリング
外部公開された記事は、バズった時にどの程度のアクセスが来るか全く予測がつかない。よって、レンダリングに必要な全てのデータはキャッシュから取得する必要がある。投稿IDをキーにして、本文と各種メタデータをJSONとしてキャッシュに格納しておく。TTLは24時間とかにしておく。バズった時にはキャッシュに乗っているはずだ。キャッシュが消えた瞬間にDBにアクセスが殺到するサンダリングハード(thundering herd)問題は残るが、DB層にもキャッシュがあるので、投稿単位のサンダリングハードが問題になることはないだろう。一覧画面に必要なスニペットのリストも当然キャッシュする。記事が更新されたり非公開にされた際には、キャッシュを明示的に消す。
キャッシュによって、DBの負荷は下げられるが、転送量は減らない。転送量が増えすぎると、VPS運用であれば転送制限に引っかかったり、クラウド運用であれば従量課金で莫大な請求が来たりするリスクがある。特に画像転送に関しては転送量が多いので問題になりがちだ。画像転送量を削減するべく、外部公開の記事に埋め込まれた画像はサムネイルにすり替える。内部では、投稿一覧画面ではサムネイルを表示し、投稿詳細画面では元画像を表示するようにしているが、外部公開では、必ずサムネイルを表示する。元画像は最大10MBで平均700KBくらいであるところ、サムネイルは平均100KBくらいになるので、劇的な効果がある。幅768ピクセルの記事内に埋め込む分には、100KBのWebP画像はほとんど劣化を感じさせない。元画像を見たければログインしてもらうことにして、コスト抑制とユーザビリティの維持と内部への誘引が鼎立できる。
転送量の問題の根本的な解決方法は、十分な帯域を確保するとともに、バズった分だけ収入を得て運用費を賄うビジネスモデルを構築する以外にはない。広告を張るのが率直な方法だろうが、それが望ましいかどうかは運営者が決めることだ。技術的に簡単にできる対策としては、アクセスが殺到した場合にその記事が見られなくなる機能を設けるというのがある。SNS内部で既に使っているスロットリング機能を流用して、外部公開記事のスロットリングもできるようにしてある。デフォルトでは、ユーザ毎の記事の1時間あたりのアクセス数の上限は1万回にしてある。これはビジネスモデルが何も無い小規模サイト用の設定だ。大規模サイトになればより大きい値にするか、無制限にすることだろう。スロットリングを記事毎でなくユーザ毎にする理由は3つある。記事毎だと記事を複製すれば簡単に回避できてしまうことと、ユーザ間の公平性を保つためと、アクセス制限回避の課金プランを作りやすくするためだ。
フィードバック機能とブランディングとビジネスモデル
SNSの特徴であるイイネやコメントの機能を外部公開することはできない。ユーザアカウントがない不特定多数からイイネを受け付けてもカウンタを上げ放題になってしまうし、同様に不特定多数からコメントを受け付けたらスパムされ放題になり、DBの負荷が制御できなくなってしまう。よって、外部公開記事へのフィードバックは外部では受け付けないと割り切る。もし記事にフィードバックしたかったら、STGYにユーザ登録してもらうように誘導する。
サービスヘッダの表示が有効である外部公開記事にアクセスした場合、STGYのセッション情報を調べたうえで、ログイン済みのユーザであれば、該当の記事の内部URLに遷移するためのボタンを画面左上に表示する。ログインしていないユーザであれば、STGYのログイン画面に遷移するボタンと、新規ユーザ登録をするボタンを、画面左上に表示する。いずれにせよ、STGY由来のデータであることがさり気なく分かるように、ボイラープレートを構成する。SubstackとかNoteとかが参考になるのだが、もっとさりげなくしたい。外部公開記事は、STGYというサービスの一部を使っているという感じではなく、そのようなWebページが突然存在しているかのようにしたい。このサイトはどんなブログエンジンを使っているのだろうと思ってページの隅々を探した読者のみが、STGYの存在に気づくくらいでいい。それすら嫌なユーザは、サービスヘッダの表示を無効にできる。
サービスヘッダの表示を無効にされると、サービスの存在を周知させることができなくなり、新規ユーザの獲得もできなくなるわけで、ブランディングの役に立たなくなる。ブランディングにも収益化にも役立たない記事が増え、しかもその一部がバズって計算コストや転送コストだけが嵩むことになるのは、ビジネス上のリスクだ。はてなブログで有料のProプランのユーザのみがサービスヘッダを外せるようになっているのは、それが理由だろう。しかし、OSSとしてのSTGYの機能としては、ユーザが望むなら、サービスヘッダを外せるようにする。勝手に何かが挿入される鬱陶しさから解放し、記事の制御権があくまで著者にあることを明確化することで差別化する。むしろ、各ユーザが自発的にサービスヘッダをつけたくなる状態を目指す。STGY内でフィードバックを得たいとか、STGYというサービス名を格好いいと思ってもらえる状態を目指す。サービスヘッダをつけてもらえないとしても、インフルエンサをサービス上に獲得できることは、利点がある。PVが増えれば、STGYというサービスが便利であることは口コミで伝わるだろう。
サービスヘッダの有無とは関係なく、PVに比例して転送量とDB負荷は上がり、その分だけ金がかかる。よって、外部公開記事のPVに応じて収益化する方法は、追って検討する必要がある。一定のアクセス数を超えたらそれに応じて著者に課金するのかもしれないし、投げ銭やサブスクリプションを導入して読者に課金するのかもしれない。OSSとしてのSTGYとしては課金機能を持たないので、広告モデルを導入するの率直だ。各ページの月間PVが1000を超えたら広告が出るとかいう方式にすると、一般ユーザの多くは広告で利便性が損なわれることはないし、バズったときは収益になるので、経済的に安定した運用ができるかもしれない。PVの数え上げはRedisでやって、100の倍数になった時だけDBに書き込むとかで良いだろう。いずれにせよ、とりあえずPVが出るようにしないと話が始まらない。
バックエンドのエンドポイント
ここまで述べた仕様を満たすには、バックエンドに以下のエンドポイントを足す必要がある。
- GET /posts/pub/{postId}
- 指定した投稿の外部公開用データを取得する。
- ログインを必要としない。
- 暗黙的に現在時刻以前の公開日時かどうかを条件に加える。
- 時系列で直前と直後の投稿のIDも返却値に含めてページネーションを効率化。
- GET /posts/pub-by-user/{userId}
- 指定したユーザの外部公開記事のスニペットの一覧を取得する。
- ログインを必要としない。
- offset、limit、orderをパラメータとして取る。
- 暗黙的に現在時刻以前の公開日時かどうかを条件に加える。
- GET /users/{userId}/pub-config
- 指定したユーザの外部公開の設定を取得する。なければデフォルト値を返す。
- ログインを必要としない。
- PUT /users/{userId}/pub-config
- 指定したユーザの外部公開の設定を記録する。あれば上書き、なければ新規作成。
- ログインを必要とする。
公開日時の設定は既存のエンドポイント(POST /posts/{id})で済ませた。専用のエンドポイントとして「POST /posts/{id}/pub」やら「DELETE /posts/{id}/pub」やらを作ることも考えたが、かえって管理がややこしくなるので止めた。
RCSやワープロとの連携
STGYでは凝った記事を書くクリエイタ層を狙う戦略を採っている。任意のテキストエディタで高効率な執筆作業をして、Git等の任意のRCS(revision control system)でデータ管理をしてもらうために、Markdownを採用している。その上で、記事の公開に伴うプレビューや校正の機能を拡充している。ここで、矛盾が生じる。STGY上でデータを更新したら、RCSとの二重管理になってしまうではないか。
STGYにRCSとしての機能群を足すのはと、CMSになることを意味する。もはやSNSではなくなってしまうので、許容できない。したがって、厳密なコンテンツ管理をしたいユースケースでは、コンテンツ管理のワークフローの一部を委任された存在として振る舞うことが望ましい。原本(single source of truth)はRCS側にあるものとし、公開に向けた編集作業と公開と公開後の修正作業をSTGY側で受け取った複製で行う。これはローカルエディタがファイルを開いているのと同じことだ。重要なのは、更新が終わった後で、RCS側にデータを書き戻すことだ。現状でも、手動でそれをやることは可能だ。フォームの内容をエディタにコピペして、保存して、RCSにコミットすればいい。
STGYは全ての操作がAPIでできるので、適当なPythonスクリプトでも書けば、Gitと組み合わせて運用することは難しくない。需要があるなら、配布パッケージにそれを含めるのもやぶさかではない。例えば、以下のようなコマンドで運用できるはずだ。ユーザ名とパスワードは環境変数に入れておき、ローカルファイル名を指定して同期の作業を行う。ローカルとSTGYを同期しさえすれば、あとはGitの機能で何とでもなる。
$ stgysync push papers/2025/oval-chainring-performance.md $ stgysync pull papers/2025/oval-chainring-performance.md
STGY上の投稿データは、元のファイル名が何だったか知らない。セキュリティ上の理由で、そのような属性を足すのも却下だ。ファイル名が分からないと、STGY上のどの投稿と同期を取ればよい分からないので困る。その回避策は2種類考えられる。pushした時に、ローカルの記事内にコメントとして「<[stgy:post-id:12345678]>」とかを埋め込む方法と、ローカルのsgty-post-id.txtとかいうファイルに、ファイル名と投稿IDの関係を記録しておくことだ。おそらく後者の方が楽だ。そのファイルもGitの管理対象にすれば、紐づけが失われることはない。pushには「--publish」とかいうオプションをつけて、いきなり外部公開できるようにしてもいい。
これもアイデアに過ぎないが、Webhookを仕掛けられるようにしても面白いだろう。user_pub_configsに属性を足すか、user_webhooksなるテーブルを作るかして、通知先のURLを仕掛けられるようにする。各投稿の詳細メニューで「invoke Webhooks」を選ぶと、投稿内容やメタデータをJSONにして通知先に送信する。投稿が新規登録または更新されたら勝手に発火するようにしてもいい。通知先がそれをどう使うかは、自由だ。記事の内容をRCSにフィードバックするかもしれないし、検索用のインデックスを作るかもしれないし、ニュースリリースサイトに転送するかもしれないし、AIが読んで何かするかもしれない。
ところで、クリエータやインフルエンサに選んでもらえるようにするのは大事だが、その層のITリテラシが必ずしも高いとは限らない。物書きであれば、Microsoft WordやGoogle Docsなどの履歴管理機能があるワープロソフトを使うことが多いだろう。ScrivenerやUlyssesのような長文執筆に特化したソフトウェアもあり、それらも自動保存による履歴管理と、適当なタイミングでスナップショット的なバックアップを取る機能がある。テキストエディタ派の場合、日付などをつけたファイルをコピーするなりGoogle Docsに置くなりして自分で履歴管理をするのが普通だろう。いずれにせよ、Gitのようなブランチのある管理方法は、作家1人と校正者1人とかいう構成では、明らかにオーバースペックだ。ローテクがむしろ好まれると言っても良いだろう。ならば、Git連携の機能がSNSに有ったとしても、使う人はかなり限定されるだろう。また、STGY上にWYSIWYGエディタを実装したとしても、WordやDocsに匹敵しない中途半端なものであれば、誰も好んで使ってくれないだろう。
現実的には、WordなりDocsなりのワープロソフトか、Emacsなり秀丸エディタなりのテキストエディタとの間で、コピーペーストでやり取りできれば十分なことがほとんどだろう。テキストエディタ派に関しては、Markdownの文字列そのものを授受するのが解法になる。そもそもこちら側の人達なので、これ以上懐柔する必要はない。ワープロ派に関しては、少なくともWordとDocsに関してはHTMLを解するので、STGYに投稿後の画面やプレビュー画面にレンダリングされた記事をそのままコピペすれば、ヘッダや太字や車体などの構造を保ったまま、ワープロ側にデータを移せる。CSSのスタイルも一緒にコピーされる。装飾を一切排除したHTMLのみをコピーする機能もつければ、STGYからコピペしたデータが、WordやDocsで文書を書いたのと全く同じ見た目になる。問題は、ワープロ側からSTGY側にデータを移す場合だ。現状では、ワープロ側でクリップボードにコピーした内容をSTGY側にペーストすると、単にテキストとして扱われて、構造が消えてしまう。その後に手動でマークアップをすればよいのだが、当然ながら面倒くさい。となると、クリップボードにはHTMLが入っているので、それをMarkdownに変換して貼り付ける機能が欲しくなる。これに関しては追々作ろうと考えている。それさえできれば、ワープロとSTGYの間の相互のデータ移動が完遂するので、ワープロ派も取り込めることになる。バージョン管理はワープロ側に任せればよいので、CMS機能は必要ない。Wordのdocxファイルをアップロードしたら中身をMarkdownにしてフォーム上に挿入する機能も便利かもしれない。Git連携に私の工数を使うよりは、ワープロ連携に工数を使うべきだろう。
CMS化のメモ
STGYにはドラフト機能がないし履歴管理機能もない。SNSとしての建付けを維持するために、それらを加える予定もない。ただし、STGYをベースにCMS機能を作りたいという人がいることも考えて、もし作るならというメモを書いておこう。まず、スキーマはこうする。
CREATE TABLE post_drafts ( id BIGINT NOT NULL, owned_by BIGINT REFERENCES users(id) ON DELETE CASCADE, ); CREATE INDEX idx_post_drafts_user_id_id ON post_drafts (user_id, id); CREATE TABLE post_draft_histories ( id BIGINT PRIMARY_KEY, draft_id BIGINT PRIMARY KEY REFERENCES post_drafts(id) ON DELETE CASCADE, payload VARCHAR(65535) NOT NULL ); CREATE INDEX idx_post_histories_draft_id_id ON post_draft_histories (draft_id, id); CREATE TABLE post_histories ( id BIGINT PRIMARY_KEY, post_id BIGINT PRIMARY KEY REFERENCES posts(id) ON DELETE CASCADE, payload VARCHAR(65535) NOT NULL ); CREATE INDEX idx_post_histories_post_id_id ON post_histories (post_id, id);
post_draftsは、各ユーザが持っているドラフトのリストを管理する。ドラフトにはIDだけがあり、実体はない。post_draft_historiesは、ドラフトIDに紐づけて任意の更新内容のJSONをpayloadという属性で保存する。タイムスタンプ由来のSnowflake IDが振られて、ドラフトID毎に時系列の降順で更新内容が見られることになる。post_historiesも全く同じデータ構造だが、紐づけられる対象がドラフトIDではなく投稿IDである。3つのデータベースとも、主キーであるIDがタイムスタンプを含むので、一定以上古いものは消せる。古いものを自動的に消せば、スケールが保たれる。ドラフトは実体を持たないので、更新履歴の最新のものがドラフトの内容となる。一方で、post_historiesは投稿の1つ前以前の内容だけをコピーして持つ。投稿データの最新版はpostsテーブル等に存在する。ドラフトは更新に格上げした時点で消されるが、ドラフトの更新履歴は、投稿に格上げした時点で、投稿の更新履歴に付け替える。
UIとしては、特定ユーザ(普通は自分)の一覧を見る画面がまず必要だ。これは投稿一覧のUIとほぼ同じデザインで良いだろう。そして、ドラフトでも投稿でも、各投稿のメニューに「show edit history」という項目を設けて、それを選んだらその投稿の更新履歴の一覧を表示する画面に遷移する。各履歴には「edit」ボタンがあり、それを押すとその履歴を入力フォームの内容に持つ編集画面が表示される。ドラフトの場合「保存」の他に「公開」ボタンがあり、それを押すとドラフトは投稿に格上げされる。また、既存の新規投稿の保存ボタンは「公開」ボタンに変え、それとは別に「ドラフト保存」ボタンを作る。プルダウンで切り替えられるようにしても良い。更新内容は前のバージョンとのdiffとして表示しても良いだろう。
はてなブログやGitHubをリバースエンジニアリングして上記を書いたが、おそらくこれが最も率直な設計だろう。特に技術的に難しいところはないし、なにか工夫して性能や機能を改善できることも無さそうだ。なので、粛々と作業すれば、普通に実現できるだろう。実装やテストの手間、メンテの手間、UIの複雑化、時間効率と空間効率の悪化などを飲んでもやるべきかどうかは、事業計画次第だ。
ニッチを狙う戦略
TwitterやFacebookがある汎用SNSの世界はレッドオーシャンだ。noteやらZennやらはてなやらアメーバやらがある汎用ブログエンジンの世界もレッドオーシャンだ。どっちの世界に正面から挑んでも、勝ち目は薄い。よって、その間のニッチを狙うのが自然だろう。ニッチなので市場は狭いが、そもそもこのプロジェクトは市場を席巻することを狙っていない。10万人のアクティブユーザに耐えるバックエンドの教科書的な作り方を世に示すことと、それ未満で良いができるだけ多くのアクティブユーザを獲得できるUXを自分なりに追求することだ。自分で運用する分には、それなりの数のユーザがいる状態で、黒字もしくはポケットマネーの範囲で安定運用できれば良い。OSS製品として公開しているので、STGYをそのまま使うか、そこで学んだことを活かして別のシステムを作って、誰かが大儲けしてくれても構わない。もし私が食いっぱぐれていたら、技術顧問的な立場で雇っていただければ幸いだ。
問題は、ニッチ市場に置いて「それなりの数のユーザを獲得する」ってのが既に難しいことだ。テクニカルライター、ソフトウェアエンジニア、理系の研究者などに多いであろうエディタ派にはMarkdownがそのまま刺さるだろうから、彼らが簡単に情報発信できる仕組みには一定の魅力があるだろう。作家やジャーナリストや文系の研究者などに多いであろうワープロ派にも、上述したワープロ連携機能が完成すれば、一定の魅力が出せるだろう。ともかく重要なのは、彼らのワークフローに寄り添って、その自然な延長として、簡単に情報発信できるようにすることだ。執筆95%・公開5%というモデルを前回の記事で書いたが、公開にかかる負荷は5%よりももっと下げたい。できれば100秒未満で済ませたい。つぶやきの延長レベルで、簡単に情報発信できることこそが重要だ。
公開作業の負荷の徹底削減というコンセプトが明確になると、いろいろアイデアが湧いてくる。例えば、Micorosoft WordやGoogle Docsのアドイン機能で、その文書の内容をワンクリックでSTGY上に同期させて公開までしてしまうのはどうか。それなら、ワープロ派(WYSIWYG派)も満足だ。イイネ数やら返信数やらもWordやDocsの画面で確認できるようにしてもいい。エディタ派をさらに満足させるべく、ローカルでプレビューができるアプリを作っても面白いかもしれない。任意のエディタでファイルを保存すると勝手にプレビューが更新されるのだ。直前のバージョンとのdiffをとれば修正場所が分かるので、自動スクロールと差分ハイライトだってできるだろう。STGYへの共有と外部公開もそのアプリからできるようにすれば、ローカルだけでワークフローが完結する。いずれのユースケースでも原本はSTGY上に無いので、やはりSTGYにCMS機能は不要だ。
コンテンツの流入を増やすための施策だけでは不十分だ。弱小SNSであるSTGY上にデータを置くだけではPVが稼げない。TwitterやFacebookなどでシェアするというのは、誰しもがやる基本行動になるだろう。外部公開記事はOGPをサポートしているので、特に新しく何かを作る必要はない。外部公開設定に「Show share buttons」とかのフラグを付けて、簡単にシェアできるようにしてもいいだろう。
STGY内部での交流を促進する施策も必要だ。FacebookやTwitterのように、各ユーザが興味を持ちそうな投稿をレコメンド表示するというのが考えられる。小説家になろうがやっているように、イイネやPVでランキングを作るのもいいだろう。そこまで作り込むのは私個人の余暇の工数では難しくなってくるが、協力者がいれば普通にできるだろう。
まとめ
SNSであるSTGYに外部公開機能を付け、ブログのエンドポイントとしても使えるようにした。これにより、Markdownによる投稿記事の書きやすさを活かして、質の高い記事を広くWWWに発信するプラットフォームになる道が開けた。SNSとしてのコンセプトが薄くなる欠点はあるが、記事の利用価値の向上およびユーザの流入とクリティカルマスの達成という大きな利点があるので、やる価値のある改修である。SNSとしての体裁を保っている以上、CMSとしての機能を作り込むことはしたくない。クリエータ層を支援することで差別化を図るために、システム外部で動作する連携機構を足していく戦略について追って考えていきたい。
実は、実装する前にこの記事を全て書き上げていた。そして、この記事自体をAIに読ませてプロトタイプを生成させることで、かなりの工数削減ができている。というかほぼ9割方はAIが書いたままのコードを採用している。広い意味ではスペック駆動開発なのだが、おそらく一流の技術者も含まれるであろう読者層の目にも耐える文章を書くという制約を自らに課すことで、その精度を高めるという目論見である。