Becalm Tech BlogTECH BLOG
バックエンド

イベント LP を Go ワーカーで静的生成する設計──Lスタンプの準備コスト削減

山下 恋花(SaaS事業チーム フルスタックエンジニア)公開日: 最終更新:

はじめに

Becalm の SaaS 事業チームでフルスタックエンジニアをしている山下恋花です。ふだんはお客様の案件の実装を担当していて、2026年11月2日に正式リリースした LINE のデジタルスタンプラリー Lスタンプ の運営チームにも加わりました。

スタンプラリーを開くのは、商店街の組合や自治体の担当者が中心です。スタンプを押す仕組みが整っても、告知ページやポスター、スポットに貼る QR の掲示物づくりは主催者の手元に残りがちです。

Lスタンプでは、主催者がイベントを公開した時点で Go のワーカーが特設 LP を静的 HTML に書き出し、S3 と CloudFront から配信します。検索エンジンと AI 向けの JSON-LD、Markdown 版、一覧ファイルも同じタイミングで出力されるので、主催者が LP を作る必要はありません。

特設 LP は Astro のビルドに含めず、Go のワーカーが html/template で 1 件ずつ生成しています。生成の経路は outbox から SQS を経てワーカーに届く 1 本だけで、同じイベントの処理は MySQL の GET_LOCK で直列にしました。LP で動く JS はインラインスクリプト 1 本だけで、JSON-LD や index.md、sitemap-events.xml、llms-events.txt は HTML と同じデータから作ります。ポスターと SNS 画像も同じ配色とデザイントークンで組み、ヘッドレス Chromium で PDF と PNG に変換しています。

書いている内容は 2026年11月時点の実装です。

Lスタンプの主催者に残っていた準備作業

Lスタンプは、商店街や自治体、商業施設がデジタルスタンプラリーを開催するための SaaS です。主催者が管理画面に入れるのはイベントの名前と期間、スポット、景品、ブランドカラー、ヘッダー画像といった事実だけで、告知と運営に要るものはそこから自動で作る前提で設計しています。

参加者はアプリを入れずに LINE ミニアプリからスポットを巡り、QR コードか GPS でスタンプを集めて、景品の獲得や抽選に参加します。主催者は Web の管理画面でイベントを登録し、店舗のスタッフはスマートフォンでクーポンを確認します。

主催者が手を動かすのは、管理画面のウィザードの 6 ステップです。最初にイベント名、期間、要約、ヘッダー画像を入れ、次にスポットの名前、住所、画像、取得方式を登録します。スポットは CSV で一括登録でき、座標が空なら住所から補います。続いて景品と特典の獲得ルールや抽選、クーポンを決め、デザインのステップではブランドカラーを 1 色選びます。テスト参加では下書きのまま本番と同じ流れを試せて、最後に 12 項目の公開前チェックを通せば公開です。

公開したあとの準備は Lスタンプが引き受けます。自動化した準備を、よくあるやり方と並べると次のようになります。

  • 告知ページ

    制作会社に頼むか CMS で手作りし、終了後の差し替えも手作業になる。Lスタンプでは公開と同時に特設 LP を生成し、開催中、終了、中止の表示も自動で切り替える

  • ポスターと掲示物

    スポットごとに QR を貼り分けて入稿データを作る。Lスタンプは 3 種類のデザインで A4 と A3 のポスターを作り、スポットごとの QR カードも生成する

  • 参加の受付

    応募フォームや会員登録を用意して、集めた個人情報を管理する。Lスタンプなら LINE ミニアプリから参加でき、会員登録はない

  • 結果の報告

    集計表を作り、報告資料にまとめる。Lスタンプでは集計画面に加え、PPTX と PDF の AI レポートを出す

告知ページを用意できず、ポスターと SNS の投稿だけで告知するイベントもあります。それでは、検索や AI の回答から来た人がイベントの詳細にたどり着けません。特設 LP の自動生成は、この状況をなくすために入れました。

Lスタンプの全体構成と技術スタック

