Mdbook Embedify

Crates.io Crates.io

This is a mdbook preprocessor plugin that allows you to embed apps to your book, like youtube, codepen, giscus and many other apps.

Copyright © 2025 • Created with ❤️ by MR-Addict

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.

Copyright © 2025 • Created with ❤️ by MR-Addict

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 and script 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.

Copyright © 2025 • Created with ❤️ by MR-Addict

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.

Copyright © 2025 • Created with ❤️ by MR-Addict

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.

Copyright © 2025 • Created with ❤️ by MR-Addict

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.

Copyright © 2025 • Created with ❤️ by MR-Addict

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

OptionDescriptionRequiredDefault
idGist IDYes- -

Example

{% embed gist id="76cf171d1bdd7da41d4ca96b908eb57a" %}

Copyright © 2025 • Created with ❤️ by MR-Addict

Vimeo

Vimeo is a video hosting platform that allows you to upload and share videos.

Options

OptionDescriptionRequiredDefault
idVideo IDYes- -
loadingSupports lazy and eagerNolazy

Example

{% embed vimeo id="914391191" loading="lazy" %}

Copyright © 2025 • Created with ❤️ by MR-Addict

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

OptionDescriptionRequiredDefault
repoRepositoryYes- -
repo-idRepository IDYes- -
categoryCategoryYes- -
category-idCategory IDYes- -
reactions-enabledEnable reactionsNo1
themeSupports book, light and darkNobook
langLocalization languageNoen
loadingSupports lazy and eagerNolazy

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.

Copyright © 2025 • Created with ❤️ by MR-Addict

Youtube

YouTube is a popular online video sharing and social media platform.

Options

OptionDescriptionRequiredDefault
idVideo IDYes- -
loadingSupports lazy and eagerNolazy

Example

{% embed youtube id="DyTCOwB0DVw" loading="lazy" %}

Copyright © 2025 • Created with ❤️ by MR-Addict

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

OptionDescriptionRequiredDefault
userusernameYes- -
slugProject slugYes- -
heightIframe heightNo600
themeSupports light and darkNodark
loadingSupports lazy and eagerNolazy

Example

{% embed codepen user="MR-Addict" slug="NWBOqKw" height="600" theme="dark" loading="lazy" %}

Copyright © 2025 • Created with ❤️ by MR-Addict

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

OptionDescriptionRequiredDefault
idProject IDYes- -
themeSupports light and darkNodark
loadingSupports lazy and eagerNolazy

Example

{% embed stackblitz id="vitejs-vite-y8mdxg" theme="light" loading="lazy" %}

Copyright © 2025 • Created with ❤️ by MR-Addict

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

OptionDescriptionRequiredDefault
idProject IDYes- -
themeSupports light and darkNodark
loadingSupports lazy and eagerNolazy

Example

{% embed codesandbox id="ke8wx" theme="light" loading="lazy" %}

Copyright © 2025 • Created with ❤️ by MR-Addict

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

OptionDescriptionRequiredDefault
idVideo IDYes- -
loadingSupports lazy and eagerNolazy

Example

{% embed bilibili id="BV1uT4y1P7CX" loading="lazy" %}

Copyright © 2025 • Created with ❤️ by MR-Addict

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.

Copyright © 2025 • Created with ❤️ by MR-Addict

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

OptionDescriptionRequiredDefault
messageFooter message, markdown supportedYes- -

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)"

Copyright © 2025 • Created with ❤️ by 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

OptionDescriptionRequiredDefault
fileFile to include, relative to book root directoryYes- -
langThis will override the automatically detected languageNo- -
rangeRange of lines to include, e.g. 1-10 or 1- or -10No- -
typeInclude type, cloud be raw or codeblockNocodeblock

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 the codeblock 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)

Copyright © 2025 • Created with ❤️ by MR-Addict

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.

Copyright © 2025 • Created with ❤️ by MR-Addict

Announcement Banner

Announcement banner allows you put important messages at the top of the page. It supports markdown syntax too.

Options

OptionDescriptionRequiredDefault
idAnnouncement idYes- -
messageAnnouncement message, markdown supportedYes- -

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.

Copyright © 2025 • Created with ❤️ by MR-Addict