Svelte Markdown

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.


Features

  • Free & Open Source
  • Written in TypeScript
  • Extremely Easy to Use
  • Zero-config setup
  • API-Friendly

Core Concepts

  • Custom Components: Simplifies development by supporting import/export of reusable components.
  • Named Layouts: Provides a powerful named layout mechanism to completely customize page design.
  • Unique Entries: Defines specialized, entry-level configuration adapted for all markdown files.
  • Unified Plugins: Enables content transformation using widely-adopted tools like remark and rehype.
  • Global Frontmatter: Streamlines workflow by offering centralized options for markdown metadata.
  • Special Elements: Supports parsing Svelte special elements such as svelte:head etc. in markdown files.
  • Code Highlighter: Offers quick and easy customization for syntax highlighting.

Intro

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.

Docs

The plan is to create online docs soon, so until its published, feel free to ask questions or share feedback in the official Discussions.

Installation

Install @hypernym/svelte-markdown package:

# via pnpm
pnpm add -D @hypernym/svelte-markdown
# via npm
npm install -D @hypernym/svelte-markdown

Usage

Zero-Config Setup

// 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

Custom Config Setup

// 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

Types

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 />

Examples

[!NOTE]

More examples will be added to the online docs.

Playground

Explore the playground to see more details.

Custom Components

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

Named Layouts

---
title: Page Title
layout: default
---

Content...
---
layout: false
---

Content...

Unique Entries

---
title: Page Title
entry: blog
---

Content...
---
entry: false
---

Content...

Special Elements

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

Code Highlighting

Shiki Syntax Highlighter

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 })
    },
  },
})

Plugins

Remark Table of Contents (Toc)

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>

Remark Reading Stats

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'

API

import {
  svelteMarkdown,
  defineConfig,
  compile,
} from '@hypernym/svelte-markdown'

import { escapeSvelte } from '@hypernym/svelte-markdown/utils'

preprocessor

  • Type: function svelteMarkdown(config?: MarkdownConfig): PreprocessorGroup
import { svelteMarkdown } from '@hypernym/svelte-markdown'

svelteMarkdown(config)

defineConfig

  • Type: function defineConfig(config: MarkdownConfig): MarkdownConfig
import { defineConfig } from '@hypernym/svelte-markdown'

defineConfig(config)

compile

  • Type: function compile(source: string, options: CompileOptions): Promise<Processed>
import { compile } from '@hypernym/svelte-markdown'

compile(source, options)

escapeSvelte

  • Type: function escapeSvelte(value: string): string
import { escapeSvelte } from '@hypernym/svelte-markdown/utils'

escapeSvelte(value)

plugins

  • Type: Plugin
import { remarkToc } from '@hypernym/svelte-markdown/plugins'

Types

Package exposes types from the main module path and from the plugins subpath for easier workflow.

main

Imports all types from the main package.

import type {
  ASTScript,
  CompileOptions,
  Entries,
  Entry,
  FileData,
  Frontmatter,
  Highlight,
  Layout,
  Layouts,
  MarkdownConfig,
} from '@hypernym/svelte-markdown'

plugins

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
// ...

Options

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.

extensions

  • Type: string[]
  • Default: ['.md']

Specifies custom file extensions.

svelteMarkdown({
  extensions: ['.md'],
})

preprocessors

  • Type: PreprocessorGroup[]
  • Default: undefined

Specifies a custom list of preprocessors that will be applied to a Svelte file.

svelteMarkdown({
  preprocessors: [vitePreprocess()],
})

plugins

  • Type: { remark: [], rehype: [] }
  • Default: undefined

Specifies the top-level plugins that will be used for all markdown files.

  • Lifecycle: pluginslayout.pluginsentry.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...

layouts

  • Type: Record<string, Layout>
  • Default: 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.

  • Lifecycle: pluginslayout.pluginsentry.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',
    },
  },
})

entries

  • Type: Record<string, Entry>
  • Default: 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.

  • Lifecycle: pluginslayout.pluginsentry.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',
    },
  },
})

frontmatter

  • Type: object
  • Default: undefined

Defines frontmatter custom options.

By default, frontmatter only supports the YAML format, but allows additional customization via parser.

defaults

  • Type: Record<string, unknown>
  • Default: undefined

Specifies frontmatter global data to be applied to all markdown files.

svelteMarkdown({
  frontmatter: {
    defaults: {
      author: 'Hypernym Studio',
    },
  },
})

marker

  • Type: string
  • Default: -

Specifies the start/end symbols for the frontmatter content block.

It only works in combination with the default parser.

svelteMarkdown({
  frontmatter: {
    marker: '+',
  },
})

parser

  • Type: (value: string) => Record<string, unknown> | void
  • Default: undefined

Specifies a custom parser for frontmatter.

Allows adaptation to other formats such as TOML or JSON.

svelteMarkdown({
  frontmatter: {
    parser: (file) => {
      // ...
    },
  },
})

highlight

  • Type: object
  • Default: undefined

Defines custom syntax highlighting options.

highlighter

  • Type: (data: HighlightData) => Promise<string | undefined> | string | undefined
  • Default: undefined

Specifies custom syntax highlighter.

svelteMarkdown({
  highlight: {
    highlighter: async ({ lang, meta, code }) => {
      // ...
      return code
    },
  },
})

specialElements

  • Type: boolean
  • Default: 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,
    },
  },
})

Community

Feel free to ask questions or share new ideas.

Use the official discussions to get involved.

Contribute

Check out the quick guide for more info.

License

Developed in 🇭🇷 Croatia, © Hypernym Studio.

Released under the MIT license.

Top categories

Loading Svelte Themes