UNPKG

markdown-rambler

Version:

Yet another opinionated & powerful static site generator.

372 lines (286 loc) 13.3 kB
# Markdown Rambler 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. ## Showcase See [webpro.nl](https://www.webpro.nl) and [github.com/webpro/webpro.nl](https://github.com/webpro/webpro.nl) for an example website powered by Markdown Rambler. ## Input ``` . └── content    ├── articles    │   ├── starting-a-blog.md    │   ├── writing-a-blogpost.md    │   └── yet-another-article    │    ├── index.md    │    └── image.webp    ├── blog.md    └── index.md ``` ## Build Script ```js const rambler = new MarkdownRambler(); rambler.run(); ``` ## Output ``` . └── 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 ``` ## Tests See [the tests](./test/index.spec.ts) to get an impression of the conversion from Markdown to HTML. ## Options ### Overview #### File Structure & Output | 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`](#feed) | `false` | Generates `feed.xml` (RSS) | | `search` | [`Search`](#search) | `false` | Generates MiniSearch index | #### Flags | 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`](#format-markdown) | `boolean` | `false` | Formats source Markdown files (using Prettier) | #### Content | 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`](#type) | `page` | Add `type` to each page `meta` data (e.g. `'article'`) | | [`defaults`](#defaults) | `Record<PageType, PageOptions>` | `undefined` | Default meta data for each document | #### Plugins In order of exection: | Option | Type | Default value | Description | | --------------------------------------------- | --------------------- | --------------------------------- | -------------------------------------- | | [`parsers`](#parsers) | `Pluggable[]` | [`parsers`](#parsers) | Remark parsers | | [`directives`](#directives) | `Record<string, any>` | `undefined` | Directives to extend Markdown syntax | | [`remarkPlugins`](#remark-plugins) | `Pluggable[]` | [`remarkPlugins`](#remarkPlugins) | Additional remark plugins | | [`remarkRehypeOptions`](#remarkrehypeoptions) | `RemarkRehypeOptions` | `{}` | Options for remark-rehype | | [`rehypePlugins`](#rehype-plugins) | `Pluggable[]` | [`rehypePlugins`](#rehypePlugins) | Additional rehype plugins | | [`renderers`](#renderers) | `Pluggable[]` | [`renderers`](#renderers) | Plugins to render (stringify) the hast | 1. mdast: Markdown Abstract Syntax Tree 2. hast: HyperText (HTML) AST ### Feed ```ts type Feed = { pathname: string; title: string; description?: string; author?: string; tags?: string[]; filter?: (type: string, vFile: VFile) => boolean; }; ``` ### Search ```ts type Search = { outputDir?: string; filter?: (type: string, vFile: VFile) => boolean; }; ``` Generates a [MiniSearch](https://lucaong.github.io/minisearch/) index file to be used in your client. Here's a minimal example of a client script to use the search index: ```js (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. ### Format Markdown Set `formatMarkdown: true` and the following plugins will be applied to the Markdown source files: - [remark-prettier](https://github.com/remcohaszing/remark-prettier) to format the document - [remark-reference-links](https://github.com/remarkjs/remark-reference-links) to turn `[text](url)` into `[text][ref]` (and add definitions to the end) - [order-links](./src/unist/order-links.ts) to order the definitions ### Type ```ts type TypeFn = (filename: string, matter: FrontMatter) => PageType; ``` Example: ```ts { type: filename => (filename.match(/^blog\//) ? 'article' : 'page'); } ``` ### Defaults Sets default for each type of page. By default there's only the `page` type. Example: ```js 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. ```md --- 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](./src/types.d.ts) for details. #### Layout 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: ```ts 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. ### Plugins #### Parsers The default remark plugins: - [remark-parse](https://github.com/remarkjs/remark/tree/main/packages/remark-parse) - [remark-frontmatter](https://github.com/remarkjs/remark-frontmatter) - [table](./src/mdast/table.ts) - [remark-directive](https://github.com/remarkjs/remark-directive) (also see [Directives](#directives)) These can be entirely replaced with different `parsers`, or extended using [remarkPlugins](#remark-plugins). #### remark Plugins Use `remarkPlugins` to add [remark plugins](https://github.com/remarkjs/remark/blob/main/doc/plugins.md) (to work with the mdast before it is converted to hast). #### remarkRehypeOptions Use `remarkRehypeOptions` to pass [options](https://github.com/remarkjs/remark-rehype#options) to [remark-rehype](https://github.com/remarkjs/remark-rehype). #### rehype Plugins The default rehype plugins: - [rehype-autolink-headings](https://github.com/rehypejs/rehype-autolink-headings) (only wraps h2-h6) - [rehype-slug](https://github.com/rehypejs/rehype-slug) - [rehype-document](https://github.com/rehypejs/rehype-document) - [JSON-LD](./src/util/structured-content.ts) (structured content) in a [<script type="application/ld+json">{}</script>](./src/hast/transformers.ts). Use `rehypePlugins` to add [rehype plugins](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md) (to work with the hast after it is converted from mdast). #### Renderers - [rehype-format](https://github.com/rehypejs/rehype-format) - [rehype-stringify](https://github.com/rehypejs/rehype/tree/main/packages/rehype-stringify) Use the `renderers` option to replace these default render plugins. ### Directives Directives are a powerful way to extend the Markdown syntax. The (implemented) proposal consists of inline (`:`), leaf (`::`) and container (`:::`) block directives. ```md ::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: ```ts type DirectiveVisitor = (node: Element, index: number, parent: Parent, vFile: VFile) => Element; ``` ```ts const insertAside = (node, index, parent, vFile) => { return h('aside', { class: 'custom' }, 'news'); }; const directives = { ASIDE: insertAside }; ``` This will result in this HTML output: ```html <aside class="custom">news</aside> <h1>Header</h1> <div class="wrapper">Content with <abbr title="HyperText Markup Language">HTML</abbr></div> ```