Lスタンプは、Go で書いた API、6 つの Web アプリ、Terraform のインフラ定義を 1 つのリポジトリで管理しています。Web はどれも S3 と CloudFront で配る静的ファイルで、業務の処理は Go の API とワーカーに寄せました。

参加者・検索エンジン・AIはCloudFront経由でS3の静的ファイルだけを読み、主催者の操作はAPI、SQSを経てGoのワーカーに届き、ワーカーがLPをS3に書き出す構成図
図 1 Lスタンプの全体構成と技術スタック(2026年11月時点)
  • 公開サイト

    Astro で静的生成する。全ページを HTML で返して JS はほとんど使わない。読み込む JS の量には上限を決め、CI で検査している

  • 管理画面とミニアプリ

    React、Vite、TanStack Router、TanStack Query、Tailwind CSS で作る。静的ファイルだけで動くので、S3 と CloudFront に置ける

  • API とワーカー

    Go 標準の net/http と database/sql、素の SQL で書く。フレームワークも ORM も使わずに依存を減らし、読み込みと書き込みは CQRS で分けている

  • 契約

    OpenAPI。Web 側の型は OpenAPI から生成し、手書きの型を持たない

  • 配信

    非公開の S3 と OAC、Amazon CloudFront、CloudFront Functions、KeyValueStore。リクエストのたびに動くサーバーを持たない

  • 実行環境

    Amazon ECS on AWS Fargate、Amazon SQS、Amazon RDS for MySQL、AWS Lambda。常時動かしておくのは API とワーカーだけ

  • AI

    Amazon Bedrock の Claude を国内の推論プロファイルで呼び、集計値から報告書を書かせる。処理は国内のリージョンに閉じている

ここから先は、図 1 で赤い枠を付けたワーカーの話です。

イベント LP を Astro ではなく Go のワーカーで生成する理由

Lスタンプのイベント LP は、公開サイトの Astro のビルドに含めず、API 側の Go のワーカーが 1 件ずつ生成しています。LP を作り直すきっかけが、コードの変更ではなく主催者のデータの変更だからです。

公開サイトはすでに Astro で静的生成しているので、イベント LP も Astro のページにして、公開のたびにサイト全体をビルドし直す手もあります。ただ、ビルドとデプロイを待つと数分かかり、主催者が公開した直後に LP を見せられません。作り直すきっかけも多く、公開と内容の変更のほかに、開催の開始と終了、中止、非公開、主催者のプラン変更、景品の受付期間の終了があります。LP はイベントの数だけ増え続けるので、1 件変わるたびに全ページをビルドし直すのは無駄が大きすぎます。

そこで、静的生成を 2 系統に分けました。

公開サイトはコードの変更をきっかけにAstroでビルドし、イベントLPは主催者の公開をきっかけにGoのワーカーが1件ずつ生成し、どちらも同じS3とCloudFrontから配信することを比べた図
図 2 静的生成の 2 系統(2026年11月時点)

サービス紹介や料金、FAQ のように Becalm が書くページは Astro の担当で、デプロイのときにビルドします。主催者のデータから作るイベント LP はワーカーの担当で、データが変わったときに /e/<eventId>/ を 1 件ずつ書き出します。どちらの成果物も同じ S3 バケットに置く静的ファイルで、同じ CloudFront から配信します。

ワーカーの描画には Go 標準の html/template を使いました。主催者の入力を差し込むとき、HTML、属性、URL それぞれの文脈に合ったエスケープを標準ライブラリに任せられます。

公開から LP の配信までを outbox、SQS、名前付きロックでつなぐ

Lスタンプでは、主催者の操作から LP の配信までを、Transactional Outbox、Amazon SQS、Go のワーカーという 1 本の経路でつないでいます。ジョブに載せるのはイベント ID だけで、ワーカーは毎回 DB の最新の内容から LP をまるごと作り直します。

主催者の公開からAPIのoutbox、SQS、ワーカーの描画、S3への配置までの5段階と、10分おきの見回りがSQSに積む流れ、冪等・直列・即時反映の3つの決まりを示した図
図 3 イベント LP の生成パイプライン(2026年11月時点)

