Markdown
Import markdown files directly in HTML templates with your custom processing pipeline or presets.
Extracts the table of contents, frontmatter, excerpt and title.
Warning
This API is subject to changes
Installation
npm i @gracile/markdown
# Presets
npm i @gracile/markdown-preset-marked
Tip
You can use this extension with any Vite+Lit setup!
It’s totally decoupled from the framework.
📄 /vite.config.ts
import { defineConfigfunction defineConfig(config: UserConfig): UserConfig (+3 overloads)
Type helper to make it easier to use vite.config.ts
accepts a direct
{@link
UserConfig
}
object, or a function that returns it.
The function receives a
{@link
ConfigEnv
}
object.
} from 'vite';
import { viteMarkdownPluginfunction viteMarkdownPlugin(options?: {
MarkdownRenderer: typeof MarkdownDocumentRendererEmpty;
}): any[]
} from '@gracile/markdown/vite';
import { MarkdownRendererclass MarkdownRenderer
} from '@gracile/markdown-preset-marked';
export default defineConfigfunction defineConfig(config: UserConfig): UserConfig (+3 overloads)
Type helper to make it easier to use vite.config.ts
accepts a direct
{@link
UserConfig
}
object, or a function that returns it.
The function receives a
{@link
ConfigEnv
}
object.
({
// ...
pluginsUserConfig.plugins?: PluginOption[] | undefined
Array of vite plugins to use.
: [
viteMarkdownPluginfunction viteMarkdownPlugin(options?: {
MarkdownRenderer: typeof MarkdownDocumentRendererEmpty;
} | undefined): any[]
({ MarkdownRenderertype MarkdownRenderer: typeof MarkdownDocumentRendererEmpty
}),
// ...
],
});
Usage
📄 /src/modules/my-partial.ts
import { htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>
Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html
tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
} from 'lit';
import myDocumentconst myDocument: MarkdownModule
from './my-document.md';
export const myPartialconst myPartial: TemplateResult<1>
= htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>
Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html
tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
`
<article>
<h1>${myDocumentconst myDocument: MarkdownModule
.metameta: {
slug: string;
title: string;
frontmatter: Record<string, unknown>;
tableOfContents: TocLevel[];
tableOfContentsFlat: Heading[];
}
.titletitle: string
}</h1>
<div class="content">${myDocumentconst myDocument: MarkdownModule
.bodybody: {
html: string;
lit: TemplateResult;
}
.litlit: TemplateResult
}</div>
</article>
<details>
<div>${myDocumentconst myDocument: MarkdownModule
.excerptexcerpt: {
html: string;
lit: TemplateResult;
text: string;
}
.litlit: TemplateResult
}</div>
<pre>${JSONvar JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.
.stringifyJSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)
Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
(myDocumentconst myDocument: MarkdownModule
.metameta: {
slug: string;
title: string;
frontmatter: Record<string, unknown>;
tableOfContents: TocLevel[];
tableOfContentsFlat: Heading[];
}
.tableOfContentstableOfContents: TocLevel[]
)}</pre>
<pre>${JSONvar JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.
.stringifyJSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)
Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
(myDocumentconst myDocument: MarkdownModule
.metameta: {
slug: string;
title: string;
frontmatter: Record<string, unknown>;
tableOfContents: TocLevel[];
tableOfContentsFlat: Heading[];
}
.frontmatterfrontmatter: Record<string, unknown>
)}</pre>
<pre>${JSONvar JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.
.stringifyJSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)
Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
(myDocumentconst myDocument: MarkdownModule
.sourcesource: {
file: string;
markdown: string;
yaml: string;
}
.yamlyaml: string
)}</pre>
<pre>${JSONvar JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.
.stringifyJSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)
Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
(myDocumentconst myDocument: MarkdownModule
.pathpath: {
absolute: string;
relative: string;
}
.relativerelative: string
)}</pre>
</details>
`;
📄 /src/types.ts
You can use these types to flesh out yours. Hover to see the full signatures.
import type {
MarkdownModuletype MarkdownModule = {
path: {
absolute: string;
relative: string;
};
body: {
html: string;
lit: TemplateResult;
};
excerpt: {
html: string;
lit: TemplateResult;
text: string;
};
meta: {
slug: string;
title: string;
frontmatter: Record<string, unknown>;
tableOfContents: TocLevel[];
tableOfContentsFlat: Heading[];
};
source: {
file: string;
markdown: string;
yaml: string;
};
}
,
TocLeveltype TocLevel = {
text: string;
depth: number;
slug: string;
children: TocLevel[];
}
,
Headingtype Heading = {
text: string;
depth: number;
slug: string;
}
,
} from '@gracile/markdown/module';
// ...
With Vite’s glob import.
📄 /src/content/content.ts
Firstly, create the holder for your Markdown documents
import type { MarkdownModuletype MarkdownModule = {
path: {
absolute: string;
relative: string;
};
body: {
html: string;
lit: TemplateResult;
};
excerpt: {
html: string;
lit: TemplateResult;
text: string;
};
meta: {
slug: string;
title: string;
frontmatter: Record<string, unknown>;
tableOfContents: TocLevel[];
tableOfContentsFlat: Heading[];
};
source: {
file: string;
markdown: string;
yaml: string;
};
}
} from '@gracile/markdown/module';
export const blogPostsconst blogPosts: Record<string, MarkdownModule>
= import.meta.globImportMeta.glob: ImportGlobFunction
<MarkdownModule>(glob: string | string[], options: ImportGlobOptions<true, string>) => Record<string, MarkdownModule> (+2 overloads)
Import a list of files with a glob pattern.
Overload 3: Module generic provided, infer the type from eager: true
<MarkdownModuletype MarkdownModule = {
path: {
absolute: string;
relative: string;
};
body: {
html: string;
lit: TemplateResult;
};
excerpt: {
html: string;
lit: TemplateResult;
text: string;
};
meta: {
slug: string;
title: string;
frontmatter: Record<string, unknown>;
tableOfContents: TocLevel[];
tableOfContentsFlat: Heading[];
};
source: {
file: string;
markdown: string;
yaml: string;
};
}
>(
'/src/content/blog/**/*.md',
{ eagerImportGlobOptions<true, string>.eager?: true | undefined
Import as static or dynamic
: true, importImportGlobOptions<Eager extends boolean, AsType extends string>.import?: string | undefined
Import only the specific named export. Set to default
to import the default export.
: 'default' },
);
📄 /src/routes/blog/[slug].ts
Secondly, consume them in a dynamic route
import { defineRoutefunction defineRoute<GetHandlerData extends R.HandlerDataHtml = undefined, PostHandlerData extends R.HandlerDataHtml = undefined, CatchAllHandlerData extends R.HandlerDataHtml = undefined, StaticPathOptions extends R.StaticPathOptionsGeneric | undefined = undefined, RouteContext extends R.RouteContextGeneric = {
...;
}>(options: {
handler?: StaticPathOptions extends object ? never : R.Handler<CatchAllHandlerData> | {
GET?: R.Handler<GetHandlerData>;
POST?: R.Handler<PostHandlerData>;
QUERY?: R.Handler<Response>;
PUT?: R.Handler<Response>;
PATCH?: R.Handler<Response>;
DELETE?: R.Handler<Response>;
HEAD?: R.Handler<Response>;
OPTIONS?: R.Handler<Response>;
} | undefined;
staticPaths?: () => R.MaybePromise<StaticPathOptions[]> | undefined;
prerender?: boolean | undefined;
document?: R.DocumentTemplate<RouteContext> | undefined;
template?: R.BodyTemplate<RouteContext> | undefined;
}): (RouteModule: typeof R.RouteModule) => R.RouteModule
Defines a file-based route for Gracile to consume.
} from '@gracile/gracile/route';
import { htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>
Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html
tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
} from 'lit';
import { blogPostsconst blogPosts: Record<string, MarkdownModule>
} from '../../content/content.js';
import { documentconst document: (props: {
url: URL;
title?: string;
}) => ServerRenderedTemplate
} from '../../document.js';
export default defineRoutedefineRoute<undefined, undefined, undefined, {
params: {
slug: string;
};
props: {
title: string;
content: TemplateResult;
toc: TocLevel[];
};
}, {
url: URL;
props: {
title: string;
content: TemplateResult;
toc: TocLevel[];
};
params: {
slug: string;
};
}>(options: {
...;
}): (RouteModule: typeof RouteModule) => RouteModule
Defines a file-based route for Gracile to consume.
({
staticPathsstaticPaths?: (() => MaybePromise<{
params: {
slug: string;
};
props: {
title: string;
content: TemplateResult;
toc: TocLevel[];
};
}[]> | undefined) | undefined
A function that returns an array of route definition object.
Only available in static
output mode.
: () =>
Objectvar Object: ObjectConstructor
Provides functionality common to all JavaScript objects.
.valuesObjectConstructor.values<MarkdownModule>(o: {
[s: string]: MarkdownModule;
} | ArrayLike<MarkdownModule>): MarkdownModule[] (+1 overload)
Returns an array of values of the enumerable properties of an object
(blogPostsconst blogPosts: Record<string, MarkdownModule>
).mapArray<MarkdownModule>.map<{
params: {
slug: string;
};
props: {
title: string;
content: TemplateResult;
toc: TocLevel[];
};
}>(callbackfn: (value: MarkdownModule, index: number, array: MarkdownModule[]) => {
...;
}, thisArg?: any): {
...;
}[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.
((modulemodule: MarkdownModule
) => ({
paramsparams: {
slug: string;
}
: { slugslug: string
: modulemodule: MarkdownModule
.metameta: {
slug: string;
title: string;
frontmatter: Record<string, unknown>;
tableOfContents: TocLevel[];
tableOfContentsFlat: Heading[];
}
.slugslug: string
},
propsprops: {
title: string;
content: TemplateResult;
toc: TocLevel[];
}
: {
titletitle: string
: modulemodule: MarkdownModule
.metameta: {
slug: string;
title: string;
frontmatter: Record<string, unknown>;
tableOfContents: TocLevel[];
tableOfContentsFlat: Heading[];
}
.titletitle: string
,
contentcontent: TemplateResult
: modulemodule: MarkdownModule
.bodybody: {
html: string;
lit: TemplateResult;
}
.litlit: TemplateResult
,
toctoc: TocLevel[]
: modulemodule: MarkdownModule
.metameta: {
slug: string;
title: string;
frontmatter: Record<string, unknown>;
tableOfContents: TocLevel[];
tableOfContentsFlat: Heading[];
}
.tableOfContentstableOfContents: TocLevel[]
,
},
})),
documentdocument?: DocumentTemplate<{
url: URL;
props: {
title: string;
content: TemplateResult;
toc: TocLevel[];
};
params: {
slug: string;
};
}> | undefined
A function that returns a server only template.
Route context is provided at runtime during the build.
: (contextcontext: {
url: URL;
props: {
title: string;
content: TemplateResult;
toc: TocLevel[];
};
params: {
slug: string;
};
}
) => documentfunction document(props: {
url: URL;
title?: string;
}): ServerRenderedTemplate
({ ...contextcontext: {
url: URL;
props: {
title: string;
content: TemplateResult;
toc: TocLevel[];
};
params: {
slug: string;
};
}
, titletitle?: string | undefined
: contextcontext: {
url: URL;
props: {
title: string;
content: TemplateResult;
toc: TocLevel[];
};
params: {
slug: string;
};
}
.propsprops: {
title: string;
content: TemplateResult;
toc: TocLevel[];
}
.titletitle: string
}),
templatetemplate?: BodyTemplate<{
url: URL;
props: {
title: string;
content: TemplateResult;
toc: TocLevel[];
};
params: {
slug: string;
};
}> | undefined
A function that returns a server only or a Lit client hydratable template.
Route context is provided at runtime during the build.
: (contextcontext: {
url: URL;
props: {
title: string;
content: TemplateResult;
toc: TocLevel[];
};
params: {
slug: string;
};
}
) => htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>
Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html
tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
`
<pre>${JSONvar JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.
.stringifyJSON.stringify(value: any, replacer?: (number | string)[] | null, space?: string | number): string (+1 overload)
Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
(contextcontext: {
url: URL;
props: {
title: string;
content: TemplateResult;
toc: TocLevel[];
};
params: {
slug: string;
};
}
.propsprops: {
title: string;
content: TemplateResult;
toc: TocLevel[];
}
, null, 2)}</pre>
...
`,
});
Build your preset
The MarkdownRenderer
class is used to produce a ready-to-consume, standardized MarkdownModule
.
See the “Marked” preset for a simple implementation of the MarkdownRenderer
abstract class. It’s under 100 lines of code!
Also, it uses very few, light dependencies for achieving all tasks, like transforming the MD, extracting the ToC, and other metadata.
Contrary to Marked, remark is extremely flexible and modular. In fact, the website you are visiting is making extensive use of the unified ecosystem. This versatility is cool, but that also means you have to install quite a lot of dependencies.
As everyone has different needs, it’s up to you to plug your custom pipeline; Gracile may offer a basic preset with remark
at some point, which could be expanded or overridden.
You don’t have to implement everything if you don’t need it, also. For example,
you might just want the actual markdown body
, but no meta.frontmatter
, excerpt
…
The class methods will just return empty strings/objects if something isn’t there.
Server-side rendering
As with any lit-html
templates, you can use Web Components inside Markdown,
they will be server-side-rendered and hydrated (if needed) once
inside the client!
As an example, inspect this icon with your browser dev tools:
Opinions
Note
Will speak as a framework author here.
Server rendering custom HTML elements is a very powerful pattern for
Markdown, arguably much more elegant than most “smart MD” implementations.
They all share a major trait which is:
Not respecting the Markdown specs., which already provide full HTML support,
so why not leverage it?
Web Components weren’t a thing at the time Markdown was designed!
In practice, non-standardisms mean your Markdown will be riddled with machine code when previewing inside your IDE or GitHub, which kind of defeats the purpose of this ubiquitous, human-readable format.
Other implementations are:
- Custom Mustache-based syntaxes (think Dev.to’ editor).
- Markdoc (powerful [maybe too much] but still non-standard).
- Markdown directives (not very elegant when un-renderable, while not adding much value over good old HTML)
- MDX (very cool for React-like Storybooks, but tied to JSX, and hard to debug).
MDX is a bit apart, it’s a whole “wrapping” language,
not just an MD’s pre-processed extension, like the three others.
That means you could encounter discrepancies when using unified
plugins.
Again, this is fine if you know what you’re doing, and if you are already
deep in the React world.
You just have to be informed of those fundamental differences,
and their implications.
Let’s do a quick comparison of apples and oranges, for fun (this is untested code, from my memories, it might contain errors):
Markdoc
{% city
name="San Francisco"
deleted=false
coordinates=[1, 4, 9]
meta={ id: "id_123" }
color="red" /%}
No syntax highlight without special plugins.
This code is rendered as-is in unaware previewers.
MDX
<City
name="San Francisco"
deleted={false}
coordinates={[1, 4, 9]}
meta={{ id: 'id_123' }}
color="red"
/>
Better, but nothing is rendered without spinning
up a full JS runtime + processor. Plus it will not render the same with
different interpreters (to be fair, with Web Component it’s the case, too).
And what if you have a syntax error? Nothing will be shown at all.
The parser or runtime will crash. Blank screen of death.
On the other hand, HTML and Markdown are error-prone. There is no such
thing as “syntax error”, just unexpectedly rendered pieces, at most.
The browser is here to save us by gobbling up malformed HTML.
MDX is predominantly Markdown for React/JSX devs, not for everyone.
I believe it’s better to have a bit of incorrect content rather than no content at all! We are not launching spaceships, we just want to communicate knowledge to humans effectively, even if it’s a tad quirky sometimes.
Markdown directives
:::city{
name="San Francisco"
deleted=false
coordinates={[1, 4, 9]}
meta={{ id: "id_123" }}
color="red"
}
:::
Directives are a very interesting CommonMark proposal that never made it through.
You can get some syntax support with the “MDC” VSCode extension.
Formatting is hazardous, plus this syntax can be confusing. It will be everywhere
in your GitHub repo., as-is, un-rendered in preview.
In the end, it is just syntactic sugar, without that much sugar added IMHO.
But this idea will keep floating around, so it has merits.
Now with pure HTML in Markdown:
<city-viewer
deleted="false"
coordinates="[1, 4, 9]"
meta='{ id: "id_123" }'
color="red"
>
<span slot="name">San Francisco</span>
</city-viewer>
Pretty syntax highlight AND formatting 😃!
Also, a lot of IDE already has awesome support for “basic” CommonMark + GitHub Flavored Markdown.
Yes, you will not have the smart, interactive component in “dumb” Markdown
previewers, but at least, you can build on top of REAL content that will
gracefully fall back and be displayed, like this basic slot
example above.
As always with Web Components and HTML, debugging will also be easier, because,
well… you just have your component rendered out with a 1:1 correspondence. Also,
it will force you to keep your content as static as possible, which means easier
authoring in the long run.
Most importantly, no new unusual syntax to learn and it is copy-pastable in your
regular HTML, back-and-forth!
If you really need to build a whole interactive UI with Markdown,
it is still doable, but it is not in the original spirit of this language.
This is the beauty of it: like everything with Gracile, the goal is to build
on top of what the web is already offering us, not re-invent abstractions.
These opinions are debatable, but over time I (the author of Gracile) found myself resorting to HTML when I wanted to “augment” my Markdown.
It will just disappear itself when using a bare MD reader.
And you can still show a fallback into the Web Component slots!
You can also go quite far with just some sprinkles of CSS/JS.
I think those approaches are under-valued right now, in the
content-driven development spheres.
I firmly believe that adding Turing-completeness, composability and other
wizz-bangs to something supposed to be beginner-friendly like Markdown
is not where we want to go. Technical writers are, well…, supposed to write,
not code programs to deliver content. They most likely know a bit of
HTML, rather than JSX or Markdoc, too.
TL;DR: Keep stuff simple and stupid. Standard, declarative and portable.
Please note that this is not a thorough comparison, so you should
take this with a grain of salt.
If you prefer other solutions, it’s fine!
Also don’t forget the immense unified
ecosystem, which can itself help to pre-process
stuff a lot.
Limitations
For now, Lit SSR only renders Web Components with Declarative Shadow Root.
That might throw you off if you’re a die-hard Light DOM adept. It’s
understandable in the context of Markdown rendering where you want to
style everything globally. Indeed, you can take advantage of the Cascade of CSS,
a perfect use case for the art of typography.
There is some hope though:
Some discussion about
supporting Light-DOM rendering via Lit’s’ createRenderRoot
is happening.
And open-stylable shadow
root is attracting a lot of interest from platform-minded developers.
For now, you can leverage the Shadow Root for what is it good for: isolation.
Components like icons, geographical maps, etc. are very well suited for that,
because you don’t want your typography styles to leak in them anyway.