mu

the frameless framework

mu does not exist. There's no package to install. No config file to set up. No dependencies to keep track of. mu is nothing—but a few simple conventions that make it easy to roll your own static site generator using nothing but node.js and modern javascript.

the basics

Building a static site is simple: Read some files, manipulate the content of each file, and write a bunch of new files. You don't need a framework on top of Node to do any of that.


Use Node's built in file system module fs to read and write files. It has a set of async methods that play nicely with modern .js.

const fsp = require('fs').promises

// await fsp.readFile()
// await fsp.writeFile()
// await fsp.readdir()

When you read a file, convert it into a plain, old javascript object.

const readFile = async (src) => {
    const file = {}
    file.content = await fsp.readFile(src, 'utf8')
    return file
}

When you're working with a collection of file objects, like blog posts, store them in an array.

const readFiles = async (dir) => {
    const files = []

    for (let filename of await fsp.readdir(dir)) {
        files.push( await readfile(file) )
    }

    return files
}

Now you can manipulate your files using regular javascript. No wrestling with a framework required.

const pages = await readFiles('./markdown/')

for (let page of pages) {
    if (page.content.length > 1000) {
        page.content = doSomething(page.content)
    }
}

If you need to use a third party package, there's no need to wrap it in a plug-in format. Just use it as it is.

const marked = require('marked') // md to html

const pages = await readFiles('./markdown/')

for (let page of pages) {
    page.content = marked(page.content)
}

It might be tempting to try to tidy up your code by passing a file object or array of files into all of your functions rather than manipulating page.content directly. Don't. page.content is just a string. What you gain in pretty code you'll lose in flexibility.

// This ...
escapeHTML(page)

// Is prettier than this ...
page.content = escapeHTML(page.content)    

// But this should work as well
page.title = escapeHTML(page.title)

If a function accepts or returns a mu file object add File to the end of its name. If a function accepts or returns an array of mu file objects, add Files to the end of its name. This just makes it easy to tell at a glance what a function does.

writeFile() // not write()
mergeFiles() // not merge()
escapeHTML()  //  works with any string

organizing your files

Save your functions as javascript modules.

// writeFile.js

const fsp = require('fs').promises

module.exports = async (file) => {
    ...
    await fsp.writeFile(destination, file.content)
} 

Keep them in a folder called mu.

mu/
    readFiles.js
    writeFile.js
    escapeHTML.js
    index.js

Create an index.js file in your mu folder that exports all of your functions.

// mu/index.js

module.exports = {
    readFiles: require(`./readFiles.js`),
    writeFile: require(`./writeFile.js`),
    escapeHTML: require(`./escapeHTML.js`)
}

Now you have a toolbox of modules that you can easily access from any project folder. Just remember mu does not exist. These aren't plugins because there's nothing to plug in to.

Create a mu.js file for your project:

myProject/
    mu.js
    markdown/
        page1.md
        page2.md
    build
// myProject/mu.js

const mu = require('/Users/absolute/path/to/mu') 

const build = () => {
    try {
        const pages = await mu.readFiles('./markdown')

        for (let page of pages) {
            page.destination = './build'
            mu.writeFile(page)
        }
    } catch (e) { 
        console.log(e)
    }
}

build()

sharing

It's easy to build and share mu modules because they're just regular javascript. A gist on GitHub, a function on stack overflow, a bit of code on a blog, it all just works. When you need to do something tricky for a specific project, you don't need to bend a framework to get it to do what you want.

templates

Any html template engine will work. There's no need for a layout plug-in. If Handlebars is your poison, just require('handlebars'). I use wu. wu does not exist. wu is nothing—but modern javascript functions that return template literals. You can read more about wu but here's a quick peak:

const wu = {}

wu.layout = (page) => {
    return `
    <html>
        <head>
            <title>${page.title}</title>
        </head>
        <body>
            ${page.content}
        </body>
    </html>
    `
}