この経路で片付けたかった問題は 3 つあります。

DB の更新と LP の作り直しがずれる

API が DB を更新したあとで SQS に直接送ると、DB は更新できたのに送信に失敗したり、送ったあとでトランザクションがロールバックしたりして、両者がずれます。

Lスタンプでは、画面からの更新と同じトランザクションで domain_events テーブルに event_page.invalidated を書き込みます。これを別のプロセスが SELECT … FOR UPDATE SKIP LOCKED で取り出して SQS に送るので、LP の材料になるのはコミット済みの変更だけです。送信に失敗したら間隔を広げながら送り直し、失敗が続けばアラートを出します。

同じジョブが重複して届き、順番も入れ替わる

SQS の標準キューでは、メッセージが重複して届くことがあり、順番の保証もありません。ワーカーは何が変わったかを見ずに、毎回 DB の最新の内容から LP をまるごと作り直します。同じジョブが 2 回届いても結果は同じなので、重複も順不同も問題になりません。

描画の部分は純粋関数です。Render(site, page, now) は DB にも S3 にも時計にも触れず、現在時刻まで引数で受け取って、同じ入力なら同じバイト列を返します。開催中、開催前、終了、中止、無料プラン、デモの 6 パターンを golden ファイルでテストしているので、テンプレートを変えたときの差分はテストで確かめられます。

2 つのワーカーが同じイベントを同時に処理する

冪等にしても、まだ穴があります。主催者がイベントを非公開にした直後に、古い内容を読んだ別のワーカーが LP を置き直すと、消したはずの LP が戻ってきます。

そこで、DB を読んでから S3 に置き終えるまでを MySQL の名前付きロック GET_LOCK('lp:<eventId>') で囲みました。

api/internal/jobs/lock.go

// api/internal/jobs/lock.go(抜粋)
conn, err := db.Conn(ctx) // ロックは接続に結び付くので、1 本借りて最後まで持つ
// ...
err = conn.QueryRowContext(ctx, "SELECT GET_LOCK(?, ?)", name, int(wait/time.Second)).Scan(&got)
// ...
defer func() {
    // 放せなかったら接続ごと捨て、ロックを持ったまま別の処理に貸し出さない
    if _, err := conn.ExecContext(context.WithoutCancel(ctx), "DO RELEASE_LOCK(?)", name); err != nil {
        _ = conn.Raw(func(any) error { return driver.ErrBadConn })
    }
}()

GET_LOCK のロックはセッション、つまり接続に結び付くので、database/sql のコネクションプールと組み合わせるときは注意が要ります。ロックを取った接続を 1 本借りたまま処理を進め、解放に失敗したら driver.ErrBadConn を返して接続ごと捨てます。そうしないと、ロックを持ったままの接続がプールに戻り、別の処理に貸し出されてしまいます。

ロックのために Redis などを足さず、すでにある RDS の機能で済ませたので、常時課金のリソースは増えていません。

時刻で変わる内容は 10 分おきに比べて直す

LP の内容は、主催者が何もしなくても変わります。開始時刻を過ぎれば開催中に、終了時刻を過ぎれば終了の表示に切り替わり、受付期間が終わった景品は LP から外れます。

時刻ちょうどにタイマーを発火させる作りにはしていません。10 分おきの見回りが、LP を置いたときの状態と今あるべき状態を比べ、食い違っているイベントの ID を同じキューに積みます。見るのは 3 点です。開催前、開催中、終了という開催の段階が、置いたときから変わったかどうか。LP を置くときには、載せた景品のうち最も早く期間が切れる時刻を prizes_change_at に記録しておき、その時刻を過ぎたかどうか。そしてテンプレートの版が、置いたときと違うかどうかです。

見回りは ID を積むだけで、描画するのは通常の経路と同じワーカーです。経路が 1 本なので、どのきっかけで作られた LP も同じ結果になります。

テンプレートを変えたら公開中の LP をすべて作り直す

静的 HTML を前もって作る方式では、テンプレートを直したときに既存のページをどう更新するかが問題になります。Lスタンプでは、テンプレート、CSS、デザイントークン、インラインの JS の中身からテンプレートの版を計算しています。

