ブログ記事からスライドを作れるようにしました
作ったもの
- 記事のfrontmatterに
slidesを足すと、/slides/記事のidにスライドができる - 記事の段落はスピーカーノートになる
- リスト・コード・画像・表はスライドに出る
作ったもの

作ろうと思った理由
記事を先に書いて、スライドはあとから作る
書き方のルール
| 記事 | スライド |
|---|---|
##の見出し |
章の区切り |
###の見出し |
1枚のスライド |
--- |
同じ見出しのままスライドを分ける |
| 段落 | スピーカーノート(スライドには出ない) |
| リスト・コード・画像・表 | スライドに出る |
| 段落の中の太字 | 表示するものがないときに大きく出る |
書き方のルール

全体の流れ
- ビルド時:記事の本文をMarkdownの構文木にして、スライドのHTMLを作る
- ブラウザ描画時: スライドモードに切り替え、発表者ビューとの同期
記事をスライドに分ける
記事をスライドに分ける
Markdownを構文木にする
function parse(body: string, mdx: boolean): Root {
return fromMarkdown(body, {
extensions: mdx ? [gfm(), mdxjs()] : [gfm()],
mdastExtensions: mdx
? [gfmFromMarkdown(), mdxFromMarkdown()]
: [gfmFromMarkdown()],
});
}記事をスライドに分ける
見出しで区切る
##が来たら、章の名前を覚えて新しいスライドを始める###が来たら、章の名前を小さく添えたスライドを始める---が来たら、同じ見出しのまま次のスライドにする
記事をスライドに分ける
見出しで区切る

記事をスライドに分ける
段落とそれ以外を振り分ける
段落は、そのあとに出てくるリストや画像のスピーカーノートになる
記事をスライドに分ける
1枚に載せる量を決める
const HEAVY: ReadonlySet<VisibleKind> = new Set([
'code', 'image', 'table', 'embed', 'tweet', 'html',
]);
function conflicts(visible: VisibleBlock[], kind: VisibleKind): boolean {
const hasHeavy = visible.some((v) => HEAVY.has(v.kind));
const hasList = visible.some((v) => v.kind === 'list');
if (HEAVY.has(kind)) return hasHeavy || hasList;
if (kind === 'list') return hasHeavy;
return false;
}記事をスライドに分ける
レイアウトを選ぶ
| 載っているもの | レイアウト |
|---|---|
| リストだけ | 箇条書き |
| コードだけ | コード |
| 画像だけ | 画像 |
| 表だけ | 表 |
| 種類が混ざっている | 汎用 |
| 段落しかない | 段落中の太字を大きく出す |
HTMLにする
HTMLにする
コードの色付け
highlighter ??= createHighlighter({
themes: [CODE_THEME],
langs: [],
engine: createJavaScriptRegexEngine({ forgiving: true }),
});HTMLにする
参照の番号
return {
type: 'emphasis',
data: {
hName: 'sup',
hProperties: { className: ['cite'] },
citation,
},
children: [
{
type: 'link',
url: reference.url,
title: citation.title,
children: [{ type: 'text', value: `[${citation.n}]` }],
},
],
} as RootContent;HTMLにする
参照の番号

スライドの表示
スライドの表示
16:9を保つ
.deck {
position: fixed;
inset: 0;
display: grid;
place-items: center;
container-type: size;
}
.slide {
grid-area: 1 / 1;
inline-size: min(100cqi, 100cqb * 16 / 9);
aspect-ratio: 16 / 9;
container-type: size;
overflow: clip;
}スライドの表示
デザインをCSS変数で切り替える
| 名前 | 動き |
|---|---|
| simple | アニメーションなし |
| keynote | ふわっと拡大して出る |
| paper | 横に流れる |
| pop | 弾んで出る |
| terminal | 見出しを打ち込むように出る |
スライドの表示
デザインをCSS変数で切り替える
.slide.is-active .anim,
.slide.is-active .block-list li {
animation: var(--enter) var(--dur) var(--ease) backwards;
animation-delay: calc(var(--delay) + var(--i, 0) * var(--stagger));
}スライドの表示
デザインをCSS変数で切り替える

スライドの表示
デザインをCSS変数で切り替える

スライドの表示
発表者ビューとの同期
Nでスライドの下にスピーカーノートを出すFで全画面にする- 印刷するとPDFにできる
はみ出し対策
- リストとコードや画像は、同じスライドに載せない
- はみ出すスライドだけ、表示するときに文字を小さくする
- ビルドしたスライドをブラウザで開いて、はみ出していないかをテストする
はみ出し対策
表示するときに文字を小さくする
if (fitsAt(1)) return { fit: 1, overflow: false };
if (!fitsAt(MIN_FIT)) return { fit: MIN_FIT, overflow: true };
let low = MIN_FIT;
let high = 1;
while (high - low > PRECISION) {
const mid = (low + high) / 2;
if (fitsAt(mid)) low = mid;
else high = mid;
}
return { fit: Math.floor(low * 100) / 100, overflow: false };はみ出し対策
はみ出しの判定
- スライドの本文の領域や、その中のブロックから、子の要素の矩形が出ていないか
- コードのようにスクロールする要素で、中身が要素より大きくなっていないか
まとめ
- 記事を書けば、スライドもできる
- 段落はスピーカーノート、リストや画像はスライドになる
- 分け方とレイアウトはビルドのときに決め、はみ出しはブラウザで調整する
- はみ出しはテストで確かめている
参照
- syntax-tree/mdast-util-from-markdowngithub.com/syntax-tree/mdast-util-from-markdownMarkdownをmdast(Markdownの構文木)にするライブラリ
- Shiki: Regex Enginesshiki.style/guide/regex-enginesShikiの正規表現エンジン(WASMとJavaScript)
- syntax-tree/mdast-util-to-hastgithub.com/syntax-tree/mdast-util-to-hastmdastをHTMLの構文木(hast)にするライブラリ。data.hNameで要素名を変えられる
- MDN: Container size and style queriesdeveloper.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_size_and_style_queriesコンテナ単位(cqw / cqh)の説明
- MDN: inertdeveloper.mozilla.org/ja/docs/Web/HTML/Global_attributes/inert
- MDN: Broadcast Channel APIdeveloper.mozilla.org/ja/docs/Web/API/Broadcast_Channel_API
ありがとうございました
スピーカーノート全文はブログ記事で読めます
