記事ができた。確認した。さて、どこに出すか。
Web・LINE・PDF。地域情報メディアの配信手段は複数あっていい。それぞれの入口から届く人が違う。AiInterviewは3つの配信手段を持つ。
GASからS3に直接PUT
記事を承認すると、管理画面の「Web公開」ボタンでS3に記事HTMLをアップロードする。
通常はAWS SDKを使う。しかしGASはNode.jsではなく独自のJavaScriptランタイムで動く。npmパッケージが使えない。SDKは使えない。
AWS Signature V4(SigV4)を自前で実装した。Utilities.computeHmacSignature()でHMACハッシュを計算し、AuthorizationヘッダーをURLFetchAppでS3にPUTリクエストとして送る。
function putToS3(key, content, contentType) {
const region = 'ap-northeast-1';
const bucket = 'aiinterview-articles';
const host = `${bucket}.s3.amazonaws.com`;
const now = new Date();
// SigV4の計算...
const response = UrlFetchApp.fetch(
`https://${host}/${key}`,
{
method: 'PUT',
headers: {
'Authorization': authHeader,
'x-amz-date': amzDate,
'x-amz-content-sha256': contentHash,
'Content-Type': contentType,
},
payload: content,
muteHttpExceptions: true,
}
);
}
実装で詰まった点はbyte[]の扱いだ。Utilities.computeHmacSignature()はbyte[]を返す。次のHMAC計算にはbyte[]のまま渡す必要がある。文字列に変換してから再変換すると正しくない結果になる。GASのbyte[]型はJavaScriptの配列としては扱えるが、型の素性を意識しないとハマる。
雑誌レイアウト
月号分の記事をA4サイズの雑誌レイアウトで表示して、ブラウザの印刷機能でPDFに書き出す機能を作った。
2カラム構成だ。左カラム(広め)に「今週の人」と本文・グルメ・新店情報。右カラム(60mm)に「今週のイベント」とニュース・お知らせ。
カラーパレットは紺(#1a3a5c)・ゴールド(#b8801a)・赤(#aa2218)。フォントは見出しにNoto Serif JP・本文にNoto Sans JP。地域情報誌らしい、落ち着いた雑誌感を目指した。
縦書きも検討した。日本語の紙媒体らしさが出るからだ。しかし断念した。ブラウザによってページ分割の動作が変わる。印刷時に安定しない。横書き2カラムの方が、実際に印刷して配れる品質になった。
号編成
記事を「号」にまとめる管理機能を実装した。
管理者は承認済み記事を号に追加して、セクションを割り当てる。「今週の人」「グルメ」「イベント」など。号ができたらMagazine.htmlで表示して印刷する。
スプレッドシートの構造はシンプルだ。Issuesシートが号ごとの情報(タイトル・ステータス・公開日)を持つ。IssueArticlesシートが号と記事の紐付け(セクション・表示順)を持つ。
LINE読者配信
LINE公式アカウントを友だち追加したユーザーが読者として登録される。
function handleFollow(userId) {
registerReader(userId);
pushMessage(userId, 'LINE友だち追加ありがとうございます!地域人の最新情報をお届けします。');
}
号を公開するタイミングで、全ACTIVEリーダーに記事タイトル一覧をLINEで送る。各記事の詳細はWebサイトのURLで誘導する。
大量のpushMessageを連続で送るとLINEのAPIレートリミットに引っかかる。50ミリ秒のスリープを1送信ごとに入れた。
S3ホスティングとLIFF
Register.html(事業者登録フォーム)はS3+CloudFrontでホスティングしている。
GASのHTMLサービスで配信する方法もある。しかしLIFFのエンドポイントURLとしてgoogleusercontent.comドメインのURLを使うと、LINEアプリ内での表示に問題が出ることがある。S3+CloudFrontの独自ドメインURLの方が安定して動作する。
S3からGASを呼び出すとき、google.script.run(GASのクライアントライブラリ)は使えない。S3からGASのWebアプリURLに直接fetch()でPOSTするように実装を変えた。
fetch('https://script.google.com/macros/s/.../exec', {
method: 'POST',
body: JSON.stringify({ action: 'register', ...formData }),
headers: { 'Content-Type': 'application/json' },
});
ホスティング先が変わると、GASとの通信方法を変える必要がある。前提を確認してから実装する順番が重要だ。
このシステムが解決したもの
コンテンツを人が取材して書く場合、1記事あたり数時間かかる。AiInterviewでは事業者がLINEで答えるだけで素材が集まり、Geminiが記事として整形する。人間の作業は確認と修正だけになる。
地域メディアが続くかどうかは「コンテンツを供給し続けられるか」にかかっている。供給コストを下げることが、持続可能なローカルメディアを作る技術的な解答だと思っている。
*シンプルシステム株式会社 代表 伊藤勝彦*