api/internal/eventpage/version.go

// api/internal/eventpage/version.go(抜粋)
var templateVersion = sync.OnceValue(func() string {
    h := sha256.New()
    fmt.Fprintf(h, "revision:%d\n", renderRevision) // Go 側の組み立てを変えたら手で上げる
    for _, name := range templateFiles {            // page.html.tmpl・page.md.tmpl・page.css・lp-tokens.css
        b, _ := files.ReadFile(name)
        fmt.Fprintf(h, "%s:%d\n", name, len(b))
        h.Write(b)
    }
    h.Write([]byte(joinFromMessageJS))
    return hex.EncodeToString(h.Sum(nil))[:16]
})

LP を置くたびにこの版を DB に記録し、見回りが版の違う公開中の LP を探して積みます。1 回の見回りで扱う件数には上限があり、残りは次の回にカーソルの位置から再開します。1 件ずつ別のジョブなので、どれかが失敗しても残りは止まりません。テンプレートを変える開発者はワーカーをデプロイするだけでよく、移行用のスクリプトは書きません。

ブランドカラー 1 色から読める配色を自動で作る

Lスタンプの LP で主催者が決める見た目は、ブランドカラー 1 色とヘッダー画像 1 枚だけです。ワーカーはブランドカラーから 5 つの CSS 変数を作り、WCAG 2 のコントラスト比 4.5:1 を下回る組み合わせが出ないように調整します。

ブランドカラー#f07b1aから、アクセント、濃い地、淡い地、地の上の文字の4色を作り、それぞれのコントラスト比を示し、LINEで参加するボタンだけ緑で固定することを示した図
図 4 ブランドカラーから 5 つの CSS 変数を作る処理(api/internal/eventpage/theme.go)

図 4 では、主催者がオレンジの #f07b1a を選んでいます。白い文字を載せるとコントラスト比は 2.78 で、このままでは読めません。ワーカーは用途ごとに次のように色を作ります。

  • --theme-main

    見出しの印やステップ番号に使う。入力の色をそのまま使い、上に載せる文字は白と黒のうち比が大きい方を選ぶ。例では黒文字で 7.55 になる

  • --theme-main-strong

    画像が無いときのヒーローの地に使う。黒を 15% 混ぜ、白文字との比が 4.5 以上になるまで黒を 2% ずつ足す。例では #b55d14 になり、比は 4.62

  • --theme-main-bg

    淡い帯や、景品画像が無いときの地に使う。白に 90% 寄せた色で、例では #fef2e8、本文色との比は 16.13

  • --theme-main-text

    ページの地に載せる色付きの文字に使う。ページの地 #f4f6f5 との比が 4.5 以上になるまで暗くする。例では #ae5913 で、比は 4.57

表の比は、theme.go と同じ WCAG 2 の式で私が計算し直したものです。

実装で気を付けたことが 2 つあります。1 つは、比を判定する色を #rrggbb に丸めたあとの値にしたことです。小数のまま判定すると、CSS に書き出すときの丸めで 4.5 を割ることがあります。もう 1 つは、色付き文字の基準を白ではなく、実際に置くページの地の色にしたことです。白で判定すると、少し暗い地の上では基準を下回ってしまいます。

鮮やかなピンクの #e91e63 は白文字が合いそうに見えますが、白との比は 4.35 で基準に届かず、アクセントの上には比 4.83 の黒が選ばれます。テストでは白、黒、明るい黄色など 14 色を入れ、どの組み合わせでも 4.5 以上になることを確かめています。LINE で参加するボタンだけはテーマに関係なく緑に固定し、押せば LINE が開くとひと目でわかるようにしました。

色、余白、文字の大きさのトークンは、Figma と同期している Web 側の共通パッケージ @l-stamp/ui が正本です。ここから LP 用の CSS を生成して Go の embed で取り込み、正本とのずれは CI で検査しています。管理画面もミニアプリも LP も、同じトークンで見た目をそろえています。

