Yet another opinionated & powerful static site generator.
Turns directories with Markdown files into static websites.
- Based on the remark and rehype ecosystems.
- Powerful and extensible plugins.
- Zero-config with sane defaults to get started.
- Directory structure and file names determine the url's.
- Front Matter in Markdown to override defaults.
- Use a page
type
to enable different layouts and plugins. - Easily build layouts around the Markdown-based content and their
type
. - Optimize SEO with HTML documents including (OpenGraph) meta tags and structured content (
application/ld+json
). - Optimize performance by bundling CSS and JS assets.
- Mark drafts to exclude from lists (yet available with
<meta name="robots" content="noindex">
) - Includes SVGO to optimize SVGs assets.
- Writes sitemap.txt.
- Writes RSS feed.
- Writes search index (using MiniMatch).
- Features a
--watch
mode for auto re-generation.
See webpro.nl and github.com/webpro/webpro.nl for an example website powered by Markdown Rambler.
.
└── content
├── articles
│ ├── starting-a-blog.md
│ ├── writing-a-blogpost.md
│ └── yet-another-article
│ ├── index.md
│ └── image.webp
├── blog.md
└── index.md
const rambler = new MarkdownRambler();
rambler.run();
.
└── dist
├── articles
│ ├── starting-a-blog
│ │ └── index.html
│ ├── writing-a-blogpost
│ │ └── index.html
│ └── yet-another-article
│ ├── index.html
│ └── image.webp
├── blog
│ └── index.html
├── index.html
└── sitemap.txt
And all URLs in /sitemap.txt
:
https://example.org/
https://example.org/articles/starting-a-blog
https://example.org/articles/writing-a-blogpost
https://example.org/articles/yet-another-article
https://example.org/blog
See the tests to get an impression of the conversion from Markdown to HTML.
Option | Type | Default value | Description |
---|---|---|---|
contentFiles |
string | string[] |
'**/*' |
Include Markdown and assets |
contentDir |
string | string[] |
['content'] |
Directories containing Markdown |
ignorePattern |
string |
/^(\.|node_modules)/ |
File pattern(s) to ignore with --watch |
publicDir |
string |
'public' |
Directory containing public assets |
outputDir |
string |
'dist' |
Output directory |
sitemap |
boolean |
true |
Generates sitemap.txt |
feed |
Feed |
false |
Generates feed.xml (RSS) |
search |
Search |
false |
Generates MiniSearch index |
Option | Type | Default value | Description |
---|---|---|---|
verbose |
boolean |
false |
Logs more output about the process |
watch |
boolean |
false |
Add watcher to re-process modified files |
formatMarkdown |
boolean |
false |
Formats source Markdown files (using Prettier) |
Option | Type | Default value | Description |
---|---|---|---|
host |
string |
'' |
Host (e.g. 'https://example.org' ) |
name |
string |
'' |
Website name |
language |
string |
'en' |
Website language (e.g. 'fr-BE' ) |
manifest |
false | string |
false |
Link to PWA manifest file |
type |
TypeFn |
page |
Add type to each page meta data (e.g. 'article' ) |
defaults |
Record<PageType, PageOptions> |
undefined |
Default meta data for each document |
In order of exection:
Option | Type | Default value | Description |
---|---|---|---|
parsers |
Pluggable[] |
parsers |
Remark parsers |
directives |
Record<string, any> |
undefined |
Directives to extend Markdown syntax |
remarkPlugins |
Pluggable[] |
remarkPlugins |
Additional remark plugins |
remarkRehypeOptions |
RemarkRehypeOptions |
{} |
Options for remark-rehype |
rehypePlugins |
Pluggable[] |
rehypePlugins |
Additional rehype plugins |
renderers |
Pluggable[] |
renderers |
Plugins to render (stringify) the hast |
- mdast: Markdown Abstract Syntax Tree
- hast: HyperText (HTML) AST
type Feed = {
pathname: string;
title: string;
description?: string;
author?: string;
tags?: string[];
filter?: (type: string, vFile: VFile) => boolean;
};
type Search = {
outputDir?: string;
filter?: (type: string, vFile: VFile) => boolean;
};
Generates a MiniSearch index file to be used in your client. Here's a minimal example of a client script to use the search index:
(async () => {
await import('https://cdn.jsdelivr.net/npm/minisearch@4.0.3/dist/umd/index.min.js');
const searchIndex = await fetch('/_search/index.json').then(response => response.text());
const index = MiniSearch.loadJSON(searchIndex, { fields: ['title', 'content'] });
const searchBox = document.querySelector('input[type=search]');
const search = query => {
const results = index.search(query, { prefix: true, fuzzy: 0.3 });
console.log(results);
};
searchBox.addEventListener('input', event => {
search(event.target.value);
});
})();
The script(s) can be added to e.g. the public
folder and its path to the defaults.page.scripts
array.
Set formatMarkdown: true
and the following plugins will be applied to the Markdown source files:
- remark-prettier to format the document
- remark-reference-links to turn
[text](url)
into[text][ref]
(and add definitions to the end) - order-links to order the definitions
type TypeFn = (filename: string, matter: FrontMatter) => PageType;
Example:
{
type: filename => (filename.match(/^blog\//) ? 'article' : 'page');
}
Sets default for each type of page. By default there's only the page
type. Example:
const options = {
defaults: {
page: {
layout: '[See "Layout" below]'
stylesheets: ['/css/stylesheet.css'],
author: {
name: 'Lars Kappert',
href: 'https://www.webpro.nl',
twitter: '@webprolific'
},
publisher: {
name: 'Lars Kappert',
href: 'https://www.webpro.nl',
logo: {
src: 'https://www.webpro.nl/img/logo-512x512.png'
}
},
icon: {
src: '/img/logo.svg'
},
logo: {
alt: 'Blog Logo',
src: '/img/logo.svg',
href: '/'
},
sameAs: ['https://github.com/webpro'],
layout: () => {},
prefetch: '/blog'
}
}
};
Any Front Matter in the Markdown augments or overrides these defaults.
---
published: 2022-03-05
modified: 2022-04-20
image: /articles/yet-another-article/image.webp
draft: true
---
# Yet Another Article
Lorem ipsum
The merged meta data will be used in the meta tags and structured content, and is available in layouts and directives.
- The
published
date adds<meta property="article:published_time" content="2022-03-05T00:00:00Z">
- The
author.name
adds<meta name="author" content="Lars Kappert">
- The
prefetch
value will add<link rel="prefetch" href="/blog">
See the PageOptions
type for details.
Each page type can have its own layout to wrap the content. Render ${node}
somewhere, and use all of the page's meta
data that was provided by Markdown Rambler, merged in with the provided default configuration:
import { html } from 'markdown-rambler';
export default (node, meta) => {
const { logo } = meta;
return html`
<header>
<a href="${logo.href}">
<img src="${logo.src}" alt="${logo.alt}" />
</a>
</header>
<main class=${meta.class}>${node}</main>
<footer>© 2022, Lars Kappert</footer>
`;
};
In this example, the class
field of the Front Matter of each Markdown file would be added to the <main>
element,
while the default.page.class
option could serve as a fallback class
value.
The default remark plugins:
These can be entirely replaced with different parsers
, or extended using remarkPlugins.
Use remarkPlugins
to add remark plugins (to work with
the mdast before it is converted to hast).
Use remarkRehypeOptions
to pass options to
remark-rehype.
The default rehype plugins:
- rehype-autolink-headings (only wraps h2-h6)
- rehype-slug
- rehype-document
- JSON-LD (structured content) in a <script type="application/ld+json">{}</script>.
Use rehypePlugins
to add rehype plugins (to work with
the hast after it is converted from mdast).
Use the renderers
option to replace these default render plugins.
Directives are a powerful way to extend the Markdown syntax. The (implemented) proposal consists of inline (:
), leaf
(::
) and container (:::
) block directives.
::ASIDE
# Header
:::div{.wrapper}
Content with :abbr[HTML]{title="HyperText Markup Language"}
:::
The inline and container directives are readily available. To use a leaf block directive, pass an object with the
directive as a key, and a function that returns a hast
node. The function is much like an AST visitor function, and
adds the vFile
argument for convenience:
type DirectiveVisitor = (node: Element, index: number, parent: Parent, vFile: VFile) => Element;
const insertAside = (node, index, parent, vFile) => {
return h('aside', { class: 'custom' }, 'news');
};
const directives = {
ASIDE: insertAside
};
This will result in this HTML output:
<aside class="custom">news</aside>
<h1>Header</h1>
<div class="wrapper">Content with <abbr title="HyperText Markup Language">HTML</abbr></div>