Import Markdown content anywhere in your Vite app
Short on time? Skip to the code snippet you came here for.
Vite is a build tool created by Evan You that offers a really great developer experience.
If you're using Vite with Vue, its default configuration allows you to import .js
, .ts
, .css
, .json
, and .vue
files anywhere in your app.
You can extend that configuration to support any other type of file. In this article, I'll explore how you can load Markdown files as if they were Vue single file components, and why you would want to do so.
Markdown support in Vuepress, Nuxt Content, and Vitepress is awesome 🔥
I've long been an admirer of Vuepress's developer experience—you can write content and Vue components in a Markdown file, and Vuepress will do the following:
- Load your Markdown files during the build process using Webpack
- Use
markdown-it
plus a host of custom plugins to parse your markdown and render it as HTML, leaving Vue components intact - Wrap that HTML in a
template
tag, so that it becomes a valid Vue Single File Component - Pass your component to Vue Loader, where it gets transformed into an ES module by
vue-template-compiler
(for Vue 2) or@vue/compiler-sfc
(for Vue 3). - Import that module into a dedicated page of your static site1
Some of those steps are pretty complex, but under the hood, it's just a bunch of functions that each transform a string into a different string.
Two newer tools from the Vue ecosystem are Nuxt Content and Vitepress, which follow similar workflows to deliver the same writing experience.
Vuepress, Vitepress, and Nuxt Content are all pre-configured with awesome, full-featured Markdown-to-Vue pipelines that meet tons of different needs you have when building a blog or a documentation site.
But, if you're looking for something simpler or more flexible, they're not quite the right solution!
I often need something less opinionated.
While working on my projects, including this blog, I've found myself needing more flexibility, more modularity, and less of the opinions and infrastructure that come with Vuepress, Nuxt Content, and Vitepress.
I prefer:
- Having full control over my Markdown renderer, so I can start with something extremely simple, and work my way up to plugin-heavy configurations as needed
- Having the freedom to swap
markdown-it
(used by Vuepress and Vitepress) for something likeremark
(used by Nuxt Content) at any time, for any reason - Writing Vue components in my Markdown anytime, anywhere, and any way I want
- Freely configuring this functionality into any Vite-based site or app, with minimal effort and no impact on my folder structure.
Here's a concrete example: in an app I'm working on, the landing page includes a sales pitch, which I wrote as a short Markdown article.
I want to keep that pitch in a separate .md
file, and include custom Vue components in that file, as well as some more standard stuff like Vue Router's RouterLink
component.
So, I need support for the very basic features of the Vuepress/Vitepress/Nuxt Content workflow, but there are so many other things they offer that I don't need:
- Table of contents for any headings I use in the pitch
- Syntax highlighting
- Hoisted script and style tags
- Page data, e.g. last-edit timestamps and git repo links that I can display in my article
- Build tools that scan my directories for Markdown files and create separate, static HTML pages for them
My ideal developer experience for this use case looks something like this:
// sales-pitch.md
# You should totally buy this thing
It has cool features:
<ImageCarousel />
It will make your life better 🎉
<RouterLink to="/sign-up">Sign up!</RouterLink>
// LandingPage.vue
<template>
<!-- Start with some eye-catching content and a call to action -->
<header>
<h1>My Special App</h1>
<p>You need this app!</p>
<a class="button" href="/sign-up">Sign up</a>
</header>
<!-- Then, bring in the sales pitch -->
<main>
<article>
<!--
All my Markdown would render here, and Vue components
in my Markdown would be fully working.
-->
<SalesPitch />
</article>
</main>
</template>
<script>
// Import my sales pitch from its Markdown file, as if it were a Vue component
import SalesPitch from 'path/to/sales-pitch.md'
export default {
// Register the component so I can use it in my template
components: {
SalesPitch
},
}
</script>
In my opinion, this workflow looks super comfortable and flexible, allowing me to sprinkle superpowered Markdown anywhere in my app.
So let's make it happen!
Vite's config file is where the magic happens!
A vite.config.js
file at the root of our Vite project allows us to customize the way Vite processes our code, both in development and during production bundling.
If you've worked with Nuxt, Vuepress, or Vue CLI, you might be familiar with the basics of build configuration, but Vite has one important difference you should be aware of: production and development builds are managed by separate tools. Vite uses Koa to serve our app during development and Rollup to bundle it for production.
So, instead of configuring Webpack once, and expecting that configuration to work both in development and production, we'll need to configure the Koa server and Rollup bundler separately.
Specifically, we need to use viteConfig.configureServer
to add a middleware to the Koa server, and viteConfig.rollupInputOptions.plugins
to add a plugin to the Rollup production build. We also need to use viteConfig.rollupPluginVueOptions
to tweak the way Rollup handles Vue components.
// vite.config.js
module.exports = {
configureServer: [
// We'll do some work in here
],
rollupInputOptions: {
plugins: [
// And in here
]
},
rollupPluginVueOptions: {
// And make a quick change here
},
}
Identify concerns, and separate them into tasks.
Right off the bat, it's important that we identify and separate our concerns. Here are the three separate tasks I see:
- Write a function that transforms a string of Markdown into a valid Vue component
- Write a Rollup plugin that applies our function to the contents of
.md
files - Write a Koa server middleware that not only applies our function to the contents of
.md
files, but also watches.md
files for changes, and reapplies that same function when the file is changed while the development server is running.
Good news: the Baleada toolkit has two tools that can help us accomplish the Rollup and Koa tasks!
The first is Baleada Source Transform. The Rollup-specific version of that tool wraps up all the boilerplate code you need to get a generic Rollup transform plugin up and running.
The second is actually built specifically for Vite: Baleada Vite Serve as Vue. This tool wraps up the boilerplate code for a Koa middleware that can serve any file as a Vue component, watch that file for changes, and hot reload your Vue component when a change is detected.
Install them both as dev dependencies in your project:
npm i @baleada/rollup-plugin-source-transform @baleada/vite-serve-as-vue --save-dev
Later on, you can visit the docs for those tools to get full details on how to use them, but for now, let's just cut to the chase: the code below shows how to use these tools in vite.config.js
to process Markdown files as if they were Vue components.
The code snippet you came here for 🍰
// vite.config.js
// In this example, I'll use markdown-it to process my markdown
const MarkdownIt = require('markdown-it'),
md = new MarkdownIt({
html: true // Allow HTML so that your Vue components don't get stripped out
})
// Define a function that transforms our Markdown into a Vue component.
// Our Rollup and Koa middlewares will both call this function, both passing
// an object with a `source` property that exposes the Markdown file's
// contents.
function markdownToVue ({ source }) {
// markdown-it returns valid HTML. To transform that into a valid Vue component,
// just wrap the HTML (including inline Vue components) in a <template> tag.
return `<template>${md.render(source)}</template>`
}
// Import the getServeAsVue higher order function
const getServeAsVue = require('@baleada/vite-serve-as-vue')
// Call getServeAsVue, passing a couple of required options.
// The return value is your Koa middleware.
const serveAsVue = getServeAsVue({
// Pass our markdown-it function as the `toVue` param.
//
// The Koa middleware will call this function to transform our files
// to Vue components, both during the first development build
// and every time your Markdown file changes.
toVue: markdownToVue,
// Tell the Koa middleware to apply that logic to any .md file
// in any directory
include: '**/*.md',
})
// Import the sourceTransform function. This function
// returns an object that Rollup recognizes as a plugin.
const sourceTransform = require('@baleada/rollup-plugin-source-transform')
// Call sourceTransform, passing a couple of required options.
const sourceTransformMarkdownToVue = sourceTransform({
transform: markdownToVue,
// Tell the Rollup plugin to apply that logic to any .md file
// in any directory
include: '**/*.md',
})
module.exports = {
configureServer: [
// Include our shiny new Koa middleware in the development build 🎉
serveAsVue,
],
rollupInputOptions: {
plugins: [
sourceTransformMarkdownToVue,
]
},
rollupPluginVueOptions: {
// Note: Rollup plugins execute in order, and internally, Vite
// adds our plugin from viteConfig.rollupInputOptions.plugins
// *before* adding rollup-plugin-vue.
//
// So, our plugin will run first during production build, and
// we can be confident that rollup-plugin-vue will receive the
// results of our markdownToVue function, *not* the raw contents
// of our Markdown files.
include: [
// rollup-plugin-vue will only process Vue files by default.
// We can use the '**/*.md' pattern to tell the plugin
// to process our Markdown files as well.
'**/*.vue',
'**/*.md',
],
},
}