主催者の入力と画像の扱いにも決まりがあります。入力はテキストと https:// のリンクの断片に分けてからテンプレートに渡し、HTML は受け付けません。リンクには rel="ugc nofollow noopener" を付けます。日本語の折り返しは CSS の line-break: strict で禁則処理し、長音や小さい「ゃ」が行頭に来ないようにしています。Go 標準のデコーダーは Exif の向きを見ないので、画像は縮小の前に向きを回します。展開する前には画素数を確かめ、大きすぎる画像は使いません。縮小した画像は原本のハッシュをファイル名にして immutable で配信し、古い画像は HTML を置き終えてから消します。

管理画面にある LP のプレビューも、ワーカーと同じ組み立て関数とテンプレートで描画し、sandbox 属性付きの iframe で表示しています。だからプレビューと本番の見た目はずれません。

検索エンジンと AI に読まれる LP の構造

Lスタンプのイベント LP は、人が読む HTML と同じデータから、検索エンジン向けの JSON-LD とサイトマップ、AI 向けの Markdown 版と llms-events.txt を一緒に出力します。検索や AI の回答から来た人を LINE ミニアプリへ案内するのが、LP の役目だからです。

1つのイベントのデータから、人向けのindex.html・OG画像・QR、検索エンジン向けのJSON-LD・canonical・sitemap-events.xml、AI向けのindex.md・llms-events.txt・AIクローラーの許可を出力することを示した図
図 5 イベント LP の生成物と読み手(2026年11月時点)

ポスターの QR は LINE ミニアプリに直接つながりますが、近くのスタンプラリーを検索した人や AI に尋ねた人が最初に着くのは LP です。LP の冒頭には、誰がいつどこで何を開き、どう参加すればいいかを文章で書き、開催期間、エリア、スポット数、参加費、参加の手順を最初の 1 画面に並べています。AI の回答はページ全体ではなく段落単位で引用されることが多いので、冒頭の要約だけで答えが完結するようにしました。

機械向けの Markdown と一覧ファイルを出す

HTML と同じ内容の Markdown を /e/<eventId>/index.md に置き、HTML の <link rel="alternate" type="text/markdown"> から指しています。主催者の文章は Markdown の記号をエスケープしてから書き出すので、見出しやリンクとして解釈されることはありません。

一覧は 2 種類あります。sitemap-events.xml には検索に載せる LP を終了済みのものも含めてすべて入れ、llms-events.txt には開催前と開催中のイベントだけを、名前、期間、エリア、URL の形で並べます。どちらも robots.txt と llms.txt からたどれます。robots.txt では GPTBot、OAI-SearchBot、ClaudeBot、Claude-SearchBot、PerplexityBot、Google-Extended を名指しで許可しています。

イベントが 1 件もない時期にも robots.txt が指す一覧が 404 にならないよう、毎日の見回りでも一覧を作り直しています。

LP で実行する JS はインラインの 1 本だけにする

LP で実行する JS は、インラインスクリプト 1 本だけです。LINE の配信から LP に来た人の参加リンクに配信 ID を引き継ぐためのもので、ほかの動きは HTML と CSS で作りました。

CSS も HTML にインラインで書いていて、HTML と CSS を合わせた大きさには上限を決めてテストで検査しています。

PC で開いた人向けの QR は、生成時に SVG で埋め込みます。地図は操作できる地図にせず、Mapbox の Static Images API の画像を <img> の遅延読み込みで表示しています。JS が要らず、課金も表示した回数の分だけで済みます。

公開サイトの CSP では 'unsafe-inline' を使わず、LP に埋め込んだ JS の sha256 だけを許可しています。JS を変えると、まだ作り直していない LP の古い JS が CSP で止まってしまいます。そのため移行のあいだだけ、Terraform の lp_script_previous_sha256 に前の版のハッシュを足して両方を許可します。

検索に載せない LP を決めておく

