Svelte Markdown Preprocessor.
Repository ✦ Package ✦ Releases ✦ Discussions
pnpm add -D @hypernym/svelte-markdown
[!NOTE]
While the API is solid and mostly complete, some changes may still occur before the first stable release.
Ideas, suggestions and code contributions are welcome.
If you find any issues or bugs, please report them so the project can be improved.
import
/export
of reusable components.layout
mechanism to completely customize page design.entry-level
configuration adapted for all markdown files.remark
and rehype
.metadata
.svelte:head
etc. in markdown files.syntax highlighting
.This project was inspired by MDsveX — thanks to its authors for their awesome work!
Svelte Markdown has been completely rewritten to take full advantage of Svelte 5
and its Runes
mode.
It’s a light, simple and powerful preprocessor designed specifically for managing Markdown
content within Svelte
projects.
Also, it comes with zero-config setup, built-in types and a dev-friendly API.
The plan is to create online docs soon, so until its published, feel free to ask questions or share feedback in the official Discussions.
Install @hypernym/svelte-markdown
package:
# via pnpm
pnpm add -D @hypernym/svelte-markdown
# via npm
npm install -D @hypernym/svelte-markdown
// svelte.config.js
import adapter from '@sveltejs/adapter-static'
import { svelteMarkdown } from '@hypernym/svelte-markdown'
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: [svelteMarkdown()],
extensions: ['.svelte', '.md'],
kit: { adapter: adapter() },
}
export default config
// markdown.config.js
import { defineConfig } from '@hypernym/svelte-markdown'
export const markdownConfig = defineConfig({
frontmatter: {
defaults: {
layout: 'default',
author: {
name: 'Hypernym Studio',
url: 'https://github.com/hypernym-studio',
},
// other global data...
},
},
layouts: {
default: {
path: 'lib/content/layouts/default/layout.svelte',
},
blog: {
path: 'lib/content/layouts/blog/layout.svelte',
plugins: {
remark: [],
rehype: [],
},
},
// other layouts...
},
})
Import the config to the svelte.config.js
file:
// svelte.config.js
import adapter from '@sveltejs/adapter-static'
import { svelteMarkdown } from '@hypernym/svelte-markdown'
import { markdownConfig } from './markdown.config.js'
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: [svelteMarkdown(markdownConfig)],
extensions: ['.svelte', '.md'],
kit: { adapter: adapter() },
}
export default config
If you work with TypeScript
and Markdown
components, you can define types to avoid potential issues when importing .md
into .svelte
files.
// src/app.d.ts
declare global {
namespace App {
declare module '*.md' {
import type { Component } from 'svelte'
declare const MarkdownComponent: Component
export default MarkdownComponent
}
}
}
export {}
Now you can import .md
file into .svelte
without type errors:
<!-- +page.svelte -->
<script lang="ts">
import Comp from '$lib/content/components/comp.md'
</script>
<Comp />
[!NOTE]
More examples will be added to the online docs.
Explore the playground to see more details.
---
title: Page Title
---
<script lang="ts">
import { Component } from '$lib/components'
</script>
<Component />
<Component prop="data" />
<Component>
Children content
</Component>
Content...
---
title: Page Title
---
<script lang="ts">
import { Component } from '$lib/components'
</script>
::Component
::Component prop="data"
::Component
Children content
::
Content...
---
title: Page Title
layout: default
---
Content...
---
layout: false
---
Content...
---
title: Page Title
entry: blog
---
Content...
---
entry: false
---
Content...
---
title: About page
description: Svelte Markdown Preprocessor.
layout: false
specialElements: true
---
<svelte:head>
<title>Custom Title - {title}</title>
<meta name="description" content={`Custom Description - ${description}`} />
</svelte:head>
<style>
p {
opacity: 0.6;
font-family: monospace;
font-size: 1.125rem;
}
</style>
{description}
Content...
import { createHighlighter } from 'shiki'
const theme = 'github-dark-default'
const highlighter = await createHighlighter({
themes: [theme],
langs: ['javascript', 'typescript', 'svelte'],
})
svelteMarkdown({
highlight: {
highlighter: async ({ lang, code }) => {
return highlighter.codeToHtml(code, { lang, theme })
},
},
})
import { remarkToc } from '@hypernym/svelte-markdown/plugins'
svelteMarkdown({
plugins: {
remark: [remarkToc],
},
})
Usage in markdown page:
---
title: Blog page
description: Read the latest news.
---
## What's New
## Featured Posts
<ul>
{#each frontmatter.toc as toc}
<li><a href="#{toc.id}">{toc.value}</a></li>
{/each}
</ul>
import { visit, CONTINUE } from 'unist-util-visit'
import readingTime from 'reading-time'
import type { Frontmatter } from '@hypernym/svelte-markdown'
import type { Plugin, Mdast } from '@hypernym/svelte-markdown/plugins'
/**
* Estimates how long an article will take to read.
*/
export const remarkReadingStats: Plugin<[], Mdast.Root> = () => {
return (tree, file) => {
const frontmatter = file.data.frontmatter as Frontmatter
let text = ''
visit(tree, ['text', 'code'], (node) => {
if (node.type !== 'text' && node.type !== 'code') return CONTINUE
text += node.value
frontmatter.readingStats = readingTime(text)
})
}
}
Config:
svelteMarkdown({
plugins: {
remark: [remarkReadingStats],
},
})
Usage in markdown page:
---
title: Page title
---
Reading stats: {JSON.stringify(readingStats)}
# returns an object: { text: '1 min read', minutes: 1, time: 60000, words: 200 }
Reading time: {readingStats.text}
# returns an string: '1 min read'
import {
svelteMarkdown,
defineConfig,
compile,
} from '@hypernym/svelte-markdown'
import { escapeSvelte } from '@hypernym/svelte-markdown/utils'
function svelteMarkdown(config?: MarkdownConfig): PreprocessorGroup
import { svelteMarkdown } from '@hypernym/svelte-markdown'
svelteMarkdown(config)
function defineConfig(config: MarkdownConfig): MarkdownConfig
import { defineConfig } from '@hypernym/svelte-markdown'
defineConfig(config)
function compile(source: string, options: CompileOptions): Promise<Processed>
import { compile } from '@hypernym/svelte-markdown'
compile(source, options)
function escapeSvelte(value: string): string
import { escapeSvelte } from '@hypernym/svelte-markdown/utils'
escapeSvelte(value)
Plugin
import { remarkToc } from '@hypernym/svelte-markdown/plugins'
Package exposes types from the main
module path and from the plugins
subpath for easier workflow.
Imports all types from the main package.
import type {
ASTScript,
CompileOptions,
Entries,
Entry,
FileData,
Frontmatter,
Highlight,
Layout,
Layouts,
MarkdownConfig,
} from '@hypernym/svelte-markdown'
Imports all types from Unified
, VFile
, Mdast
and Hast
as namespaces so there is no need to install additional packages.
Super useful when building custom plugins.
import type {
Plugin,
Plugins,
PluginList,
Unified,
VFile,
Mdast,
Hast,
TocItem,
TocItems,
TocOptions,
} from '@hypernym/svelte-markdown/plugins'
// Unified collective
Unified.Transformer
// ...
// Virtual file
VFile.Options
// ...
// Remark plugins
Mdast.Root
Mdast.Code
// ...
// Rehype plugins
Hast.Root
Hast.Element
// ...
All options are documented with descriptions and examples so autocompletion will be offered as you type. Simply hover over the property and see what it does in the quick info tooltip.
string[]
['.md']
Specifies custom file extensions.
svelteMarkdown({
extensions: ['.md'],
})
PreprocessorGroup[]
undefined
Specifies a custom list of preprocessors that will be applied to a Svelte file.
svelteMarkdown({
preprocessors: [vitePreprocess()],
})
{ remark: [], rehype: [] }
undefined
Specifies the top-level plugins that will be used for all markdown files.
plugins
→ layout.plugins
→ entry.plugins
svelteMarkdown({
plugins: {
remark: [], // Specifies custom `remark` plugins at the top-level (optional).
rehype: [], // Specifies custom `rehype` plugins at the top-level (optional).
},
})
Also, plugins can be disabled at the file-level:
---
title: Page title
plugins:
remark: false # Disables remark plugins for this file only
rehype: false # Disables rehype plugins for this file only
---
Content...
Record<string, Layout>
undefined
Specifies a custom layout records.
Layout component serves as a wrapper for the markdown files, which means the page content is displayed via the component's children prop.
plugins
→ layout.plugins
→ entry.plugins
svelteMarkdown({
layouts: {
default: {
path: 'lib/content/layouts/default/layout.svelte', // Specifies the path to the layout file (required).
plugins: {
remark: [], // Specifies custom `remark` plugins at the layout-level (optional).
rehype: [], // Specifies custom `rehype` plugins at the layout-level (optional).
},
},
blog: {
path: 'lib/content/layouts/blog/layout.svelte',
},
},
})
Can be enabled at the top-level (via config) or at the file-level (via frontmatter).
File-level
---
title: Page title
layout: blog
---
Content...
Also, layout plugins can be disabled at the file-level:
---
title: Page title
layout:
name: blog
plugins:
remark: false # Disables remark layout plugins for this file only
rehype: false # Disables rehype layout plugins for this file only
---
Content...
Config
svelteMarkdown({
frontmatter: {
defaults: {
layout: 'default',
},
},
})
Record<string, Entry>
undefined
Specifies a custom entry records.
Entry serves as a special configuration for markdown files, which means it is similar to layout but without the need to create a custom component file.
Allows unique and straightforward customization for an individual markdown file. An entry can be a page or a component.
plugins
→ layout.plugins
→ entry.plugins
svelteMarkdown({
entries: {
blog: {
plugins: {
remark: [], // Specifies custom `remark` plugins at the entry-level (optional).
rehype: [], // Specifies custom `rehype` plugins at the entry-level (optional).
},
},
},
})
Can be enabled at the top-level (via config) or at the file-level (via frontmatter).
File-level
---
title: Page title
entry: blog
---
Content...
Also, entry plugins can be disabled at the file-level:
---
title: Page title
entry:
name: blog
plugins:
remark: false # Disables remark entry plugins for this file only
rehype: false # Disables rehype entry plugins for this file only
---
Content...
Config
svelteMarkdown({
frontmatter: {
defaults: {
entry: 'default',
},
},
})
object
undefined
Defines frontmatter custom options.
By default, frontmatter only supports the YAML
format, but allows additional customization via parser.
Record<string, unknown>
undefined
Specifies frontmatter global data to be applied to all markdown files.
svelteMarkdown({
frontmatter: {
defaults: {
author: 'Hypernym Studio',
},
},
})
string
-
Specifies the start/end symbols for the frontmatter content block.
It only works in combination with the default parser.
svelteMarkdown({
frontmatter: {
marker: '+',
},
})
(value: string) => Record<string, unknown> | void
undefined
Specifies a custom parser for frontmatter.
Allows adaptation to other formats such as TOML
or JSON
.
svelteMarkdown({
frontmatter: {
parser: (file) => {
// ...
},
},
})
object
undefined
Defines custom syntax highlighting options.
(data: HighlightData) => Promise<string | undefined> | string | undefined
undefined
Specifies custom syntax highlighter.
svelteMarkdown({
highlight: {
highlighter: async ({ lang, meta, code }) => {
// ...
return code
},
},
})
boolean
undefined
Specifies support for parsing Svelte special
elements such as svelte:head
etc. in markdown files.
Can be enabled at the top-level (via config) or at the file-level (via frontmatter).
If you don't plan to use them in every markdown file, it is recommended to enable the option only on those pages where you really need it.
File-level
---
title: Page title
specialElements: true
---
<svelte:head>...</svelte:head>
Content...
Config
svelteMarkdown({
frontmatter: {
defaults: {
specialElements: true,
},
},
})
Feel free to ask questions or share new ideas.
Use the official discussions to get involved.
Check out the quick guide for more info.
Developed in 🇭🇷 Croatia, © Hypernym Studio.
Released under the MIT license.