front matter

Front matter can be be extracted from files using whatever third party module you'd like. front-matter.js is the best one I've found. Or roll your own. YAML is notoriously difficult to parse but you can grab the JSON from the beginning of a file with a few lines of code and zero dependencies. If you use front matter a lot, parse each file as it's being read to save the extra step.

const fm = require('front-matter') 

const readFile = async (src) => {
    const file = {}
    file.content = await fsp.readFile(src, 'utf8')
    const frontmatter = fm(file.content)
    file.fm = frontmatter.attributes
    file.content = frontmatter.body
    return file
}

metadata

You can also add some metadata to each file object as its being read using Node's built in STATS and PATH modules.

const readFile = async (source) {
    const sourcePath = path.parse(source)
    const sourceStat = await fsp.stat(source)

    if (!sourceStat.isFile()) return 

    const file = {
        source: source,
        created: sourceStat.birthtime, 
        modified: sourceStat.mtime,
        createdMs: sourceStat.birthtimeMs,
        modifiedMs: sourceStat.mtimeMs,
        name: sourcePath.name,
        ext: sourcePath.ext,
        destination: ''
    }

    file.content = await fsp.readFile(source, 'utf8')

    return file
}

The created and modified properties allow you to sort your pages based on the dates of the original source files. This can save you from having to manually add dates to the front matter of your files.

const posts = await mu.readFiles('md')

// sort posts in reverse chronological order
posts.sort((a, b) => (a.created > b.created) ? -1 : 1)

Having a few standard properties like name and ext on each file object comes in handy when writing new files. When you're generating a site from markdown, usually you want to keep the original file name but change the extension and set a destination folder.

const marked = require('marked') //md to html

const pages = await mu.readFiles('markdown')

for (let page of pages) {
    page.destination = './build'
    page.ext = '.html'    
    page.content = marked(page.content)    
    mu.writeFile(page)
}
const writeFile = async (file) => {
    const destination = path.join(file.destination, file.name) + file.ext
    await fsp.writeFile(destination, file.content)
} 

markdown

Any markdown convertor will work with mu. marked.js is a good choice with zero dependencies. I keep third party scripts that I use all the time, like marked, in my mu folder and load them like any other mu module that I've written myself. That way I don't have to maintain a node_modules folder for each project. All I need is a mu.js in my project folder.

remove-markdown.js strips markdown from a string. It comes in handy whenever you need to you need to display an excerpt of text without markdown syntax.

small and focused

Keep your modules as single focused and reusable as possible. For example, pagination is just adding links between pages. There's no need to combine that with merging files or building an index for a blog.

const blogPosts = await readFiles('md')
const postsPerPage = 3
const index = mergeFiles(blogPosts, postsPerPage)

// Add next/previous link properties to each page 
paginateFiles(index)

// Now building the index is no different
// than working with any other files array
for (let page of index) {
    mu.writeFile(page)
}

more

You've probably figured out by now that mu can be used for a lot more than just generating static sites.

const source = 'css'
const destination = 'www/resources/bundle.css'
mu.bundleFiles('source', destination)
const bundleFiles = async (source, destination) => {

    const files = await readFiles(source)
    const p = path.parse(destination)

    const bundle = {
        name: p.name,
        ext: p.ext,
        destination: p.dir,
        content: ''
    }

    for (let file of files) { 
        bundle.content += file.content + '\n'
    }

    writeFile(bundle)    
}

examples

Here are the mu.js and wu.js files I use to generate this site.

Here's a simple mu.js that could be used to generate a blog.

And here are a few of my mu modules that I use on most of my projects.

Do whatever you'd like with them. They're a bit kludgey. They could be improved by a better programmer. But that's ok. They work. I'm not processing thousands of files. None of this is on a live server. I can open up my mu.js for a project and tweak it to do whatever I'd like in a few minutes without having to wrestle with a framework. And that gives me a deep sense of inner peace.