無料プランのイベント、主催者が検索に載せない設定を選んだイベント、デモのイベントは noindex にして、サイトマップと llms-events.txt から外しています。無料プランでも登録はでき、書いた文章とリンクは Lスタンプのドメインで公開されます。これがスパムやフィッシングに使われると、同じドメインにある管理画面まで信用を落とします。無料プランの LP を検索に載せないのは、そうした目的で登録する動機を減らすためです。問題のある LP は運営が強制的に非公開にでき、その操作は監査ログに残ります。

ポスターと SNS 画像をヘッドレス Chromium で生成する

Lスタンプのポスター、スポットの QR カード、SNS 画像は、LP と同じテーマ色とデザイントークンを使って HTML で組み、ヘッドレス Chromium で PDF や PNG に変換しています。主催者は管理画面で作成ボタンを押すだけです。

LPと同じ配色のHTMLテンプレートを、外部通信を遮断したLambda上のChromiumでPDFとPNGに変換し、ポスター・QRカード・SNS画像・一括ZIPを作る流れの図
図 6 告知素材の生成(api/internal/materials・renderer・platform/chromium)

作れる素材は、3 種類のデザインから選べる A4 と A3 のポスター、A6 のスポット QR カードとそれを A4 に 4 面付けしたもの、1080×1080 と 1200×630 の SNS 画像、それにこれらをまとめた ZIP です。PDF を Go の PDF ライブラリで組み立てなかったのは、LP と同じ配色の関数とトークンをそのまま使え、AI レポートの PDF とも作り方をそろえられるからです。A3 のポスターは、CSS 変数 --k: 1.414 で A4 のレイアウトを √2 倍にしているだけです。変換は Lambda で動かしています。

主催者の入力を含む HTML をブラウザで開くことになるので、外への通信は 4 段階で止めています。まず、テンプレートの <head> で charset の直後に CSP の meta を置いて default-src 'none' にし、画像と QR はすべて data: URL で埋め込みます。次に、Chromium の起動引数で名前解決を無効にし、待ち受けのないプロキシを指定して、IP を直書きされても外へ出られないようにしました。HTML は file:// で開かず、about:blank に CDP の Page.setDocumentContent で流し込みます。ローカルのファイルを読み込まれる経路を残さないためです。最後に、読み込みが 1 件でも止められたら変換を失敗として扱います。

見落としやすいのはこの最後の段階です。CSP で読み込みが止められても、ページの load イベントは発火します。何もしなければ、画像の欠けた PDF が成功扱いで主催者に届きます。CDP の Network.loadingFailed と CSP 違反のログを数え、1 件でもあれば失敗にしています。

スポットの QR には、HMAC-SHA256 で署名したトークンを入れています。QR を再発行すると世代が上がり、以前に印刷した QR はその時点で使えなくなります。参加用の QR はイベント ID だけで決まるので、公開前の下書きの段階から印刷を発注できます。

参加者の個人情報を集めない設計

Lスタンプは参加者に会員登録を求めず、LINE ミニアプリの ID トークンで本人を確かめます。メールアドレスを取得するためのスコープはそもそも申請しておらず、氏名、住所、電話番号を受け取るのは景品の配送を申し込んだときだけです。

個人情報保護委員会が 2026年10月28日に公表した資料によると、2026年4月1日から10月16日までに報告された不正アクセスによる個人データの漏えい等は、重複を含む延べ人数で約 2 億 6,000 万人分でした。10 月は 16 日までで 830 件の報告があり、9 月 1 か月分の約 2.7 倍です。2026年10月に公表された国内の事案でも、外食や小売の公式アプリで会員情報が数百万件単位で漏れた例が続きました。

位置情報はスタンプを押す瞬間にだけ送られ、取得の判定に使われます。主催者に見せる参加者の ID は主催者ごとに別の値にしているので、複数の主催者が CSV を突き合わせても、同じ人が他のイベントに参加しているかどうかはわかりません。個人データはイベントの終了から 1 年で匿名化し、集計値だけを残します。

AI レポートも同じ考え方で作っています。LLM に渡すのは集計表の値だけで、参加者の表を読むパッケージを import することは、静的解析の depguard で禁じています。送る前には LINE の userId やメールアドレス、電話番号などの形をした文字列が混じっていないかを検査し、1〜4 人という小さな値は 5 人未満という表記に丸めます。

