本記事では、CMS や外部サイトなど、JavaScript や iframe に制約のある環境でも
CHROCO のタイムラインを安全かつ確実に表示できるようにすることを目的として、
CHROCO が採用している Embed(埋め込み)向け静的HTML出力の仕組み を解説します。
具体的には、エンドポイント設計、Playwright によるプリレンダ処理、
埋め込み専用テンプレート構成、そしてフロント側の共有・埋め込み導線までを整理して説明します。
ここでの「静的HTML出力」とは、タイムラインの埋め込みURLを Playwright で事前に描画し、
CSS をインライン化した単体HTMLとして返却することで、
埋め込み先の実行環境に依存せずに表示可能とする仕組み を指します。
0. 全体像(図解)
まず全体フローはこうなっています。
1. なぜ静的HTML出力が必要か
通常の埋め込みは ?embed=true を付けた タイムライン描画用テンプレート を返します。
しかし、サードパーティのCMSやブログに貼り付ける場合、
- JavaScript実行が制限される
- CSS読込が期待通りに動かない
- oEmbedの
<iframe>ではなく「静的HTML URL」を求められる
といったケースがあり、レンダ済みHTMLの配布が必要になります。
CHROCOでは、/embed/{publicKey}.html のエンドポイントで
この「プリレンダ済みHTML」を配信します。
2. 静的HTML出力のエンドポイント
2.1 /embed/{timelinePublicKey}.html
静的HTML出力は EmbedPrerenderController が担当します。
公開キーを受け取り、対象タイムラインの公開可否・ユーザーを検証した上で
embed=true 付きURL を生成し、Playwrightプリレンダサービスへ渡します。
@GetMapping(value = "/embed/{timelinePublicKey}.html", produces = "text/html; charset=UTF-8")
@ResponseBody
public String embedTimeline(@PathVariable("timelinePublicKey") final String timelinePublicKey) {
final var timeline = this.timelineService.findByPublicKey(timelinePublicKey);
if (timeline == null || !timeline.isTimelinePublic()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "タイムラインが見つかりません。");
}
final var profileUser = this.userService.findById(timeline.getUserId());
if (profileUser == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "ユーザーが見つかりません。");
}
final var publicKey = this.timelineService.ensurePublicKey(timeline);
final var timelinePublicUrl = this.buildTimelinePublicUrl(profileUser.getUniqueId(), publicKey);
final var embedUrl = UriComponentsBuilder.fromUriString(timelinePublicUrl)
.replaceQueryParam("embed")
.queryParam("embed", "true")
.build()
.toUriString();
return this.embedPrerenderService.prerender(embedUrl);
}
この処理により、公開タイムラインのみ静的HTML化されます。
非公開や不正URLは 404 となります。
3. Playwrightによるプリレンダ(EmbedPrerenderService)
EmbedPrerenderService では Playwright を使ってページをレンダリングします。
ポイントは以下のとおりです。
-
WaitUntilState.NETWORKIDLEまで待機 -
#timeline-containerの描画を待機 -
document.styleSheetsから CSS を収集してインライン化 -
<script>/<link rel="stylesheet">/<style>を除去して軽量化 -
.chroco-embed-rootにall: revertを適用し、埋め込み先のCSS影響を排除
3.1 プリレンダの処理フロー(図解)
3.2 実装(抜粋)
public String prerender(final String url) {
try (BrowserContext context = this.getBrowser().newContext()) {
final var page = context.newPage();
page.navigate(url, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
page.waitForSelector("#timeline-container");
final var cssText = this.collectCss(page);
final var html = page.content();
return this.inlineCss(html, cssText);
}
}
CSSインライン化とスクリプト除去は inlineCss() 内で実施されます。
.chroco-embed-root に all: revert を加えることで、外部CSSの影響を最小化しています。
document.select("script:not([data-embed-keep])").remove();
document.select("link[rel=stylesheet]").remove();
document.select("style").remove();
...
fragment.append("<style type=\"text/css\">")
.append(".chroco-embed-root,.chroco-embed-root :where(div,span,p,ol,ul,li,section,article,header,footer,main,nav,aside,")
.append("h1,h2,h3,h4,h5,h6,strong,em,small,blockquote,figure,figcaption,table,thead,tbody,tfoot,tr,td,th,")
.append("a,button,input,textarea,select,label){all:revert;box-sizing:border-box;}")
.append("</style>\n");
4. 埋め込み専用テンプレート構成
4.1 embed-body.html
埋め込み専用の軽量レイアウトです。
ヘッダーやナビゲーションを持たず、contentブロックのみ出力します。
また、CSS/JS は最小限に抑えられています。
<body class="m-0 bg-white font-sans text-neutral-900">
{% block content %}{% endblock %}
<div style="display:flex; align-items:center; justify-content:flex-end; padding:8px 12px;">
<a href="https://chroco.ooo/" target="_blank" rel="noopener"
style="display:flex; align-items:center; gap:8px; font-size:16px; color:#6b7280; text-decoration:none;">
<img src="https://chroco.ooo/img/chroco_icon.png" alt="CHROCO" style="height:30px;" />
<span>CHROCO</span>
</a>
</div>
4.2 timeline-embed.html
embed=true の場合、タイムライン描画部分だけを出力するテンプレートを使用します。
<script id="timeline-data" type="application/json">
{{ timelineDataJson | raw }}
</script>
<div id="timeline-config" data-timeline-id="{{ timelineId }}" data-has-data="{{ hasTimelineData ? 'true' : 'false' }}" data-embed="true" data-click-mode="link"></div>
<div class="timeline-scroll timeline-scroll-embed overflow-x-auto overflow-y-hidden">
<div class="timeline-scroll-embed-inner">
<div id="timeline-container" class="w-full">
<div id="timeline-years" class="timeline-section timeline-years"></div>
<div id="timeline-months" class="timeline-section timeline-months"></div>
<div id="timeline-grid" class="timeline-section timeline-grid"></div>
</div>
</div>
</div>
5. 公開タイムライン側の埋め込み導線
公開タイムライン画面では、埋め込みモーダルに iframe と 静的HTML URL が表示されます。
PublicTimelineController が埋め込みURLと静的HTML URLを組み立て、テンプレートに渡しています。
model.addAttribute("timelineEmbedUrl", this.buildTimelineEmbedUrl(timelinePublicUrl));
model.addAttribute("timelineEmbedHtmlUrl", this.buildTimelineEmbedHtmlUrl(publicKey));
テンプレート側では data-embed-html-url に静的HTMLのURLを保持し、
embed-share.js がモーダルで表示・コピー用に組み立てます。
<div id="timeline-embed-config"
data-embed-url="{{ timelineEmbedUrl }}"
data-embed-html-url="{{ timelineEmbedHtmlUrl }}"
data-height="600"></div>
const embedHtmlUrl = ($config.data('embedHtmlUrl') || '').toString();
...
const staticHtmlCode = embedHtmlUrl ? embedHtmlUrl : '';
$('#embed-static-html-url').val(staticHtmlCode);
6. 静的HTMLの利用例(ブログ埋め込み)
以下のように、iframeではなく静的HTML URLをリンクとして扱うことも可能です。
CMSによってはHTML断片の埋め込みに制限があるため、リンク先として提供する運用も想定できます。
[タイムラインの静的HTMLを表示する](https://chroco.ooo/embed/{timelinePublicKey}.html)
もし <iframe> を許可している環境であれば、oEmbedの返却HTMLや
?embed=true のURLを使った埋め込みも選択肢になります。
7. まとめ
-
/embed/{publicKey}.htmlが 静的HTML出力の入口 - Playwrightで描画 → CSSインライン化 →
<script>除去 → 返却 - 埋め込み専用テンプレートと軽量レイアウトで最小構成に
- 公開タイムライン画面のモーダルで静的HTML URLを提供
「動的描画が使えない環境でもタイムラインを正しく見せる」ために、
プリレンダ&CSSインライン化が有効に機能しています。