Embed media components using MDX and SvelteKit

Matías Hernández

In this article, you'll learn how to effectively use this new authoring media to embed rich media components into your SvelteKit site while keeping the simplicity of markdown format for writing your content.

To accomplish this goal, through the article you'll learn:

  • What is MDX and MDsveX
  • How to set up an SvelteKit app
  • How yo create your own media components to use in the markdown

At the end, you'll be able to add custom media elements to your markdown files like:

  • Embeded youtube videos
  • CodeSandbox
  • Twitter items
  • Snack
  • Custom components

You can find An example of what MDsveX can achieve directly in the playground

You can find the result of this tutorial in this github repository or play around in the following sandbox.

What is MDX and MDsveX?

MDX is a combination of Markdown and JSX

An authoring format that allows the writer to write the well-known markdown format but also introduce dynamic content by using the power of JSX components

It takes a string of markdown and transforms it to a Javascript string (React or Svelte)

It basically allows you to consider a regular markdown file as a component opening the door to add working code into the markdown file.

The MDX parser will insert your custom components into the parsed result, you can even replace base elements like paragraphs, headings, and others HTML elements.

This allows you to have a better authoring experience and to create all of your content using your lovely markdown, but also empowering your content with rich media elements like interactive charts, alerts, or embed external content like youtube videos, images, etc.

MDsveX is the answer from Svelte world to the same need of authoring rich media content using a powerful media like markdown. This is a markdown pre-processor for Svelte components, this preprocessor allows you to use Svelte components in your markdown, or vice versa. It supports all Svelte syntax and almost all Markdown

It uses remark and rehype so you can use several plugins to enhance your experience.

Get up running with SvelteKit

SvelteKit is a framework on top of Svelte, at the time of this writing is still in beta and under active development. This framework can be seen as the Next.js version for Svelte, full of features and opinions about routing, layout, state management, API endpoints, SSG, and SSR.

SvelteKit allows you to build full-fledged applications with an outstanding developer experience and an incredibly fast user experience coming from the Svelte philosophy.

One way to try Sveltekit is by building a website to share content like a blog or articles site, this is the easier way to accomplish two things:

  • Get to know SvelteKit
  • Integrate MDsveX to the mix.

Let's start by setting up your SvelteKit powered site

1npm init svelte@next my-site
2cd my-site
3npm install

Add an image here

Then to add some style will add tailwind

1npx svelte-add tailwindcss

and similar to add MDsveX

1npx svelte-add mdsvex

With that, you'll have the base setup for a SvelteKit site that will use Tailwindcss and MDsveX

1├── README.md
2├── mdsvex.config.cjs
3├── mdsvex.config.js
4├── package.json
5├── postcss.config.cjs
6├── src
7│ ├── app.html
8│ ├── app.postcss
9│ ├── global.d.ts
10│ ├── lib
11│ └── routes
12│ └── index.svelte
13├── svelte.config.js
14├── tailwind.config.cjs
15└── tsconfig.json

Let's say that you want to have a list of blog posts and you love markdown to write those

Since SvelteKit uses a file-based routing system you can leverage this and quickly set up your markdown files (or .svx) inside src/routes everything under that folder will be considered as a page.

1├── src
2│ └── routes
3│ ├── blog
4│ │ └── my-first-post.svx
5│ └── index.svelte

This means that there is a new page under https://your-site.com/blog/my-first-post

You can try this right away by running npm run dev and visiting http://localhost:3000/blog/my-first-post

Markdown Content

The markdown file will support frontmatter, this content will act as metadata variables that can be used by the svelte components.

1---
2title: Svex up your markdown
3description: My first markdown content
4keywords:
5 - Keyword 1
6 - Keyword 2
7---
8
9# { title }
10
11This is more markdown **base content**

To enhance the experience further, mdsvex allow the use of custom layouts

Layouts

SvelteKit supports the concept of Layouts, this is a special component that can wrap every page under it, this is very useful to create shared elements that should be visible across every page.

To create a layout component you just need to create a file named as __layout.svelte

Upon creation of your site code, there is a base layout component under src/routes/__layout.svelte

A similar concept exists for mdsvex files. There is a configuration option that allows you to provide a custom layout component that will wrap your mdsvex file like

1<Layout {...props}>
2 <YourMarkdownContent />
3</Layout>

This layout components will receive all the frontmatter data of your files as props

To configure this layout you can update the mdsvez.config.js file and pass a string that represents the path to your layout component

1mdsvex({
2 layout: join(__dirname, './src/components/PostLayout.svelte')
3})

There are cases where you have different markdown based pages with completely different content, in this cases would be useful to have different layouts for the content, that can be addressed by passing an object to the layout property where each key of the object is considered as the name of the layout.

1mdsvex({
2 layout: {
3 blog: "./path/to/blog/layout.svelte",
4 article: "./path/to/article/layout.svelte",
5 _: "./path/to/fallback/layout.svelte" // Default layout
6 }
7});

