Seihon

Bind frontmatters into collections

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

This toolkit is the last piece of the puzzle that enables code-splitted CMS-less MDX-based static site generation.

Problem

CMS, whether headless or not, is usually overkill for simple static sites like personal blogs and portfolios. 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 its 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

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.

  2. @seihon/macro is a babel-macro that transpiles collection<Item>('../example.collection.js') into require('../example.collection.js').

Usage

This is an example of the 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
          ...
        seihon.config.js
      projects/
        ...
        seihon.config.js
    ...
  webpack.config.js
  ...
// webpack.config.js
// ...
module.exports = {
  rules: [
    {
      test: /seihon\.config\.js$/,
      use: ['babel-loader', '@seihon/loader'],
    },
    // ...
  ],
};
// 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.
// src/content/{posts,projects}/seihon.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),
  }),
};
// src/components/home.jsx
import React from 'react';
import seihon from '@seihon/macro';
import PostPreview from './PostPreview';

const posts = seihon('../content/posts/seihon.config.js');

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

Check out the project's GitHub repo for more information.