Mdbook Embedify
This is a mdbook preprocessor plugin that allows you to embed apps to your book, like youtube, codepen, giscus and many other apps.
Usage
Installation
There are two ways to install this preprocessor.
You can install it from crates.io using cargo.
cargo install mdbook-embedify
Or you can download the binary from releases page.
Then you can check your installation by running:
mdbook-embedify --version
After installation, add the following code to your book.toml
file:
[preprocessor.embedify]
And that’s it! You can now use embed
macro to embed apps to your book.
Syntax
The basic syntax is like this:
{% embed app options[] %}
options are key-value based array seperated by space and its value should be wrapped by quotes.
For example:
{% embed codepen user="MR-Addict" slug="NWBOqKw" height="600" theme="dark" loading="lazy" %}
See more examples at apps section.
More Apps
Attention 💥
Support since v0.2.14.
Good to know 💡
Custom templates acts like dynamic reusable components. If you want to just copy static content, you should use the include app instead.
You may have some other apps that preprocessor doesn’t support yet. However, it’s very easy to add a new app based on this project custom template engine.
In this section, I will show you how to add custom app to this preprocessor.
Create a new app
Template folder
First we need to put a new app template in the assets/templates folder (which is relative to book.toml
file).
You can change the template folder path by setting custom-templates-folder
value in the preprocessor section. The default value is assets/templates
.
[preprocessor.embedify]
custom-templates-folder = "assets/templates"
The template folder path shoulde be relative to the book root directory, which is the directory where the book.toml
file is located.
Template file
Now let’s create a new app called canvas. Which is a simple drawable canvas app.
The template file name will be the app name. For example, we want to add a new app called canvas, then we should create a canvas.html under templates folder.
If your custom app name is the same as the built-in app name, the custom app will override the built-in app while rendering.
First we add some basic html structure and some styles to the canvas.html
file:
<div class="canvas-container">
<canvas height="400"></canvas>
</div>
<style>
.canvas-container {
width: 100%;
background: white;
border-radius: 1rem;
border: 1px solid #ccc;
background-size: 20px 20px;
background-image: linear-gradient(to right, #eee 1px, transparent 1px),
linear-gradient(to bottom, #eee 1px, transparent 1px);
}
</style>
And then add some js code to make it drawable:
<script>
document.addEventListener("DOMContentLoaded", () => {
const container = document.querySelector(".canvas-container");
const canvas = container.querySelector("canvas");
const ctx = canvas.getContext("2d");
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) canvas.width = entry.contentRect.width;
});
resizeObserver.observe(container);
let drawing = false;
const lastPos = { x: 0, y: 0 };
// Draw a line from last position to current position
function draw(x, y) {
ctx.beginPath();
ctx.moveTo(lastPos.x, lastPos.y);
ctx.lineTo(x, y);
ctx.stroke();
lastPos.x = x;
lastPos.y = y;
}
// Mouse events
canvas.addEventListener("mousedown", (e) => {
drawing = true;
lastPos.x = e.offsetX;
lastPos.y = e.offsetY;
});
canvas.addEventListener("mousemove", (e) => {
if (!drawing) return;
draw(e.offsetX, e.offsetY);
});
canvas.addEventListener("mouseup", () => (drawing = false));
canvas.addEventListener("mouseout", () => (drawing = false));
// Touch events
canvas.addEventListener("touchstart", (e) => {
if (e.touches.length !== 1) return;
e.preventDefault();
drawing = true;
const rect = canvas.getBoundingClientRect();
lastPos.x = e.touches[0].clientX - rect.left;
lastPos.y = e.touches[0].clientY - rect.top;
});
canvas.addEventListener("touchmove", (e) => {
if (!drawing || e.touches.length !== 1) return;
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const x = e.touches[0].clientX - rect.left;
const y = e.touches[0].clientY - rect.top;
draw(x, y);
});
canvas.addEventListener("touchend", () => (drawing = false));
canvas.addEventListener("touchcancel", () => (drawing = false));
});
</script>
Good to know 💡
You can add css and js content to the template file which should be put inside
style
andscript
blocks.
However, we want to the canvas height to be dynamic. We can do this by using placeholder syntax:
<canvas height="{% height=400 %}"></canvas>
Which means the height of the canvas will be replaced by the value of height key. If user doesn’t provide the value, the default value 400 will be used.
Placeholder syntax
Syntax
There are two ways of adding dynamic values to the template file:
- Put key name in the placeholder, like {% key %}, and you can add default value after the key name, like {% key=default %}. The default value will be used if user doesn’t provide the value.
- Wrapped with preprocessor name, like {% processor(key=default) %}. The processor name acts like function name, it will be used to process the inner value and replace the placeholder.
Placeholder
The inner value is key follwed by a default value in the form of key=default. If the key is not provided, the default value will be used.
Preprocessor
Now only markdown is supported, markdown will treat the inner value as markdown content and render it to be html.
Examples
- {% height %} means the placeholder will be replaced by the value of height key and height is not optional because it doesn’t have a default value.
- {% height=400 %} means the placeholder will be replaced by the value of height key. If user doesn’t provide the value, the default value 400 will be used.
- {% markdown(message) %} means the placeholder will be replaced by the value of message processed by markdown processor.
Final template file
Here is the final template file for the canvas app:
<div class="canvas-container">
<canvas height="{% height=400 %}"></canvas>
</div>
<style>
.canvas-container {
width: 100%;
background: white;
border-radius: 1rem;
border: 1px solid #ccc;
background-size: 20px 20px;
background-image: linear-gradient(to right, #eee 1px, transparent 1px),
linear-gradient(to bottom, #eee 1px, transparent 1px);
}
</style>
<script>
document.addEventListener("DOMContentLoaded", () => {
const container = document.querySelector(".canvas-container");
const canvas = container.querySelector("canvas");
const ctx = canvas.getContext("2d");
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) canvas.width = entry.contentRect.width;
});
resizeObserver.observe(container);
let drawing = false;
const lastPos = { x: 0, y: 0 };
// Draw a line from last position to current position
function draw(x, y) {
ctx.beginPath();
ctx.moveTo(lastPos.x, lastPos.y);
ctx.lineTo(x, y);
ctx.stroke();
lastPos.x = x;
lastPos.y = y;
}
// Mouse events
canvas.addEventListener("mousedown", (e) => {
drawing = true;
lastPos.x = e.offsetX;
lastPos.y = e.offsetY;
});
canvas.addEventListener("mousemove", (e) => {
if (!drawing) return;
draw(e.offsetX, e.offsetY);
});
canvas.addEventListener("mouseup", () => (drawing = false));
canvas.addEventListener("mouseout", () => (drawing = false));
// Touch events
canvas.addEventListener("touchstart", (e) => {
if (e.touches.length !== 1) return;
e.preventDefault();
drawing = true;
const rect = canvas.getBoundingClientRect();
lastPos.x = e.touches[0].clientX - rect.left;
lastPos.y = e.touches[0].clientY - rect.top;
});
canvas.addEventListener("touchmove", (e) => {
if (!drawing || e.touches.length !== 1) return;
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const x = e.touches[0].clientX - rect.left;
const y = e.touches[0].clientY - rect.top;
draw(x, y);
});
canvas.addEventListener("touchend", () => (drawing = false));
canvas.addEventListener("touchcancel", () => (drawing = false));
});
</script>
Use the new app
After creating the template file, we can use the new app in our book:
{% embed canvas height=400 %}
Because the height has default value of 400, we can omit it:
{% embed canvas %}
Test canvas app by drawing something on it:
Conclusion
That’s it.
You can also use the same method to add your own custom apps to this preprocessor. Just clone this repository and add your own app template to the src/assets/templates folder.
Welcome to contribute to this project by adding more apps. If you have any questions or suggestions, feel free to open an issue or pull request on the GitHub repository.
Ignore Embeds
Sometimes you may want preprocessor to ignore some embeds.
You can do it by wrapping content that you want to ignore with below two comments:
<!-- embed ignore begin -->
<!-- embed ignore end -->
For example:
<!-- embed ignore begin -->
{% embed youtube id="DyTCOwB0DVw" loading="lazy" %}
<!-- embed ignore end -->
And youtube embed won’t be rendered.
Global Embedding
Some apps allow you to automatically embed to every chapter. You can do this by modifying book.toml
file to enable them.
For example:
[preprocessor.embedify]
scroll-to-top.enable = true
Attention 💥
When you do this, you don’t need to add
{% embed scroll-to-top %}
manually. It will be automatically added it to every chapter. If you do, it will be rendered twice.
Below is a full list of apps that support global configuration:
[preprocessor.embedify]
custom-templates-folder = "assets/templates"
scroll-to-top.enable = true
footer.enable = true
footer.message = "Copyright © 2025 • Created with ❤️ by [MR-Addict](https://github.com/MR-Addict)"
announcement-banner.enable = true
announcement-banner.id = "0.2.15"
announcement-banner.message = "*Version **0.2.15** now has relased, check it out [here](https://github.com/MR-Addict/mdbook-embedify/releases/tag/0.2.15).*"
giscus.enable = true
giscus.repo = "MR-Addict/mdbook-embedify"
giscus.repo-id = "R_kgDOLCxX0Q"
giscus.category = "General"
giscus.category-id = "DIC_kwDOLCxX0c4CdGx-"
giscus.reactions-enabled = "1"
giscus.theme = "book"
giscus.lang = "en"
giscus.loading = "lazy"
You can see more details about each app at its own page.
Third Party Apps
Third party apps are apps that are hosted on third party sites. Below are all supported third party apps and its detailed options.
Gist
Gist is a simple way to share snippets and pastes with others. All gists are Git repositories, so they are automatically versioned, forkable and usable from Git.
Options
Option | Description | Required | Default |
---|---|---|---|
id | Gist ID | Yes | - - |
Example
{% embed gist id="76cf171d1bdd7da41d4ca96b908eb57a" %}
Vimeo
Vimeo is a video hosting platform that allows you to upload and share videos.
Options
Option | Description | Required | Default |
---|---|---|---|
id | Video ID | Yes | - - |
loading | Supports lazy and eager | No | lazy |
Example
{% embed vimeo id="914391191" loading="lazy" %}
Giscus
Giscus is a comments system powered by GitHub Discussions. Let visitors leave comments and reactions on your website via GitHub! Heavily inspired by utterances.
Options
Option | Description | Required | Default |
---|---|---|---|
repo | Repository | Yes | - - |
repo-id | Repository ID | Yes | - - |
category | Category | Yes | - - |
category-id | Category ID | Yes | - - |
reactions-enabled | Enable reactions | No | 1 |
theme | Supports book, light and dark | No | book |
lang | Localization language | No | en |
loading | Supports lazy and eager | No | lazy |
Example
{% embed giscus repo="MR-Addict/mdbook-embedify" repo-id="R_XXXXXXXXXX" category="General" category-id="DIC_XXXXXXXXXXXXXXXX" theme="book" loading="eager" %}
This book’s giscus is enabled, you can see it at the bottom of this page. And you can also have a try by commenting below.
However, you may want to enable it for the whole book. You can do this by adding below options to book.toml
file after [preprocessor.embedify]
section:
giscus.enable = true
giscus.repo = "MR-Addict/mdbook-embedify"
giscus.repo-id = "R_XXXXXXXXXX"
giscus.category = "General"
giscus.category-id = "DIC_XXXXXXXXXXXXXXXX"
giscus.reactions-enabled = "1"
giscus.theme = "book"
giscus.lang = "en"
giscus.loading = "eager"
Refuse to Connect
Giscus will refuse to connect if you build and preview your book with file:// protocol. The easiest solution is to use some static server so that you can preview your book with http:// protocol.
For exampe:
node.js installed
npx serve book -p 3000
Which will serve your book at http://localhost:3000.
python installed
python -m http.server --directory book 8080
Which will serve your book at http://localhost:8080.
Youtube
YouTube is a popular online video sharing and social media platform.
Options
Option | Description | Required | Default |
---|---|---|---|
id | Video ID | Yes | - - |
loading | Supports lazy and eager | No | lazy |
Example
{% embed youtube id="DyTCOwB0DVw" loading="lazy" %}
Codepen
Codepen is a social development environment for front-end designers and developers. It’s the best place to build and deploy a website, show off your work, build test cases, and find inspiration.
Options
Option | Description | Required | Default |
---|---|---|---|
user | username | Yes | - - |
slug | Project slug | Yes | - - |
height | Iframe height | No | 600 |
theme | Supports light and dark | No | dark |
loading | Supports lazy and eager | No | lazy |
Example
{% embed codepen user="MR-Addict" slug="NWBOqKw" height="600" theme="dark" loading="lazy" %}
Stackblitz
Stackblitz is an instant fullstack web IDE for the JavaScript ecosystem. It’s powered by WebContainers, the first WebAssembly-based operating system which boots the Node.js environment in milliseconds, securely within your browser tab.
Options
Option | Description | Required | Default |
---|---|---|---|
id | Project ID | Yes | - - |
theme | Supports light and dark | No | dark |
loading | Supports lazy and eager | No | lazy |
Example
{% embed stackblitz id="vitejs-vite-y8mdxg" theme="light" loading="lazy" %}
Codesandbox
Codesandbox is an online code editor that allows you to create and share web applications. It is particularly useful for web developers who want to work on React, Vue, Angular, or any other front-end libraries.
Options
Option | Description | Required | Default |
---|---|---|---|
id | Project ID | Yes | - - |
theme | Supports light and dark | No | dark |
loading | Supports lazy and eager | No | lazy |
Example
{% embed codesandbox id="ke8wx" theme="light" loading="lazy" %}
Bilibili
Bilibili is a Chinese video sharing website based in Shanghai, themed around animation, comic, and games (ACG), where users can submit, view, and add commentary subtitles on videos.
Options
Option | Description | Required | Default |
---|---|---|---|
id | Video ID | Yes | - - |
loading | Supports lazy and eager | No | lazy |
Example
{% embed bilibili id="BV1uT4y1P7CX" loading="lazy" %}
Local Apps
Local apps are apps hosted on your local book, so it is not necessary to have internet connection to use them.
Below are all supported local apps and its detailed options.
Footer
The footer app is useful for displaying copyright information, privacy policy, and other legal information. It supports markdown syntax so that you can easily customize the message.
Options
Option | Description | Required | Default |
---|---|---|---|
message | Footer message, markdown supported | Yes | - - |
Example
{% embed footer message="Copyright © 2025 • Created with ❤️ by [MR-Addict](https://github.com/MR-Addict)" %}
This book’s footer is enabled, you can see it at the bottom of this page.
However, you may want to enable it for the whole book. You can do this by adding below options to book.toml
file after [preprocessor.embedify]
section:
footer.enable = true
footer.message = "Copyright © 2025 • Created with ❤️ by [MR-Addict](https://github.com/MR-Addict)"
Include
Attention 💥
Support since v0.2.12.
The include app is for including source file and wrapped it as markdown code block.
The language is automatically detected by the file name extension. You can override it by passing lang
option. The file path should be relative to book root directory.
Options
Option | Description | Required | Default |
---|---|---|---|
file | File to include, relative to book root directory | Yes | - - |
lang | This will override the automatically detected language | No | - - |
range | Range of lines to include, e.g. 1-10 or 1- or -10 | No | - - |
type | Include type, cloud be raw or codeblock | No | codeblock |
Attention 💥
- When
range
is used, it will insert the specified lines starts from 1.- The
raw
type will insert the raw file content into the markdown file directly, while thecodeblock
type will wrap it as a code block.
Example
{% embed include file="src/SUMMARY.md" %}
This will include the src/SUMMARY.md file and wrap it as a markdown code block which is the source code of this book’s summary.
# Summary
# Basics
- [Intro](index.md)
- [Usage](usage.md)
- [More Apps](more-apps.md)
- [Ignore Embeds](ignore-embeds.md)
- [Global Embedding](global-embedding.md)
# Apps
- [Third Party Apps](third-party/index.md)
- [Gist](third-party/gist.md)
- [Vimeo](third-party/vimeo.md)
- [Giscus](third-party/giscus.md)
- [Youtube](third-party/youtube.md)
- [Codepen](third-party/codepen.md)
- [Stackblitz](third-party/stackblitz.md)
- [Codesandbox](third-party/codesandbox.md)
- [Bilibili](third-party/bilibili.md)
- [Local Apps](local/index.md)
- [Footer](local/footer.md)
- [Include](local/include.md)
- [Scroll to Top](local/scroll-to-top.md)
- [Announcement Banner](local/announcement-banner.md)
Scroll to top button
Scroll to top button allows users to quickly smoothly scroll back to the top of the page.
Options
Scroll to top button app has no options.
Example
{% embed scroll-to-top %}
Typically, we want to use it for the whole book. You can do this by adding below options to book.toml
file after [preprocessor.embedify]
section:
scroll-to-top.enable = true
This book uses this option. You can see it at the bottom right corner of this page. But it only shows when pages are long enough to scroll. Or you can see it my another book Notes.
Announcement Banner
Announcement banner allows you put important messages at the top of the page. It supports markdown syntax too.
Options
Option | Description | Required | Default |
---|---|---|---|
id | Announcement id | Yes | - - |
message | Announcement message, markdown supported | Yes | - - |
Example
{% embed announcement-banner id="0.2.15" message="*Version **0.2.15** now has relased, check it out [here](https://github.com/MR-Addict/mdbook-embedify/releases/tag/0.2.15).*" %}
This book’s announcement banner is enabled, you can see it at the top of this page.
However, you may want to enable it for the whole book. You can do this by adding below options to book.toml
file after [preprocessor.embedify]
section:
announcement-banner.enable = true
announcement-banner.id = "0.2.15"
announcement-banner.message = "*Version **0.2.15** now has relased, check it out [here](https://github.com/MR-Addict/mdbook-embedify/releases/tag/0.2.15).*"
Note that announcement banner id must be unique, otherwise it won’t be shown if there is another announcement banner with the same id when user closed it.