Seihon

Bind MDX files into a collection

Overview

Seihon【製本】(Bookbinding in Japanese) is a JavaScript toolkit that improves your MDX transformation pipeline. It allows you to quickly transform MDX documents into a collection (like turning codices into book).

Background

This toolkit is the last piece of the puzzle that enables code-splitted CMS-less MDX-based static site generation. The website that you're currently reading is powered by Seihon.

Problem

CMS, whether headless or not, is usually an overkill for simple static sites like personal blog and portfolio. When the only content publisher is the site builder, it's better to transform the content at compile time from Markdowns to web assets. This approach saves the cost of hosting a server without sacrificing the user experience.

However, the lack of CMS also makes it hard to extract and transform content from their raw form. Even with tools like MDX and Webpack, it is still impossible to build a list view of all blog posts written in Markdown without self-managing the list. For example, if you want to show the cover image in both list view and post view, you are forced to create duplicated content - the blog post itself with its metadata as well as a list of post metadata.

Introduction

Seihon aims at streamlining the markdown content management pipeline for individual site owners to create a seamless content editing and publishing experience.

It currently consists of two libraries.

  1. @seihon/loader is a webpack loader that collects frontmatter from all MDX documents and transforms them into one single object.

    • It allows you to statically generate Table of Content, Blog Directory, Project List, or anything that contains a collection of data derived from frontmatter, without manual maintenance. You can even paginate the result using query parameters.
    • Additionally, this loader allows you to transform frontmatter into actually content in the markdown part of the MDX document.
  2. @seihon/sectionize is a unified plugin that divides a continuous piece of text into chunks wrapped by a customizable tag.

    • To use it with @mdx-js/loader, you can add it to the remarkPlugins option.
    • To use it with unified, you just need to place this plugin into the .use() pipeline.

Usage

This is an example of a complete usage of the Seihon library. For individual usage, please refer to their own README.md.

Although Seihon makes no assumption about your project structure, it's always easier to explain its usage with one. Take the following structure as an example.

my-site/
  src/
    components/
      home.jsx
      post.jsx
      ...
    content/
      posts/
        introducing-seihon/
          index.mdx
          ...
        effective-javascript/
          index.mdx
          ...
        collection.config.js
      projects/
        ...
        collection.config.js
    ...
  webpack.config.js
  ...

Webpack can be configured as follows.

// webpack.config.js
import sectionize from '@seihon/sectionize';
// ...
module: {
  rules: [
    {
      test: /\.mdx?$/,
      use: [
        'babel-loader',
        {
          loader: '@mdx-js/loader',
          options: { remarkPlugins: [sectionize] },
        },
        '@seihon/loader',
      ],
    },
    {
      test: /collection\.config\.js$/,
      use: ['babel-loader', '@seihon/loader'],
    },
    // ...
  ];
}

And with the following MDX files,

// src/content/posts/introducing-seihon.mdx
---
title: Introducing Seihon
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
// src/content/posts/effective-javascript.mdx
---
title: Effective JavaScript
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

we can set up a collection.config.js file that specifies how post list is built with the desire metadata transformation.

// src/content/{posts,projects}/collection.config.js
module.exports = {
  transform: (frontmatter, text) => {
    ...frontmatter,
    postId: frontmatter.title
      .replace(/[^0-9a-zA-Z\s]/gi, '')
      .replace(/\s+/gi, '-')
      .toLowerCase(),
    minRead: Math.ceil(text.split(' ').length / 200)
  }
};

You can then import and render the transformed post list metadata by importing the config file.

// src/components/home.jsx
import React from 'react';
import collection from '../content/posts/collection.config.js';

export default function Blog() {
  return collection.map(({ postId, minRead }) => (
    <PostPreview postId={postId} minRead={minRead} />
  ));
}

Last but not least, you can import and render the post itself using your favorite dynamic loading library.

// src/components/post.jsx
import React from 'react';
import loadable from '@loadable/component';
import { MDXProvider } from '@mdx-js/react';

const Markdown = loadable.lib((props: Props) =>
  import(
    /* webpackInclude: /\.mdx?$/i */
    `../../content/blog/${props.postId}`
  )
);
// postId can be extracted from URL segment using your favorite routing library, e.g. example.com/blog/lorem-ipsum-1
export default function Post({ postId, ...props }) {
  return (
    <MDXProvider>
      <Markdown postId={postId}>
        {({ default: Component, frontmatter }) => <Component {...props} />}
      </Markdown>
    </MDXProvider>
  );
}