割り切ったことと残課題

Lスタンプのイベント LP には、仕様として割り切ったところと、運用で補っているところがあります。どれも設計書に理由と一緒に書き残してあり、条件が変われば見直すつもりです。

  • リッチリザルト

    Google は会員登録や招待が要るイベントをリッチリザルトの対象から外している。LINE での参加がこれに当たると判断されれば、リッチリザルトには出ない。ページそのものは検索に載る

  • 時刻による切り替え

    開催の開始と終了、景品の期間切れは 10 分おきの見回りで反映するので、時刻ちょうどには切り替わらない

  • 景品の期間

    曜日や時間帯の条件が付いた景品も日付で区切るため、実際に受け取れる最後の時刻より少し長く LP に載ることがある。受け取れる景品が先に消えることはない

  • LP の JS の変更

    CSP の許可を新旧両方の版に広げ、作り直しが終わったことを DB で確かめてから元に戻す。この手順は手作業になる

技術面でよりよいと考えた選択を実際に組み上げ、使う人の目線に立ったサービスとして走り出すところまでは来られました。これからは UI と UX の改善や、パートナーと組んだ新しい機能の開発に力を入れていきます。

まとめ

静的配信でも、生成のきっかけをデプロイではなくデータの変更に置けば、サーバーを持たずに、主催者が操作したすぐあとに反映されるページを作れます。ただ、冪等な作り直しだけでは足りず、重複、順不同、同時実行のそれぞれに手当てが要りました。Lスタンプでは構造化データや Markdown 版をテンプレートに組み込んであるので、主催者が何も意識しなくても、どのイベントの LP も同じ形で検索エンジンと AI に届きます。

受託開発で私が担当してきたのは、限られた人だけが使う非公開のシステムがほとんどでした。ページが絶えず増え続け、そのひとつひとつを検索エンジンに届ける必要がある仕事は、Lスタンプが初めてです。SEO や AIO、LLMO の知見を追いかけながら、検索に読ませるための工夫を参加者の使いやすさとも両立させる。これは今までの仕事になかった視点で、設計を主に担った石塚さんと相談しながら、一つずつ形にしてきました。機械に読ませる構造と人にとっての読みやすさを同じデータから作る考え方は、受託の案件で公開サイトを作るときにも持ち帰れそうです。

スタンプラリーを企画している方は、ぜひ Lスタンプ を使ってみてください。Lスタンプでは一緒に取り組むパートナーも募集しています。詳しくはパートナー募集ページをご覧ください。

よくある質問

Q. Lスタンプのイベント LP は、アクセスのたびにサーバーで生成していますか。

生成していません。主催者がイベントを公開したり変更したりしたときに、Go のワーカーが静的 HTML を作って S3 に置き、CloudFront から配信します。アクセスのたびに動くサーバーやサーバーレス関数はありません。

Q. 無料プランのイベントの LP も検索結果に出ますか。

出ません。無料プランのイベントと、主催者が検索に載せない設定を選んだイベントの LP は noindex にし、sitemap-events.xml と llms-events.txt からも外しています。検索に載るのは有料プランのイベントの LP です。

Q. LP のテンプレートを変えたとき、公開中の LP はどう更新されますか。

ワーカーをデプロイすれば、10 分おきの見回りがテンプレートの版の違う LP を見つけて、1 件ずつ作り直します。テンプレート、CSS、JS の中身から計算したハッシュを版として LP ごとに記録しているので、移行用のスクリプトは要りません。

参考資料

Becalm では、一緒にプロダクトをつくるエンジニアを募集しています。この記事に興味を持っていただけた方は、面談の際に『この記事を読んだ』とお伝えください。採用ページ

Becalm では、Web サイト制作・Web システム開発・SaaS 開発のご相談を受け付けています。同じような課題をお持ちの方は、お気軽にお問い合わせください。お問い合わせページ

Becalmで、一緒につくりませんか。

エンジニア・デザイナーを募集しています。カジュアル面談も歓迎です。