By using this configuration you can decide with the layout should be used to which file by declaring it in the frontmatter section of the file, but if that option is not present, MDsveX will try to pick the correct layout based on your folder structured.

Let's create a layout for your current post files, just create a new folder and file under src/lib/components/PostLayout.svelte

In this example, components are saved under the lib folder to make it easy to access them. SvelteKit comes with some default paths as $lib that make reference to the src/lib folder.

This component can look something like this

1<script>
2 export let title;
3 export let description;
4 export let keywords;
5</script>
6
7<svelte:head>
8 <title>{title}</title>
9 <meta name="description" content={description} />
10 <meta name="keywords" content={keywords.join(', ')} />
11</svelte:head>
12
13<article class="w-full p-8">
14 <header>
15 <h1>{title}</h1>
16 </header>
17 <main>
18 <slot />
19 </main>
20 <footer>
21 <a href="">Share this on Twitter</a>
22 </footer>
23
24</article>

at the very top, you can see a few variables exposed as props inside the script tag, the value for these props come directly from the markdown front matter.

Check [this page under the svelte documentation](https://svelte.dev/docs#script https://svelte.dev/docs#script) to learn more about props

Then, in the component code you can see a reference to svelte:head, this is a svelte element that makes it possible to manage the content of the document.head. By using it, all the content you add as a child of svelte:head gets inserted in the <head /> of the current document. This way you can handle some of the SEO content for your article.

Then, is the actual post layout that in this case consists just of a few HTML elements. You can see that this component reference the title prop to render the title. And use another svelte especial element <slot />. This is a way to define child components, meaning that the "user" of this component can insert any element into this place. This slot is used by MDsveX to insert the parsed markdown content.

This way you can control the way your markdown content is rendered, if you want to have more granular control over how the HTML elements inside the slot are created, then you can use what is known as custom components.

Custom components

Under MDsveX, custom components are a way to replace the elements that markdown would normally generate, for example, the example markdown content used here

1---
2title: Svex up your markdown
3description: My first markdown content
4keywords:
5 - Keyword 1
6 - Keyword 2
7---
8
9# { title }
10
11This is more markdown **base content**

Will be compiled to

1<h1>title</h1>
2
3<p>This is more markdown <strong>base content</strong></p>

By using custom components in your layout you can replace these elements by more complex constructions, like a custom blockquote section.

Let's create the custom blockquote first, this is basically a blockquote element that, when hover, will show a tweet button.

You can check the source code and demo of the component directly in svelte REPL

Save this component under src/lib/components/Quote.svelte and now, let's use it as a custom component.

In your PostLayout component, add a new script tag. Custom components are defined in the "compilation" step, so to be able to use them you need to import them inside a context="module" script and export it.

The export of this custom component has to be a named export each named expoer must be named after the actual element you want to replace,

1<script context="module">
2 import blockquote from '$lib/components/Quote.svelte'
3 export { blockquote }
4</script>

Now, after compilation, MDsveX will generate new content using your custom Quote component each time a blockqote is required.

Media elements

Now that the setup part is done, is time to harness the power of MDsveX by adding media elements to your markdown content.

Let's start by adding some video content, for that, you'll create a new component under src/lib/Youtube.svelte

1<script>
2 export let videoId
3</script>
4
5<div class="video-container">
6 <iframe title="Youtube video" src={`https://www.youtube.com/embed/${videoId}`} frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
7</div>
8
9<style>
10.video-container {
11 overflow: hidden;
12 position: relative;
13 width:100%;
14}
15
16.video-container::after {
17 padding-top: 56.25%;
18 display: block;
19 content: '';
20}
21
22.video-container iframe {
23 position: absolute;
24 top: 0;
25 left: 0;
26 width: 100%;
27 height: 100%;
28}
29</style>

This component allows you to embed a youtube video by just passing the video identifier as a prop under videoId.

Now, in your markdown file, you can do

1---
2title: Svex up your markdown
3description: My first markdown content
4keywords:
5 - Keyword 1
6 - Keyword 2
7---
8
9<script>
10import Youtube from '$lib/components/Youtube.svelte'
11</script>
12
13# { title }
14
15This is more markdown **base content**
16
17<Youtube videoId="AdNJ3fydeao" />

Just like another svelte component, now you can import components into the markdown and render it!. MDsveX will take care of parse and transform the entire page and correctly render the content.

Take a look at the result by running (in case you don't have it running) npm run dev and visiting http://localhost:3000/blog/my-first-post

Now you'll see the video content directly in your post.

Now, let's create another component to embed codeSandbox examples.

1<script>
2 export let sandboxId
3</script>
4
5<iframe
6 data-testid="codesandbox"
7 title={`codeSandbox-${sandboxId}`}
8 src={`https://codesandbox.io/embed/${sandboxId}`}
9 allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb"
10 sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"
11/>
12
13<style>
14 iframe {
15 width: 100%;
16 height: 500px;
17 border: 0;
18 border-radius: 4px;
19 overflow: hidden;
20 }
21</style>

As you can see, the process to create these embed media components is fairly simple with Svelte, just add the corresponding iframe element and the required custom properties, in this case, the prop sandboxId.

Now, in your markdown content, you can use it like

1---
2title: Svex up your markdown
3description: My first markdown content
4keywords:
5 - Keyword 1
6 - Keyword 2
7---
8
9<script>
10import Youtube from '$lib/components/Youtube.svelte'
11import CodeSandbox from '$lib/components/CodeSandbox.svelte'
12</script>
13
14# { title }
15
16This is more markdown **base content**
17
18<Youtube videoId="AdNJ3fydeao" />
19
20<CodeSandbox sandboxId="j0y0vpz59"/>

Check this sandbox to review how this component works

Now let's create a Tweet component to embed a particular tweet into the markdown content.

This is a bit different the previous one since it requires loading an external script, but svelte give you simple tools to accomplish this.

1<script>
2 import { onMount } from "svelte";
3 let mounted = false;
4 export let id;
5
6 onMount(() => {
7 // The payment-form is ready.
8 mounted = true;
9 });
10
11 function scriptLoaded() {
12 if (mounted) {
13 var tweet = document.getElementById("tweet");
14 window.twttr.widgets
15 .createTweet(id, tweet, {
16 conversation: "none", // or all
17 cards: "hidden", // or visible
18 linkColor: "#cc0000", // default is blue
19 theme: "light" // or dark
20 });
21 }
22 }
23</script>
24
25<svelte:head>
26 <script src="https://platform.twitter.com/widgets.js" on:load={scriptLoaded}></script>
27</svelte:head>
28
29
30<div id="tweet" />

First thing is to use the svelte:head component to add a script tag into the document head.

This script tag point to the Twitter widget library and triggers a callback function when is loaded by using the on:load event.

Then, in the script tag of this component, you can see that the callback function scriptLoaded function check if the component is already mounted and then run the twitter set up to show the tweet.

To identify what tweet you want to render, just use the prop id

This way you have all you need to add external media components into your markdown files powered by MDsveX.

Create a list of markdown files

Something worth mentioning when working with MDsveX is about how to create a page to list your markdown files or posts, to do this you'll use the power of endpoint routes and glob import feature from Vite.

First, let's create an API endpoint to gather the content; create a file under src/routes/api/blog/index.json.ts

Inside that file, create and export a get function, this function will load, parse and return a JSON object with the list of your files.

1async function getPosts() {
2 const modules = import.meta.glob(`../routes/blog/post/*.svx`); // load all the svx files
3
4 const postPromises = [];
5 for (const [path, resolver] of Object.entries(modules)) {
6 const promise = resolver().then((post) => {
7 const slug = path.match(/([\w-]+)\.(svelte\.svx)/i)?.[1] ?? null; // Create the slug
8 return {
9 slug,
10 ...post.metadata
11 };
12 });
13 postPromises.push(promise);
14 }
15
16 const posts = await Promise.all(postPromises);
17 return posts
18}
19
20
21export async function get() {
22 const posts = await getPosts();
23 return {
24 body: {
25 posts
26 }
27 };
28}

The key here is the import.meta.glob that will retrieve all the files that match the glob, in this case, that ends with the .svx extension

Now, create the page to list this files under src/routes/blog/index.svelte

This svelte component will use a loader function to fetch the data from the previous API endpoint and then render that in the screen.

1<script lang="ts" context="module">
2 export async function load({ fetch }) {
3 const url = '/api/blog.json';
4 const res = await fetch(url);
5 if (res.ok) {
6 const { posts } = await res.json();
7 return {
8 props: {
9 posts
10 }
11 };
12 }
13
14 return {
15 status: res.status,
16 error: new Error(`Could not load ${url}`)
17 };
18 }
19</script>
20
21<script >
22 export let posts; // The prop value comes from the module above
23</script>
24
25<ul>
26{#each posts as post}
27<li>
28 <a href={`/blog/posts/${post.slug}`}>
29 {post.title}
30 </a>
31</li>
32{/each}
33</ul>

This component will just get the data returned by the API endpoint and create a list of titles from your .svx files.

Conclusion

Use markdown is an easy and simple way to create your content but it lacks interactivity and dynamism, that piece can be added by using the power of pre-processors and AST that enable the existence of tools like MDsveX.

MDsveX lets you use the full power of Svelte components from inside your markdown content adding that dynamic layer.

Anything that you can think and build as a Svelte component can be used inside MDsveX files.

Matías Hernández

Senior Frontend Engineer

Matías is a Chilean Software Engineer, father, host of two podcasts and egghead, and Escuela Frontend, instructor.

He focuses on front-end development and shares what he knows and learns with the community through articles and video lessons tailored to the Spanish community.

Matías host two podcasts: Café con Tech and Control Remoto, and write for different tech publications.

You can always reach him on twitter