preface

There are many excellent static document generators out there that work much easier than you might think.

Build a static site generator in 40 lines with Node.js

By Douglas Matoso

Translator: Simon Ma

Date: 2017-09-14

Why was this wheel built

When I was planning to build a personal website, my requirements were simple: a website with only a few pages and some information about myself, my skills and projects.

Of course, it should be purely static (no back-end services are required and can be hosted anywhere).

I’ve used well-known static document generators like Jekyll, Hugo, and Hexo, but I think they have too much functionality and I don’t want to add so much complexity to my site.

So I thought, for my needs, a simple static document generator would suffice.

Well, building a simple generator by hand shouldn’t be that hard.

The body of the

Demand analysis

This generator must satisfy the following criteria:

  • Generate HTML files from EJS templates.

  • With layout files, all pages should have the same header, footer, navigation, etc.

  • Allow reusable layout components.

  • The general information about the site is encapsulated in a configuration file.

  • Read data from a JSON file.

    For example: project list, so I can iterate and build project pages easily.

Why use EJS templates?

Because EJS is simple, it’s just JavaScript embedded in HTML.

The project structure

public/  
 src/  
   assets/  
   data/  
   pages/  
   partials/  
   layout.ejs  
 site.config.js
Copy the code
  • Public: the location where the site is generated.
  • SRC: source file.
  • SRC /assets: contains CSS, JS, and images
  • SRC /data: contains JSON data.
  • SRC/Pages: Template folder where HTML pages are generated based on EJS.
  • src/layout.ejs:Main original page template, containing special<%-body%>Placeholder to insert specific page content.
  • Site.config. js: global configuration file in the template.

The generator

The generator code is in the scripts/build.js file, and every time you want to rebuild your site, run the NPM run build command.

You do this by adding the following script to the scripts block of package.json:

"build": "node ./scripts/build"
Copy the code

Here is the complete generator code:

const fse = require('fs-extra')
const path = require('path')
const { promisify } = require('util')
const ejsRenderFile = promisify(require('ejs').renderFile)
const globP = promisify(require('glob'))
const config = require('.. /site.config')

const srcPath = './src'
const distPath = './public'

// clear destination folder
fse.emptyDirSync(distPath)

// copy assets folder
fse.copy(`${srcPath}/assets`.`${distPath}/assets`)

// read page templates
globP('**/*.ejs', { cwd: `${srcPath}/pages` })
  .then((files) = > {
    files.forEach((file) = > {
      const fileData = path.parse(file)
      const destPath = path.join(distPath, fileData.dir)

      // create destination directory
      fse.mkdirs(destPath)
        .then((a)= > {
          // render page
          return ejsRenderFile(`${srcPath}/pages/${file}`.Object.assign({}, config))
        })
        .then((pageContents) = > {
          // render layout with page contents
          return ejsRenderFile(`${srcPath}/layout.ejs`.Object.assign({}, config, { body: pageContents }))
        })
        .then((layoutContent) = > {
          // save the html file
          fse.writeFile(`${destPath}/${fileData.name}.html`, layoutContent)
        })
        .catch((err) = > { console.error(err) })
    })
  })
  .catch((err) = > { console.error(err) })
Copy the code

Next, I’ll explain the specific components of the code.

Rely on

We only need three dependencies:

  • ejs

    Compile our template into HTML.

  • fs-extra

    A derivative of the Node file module with more functionality and Promise support.

  • glob

    Recursively reads a directory that returns all files of type array that match the specified pattern.

Promisify

We use the util.promisify provided by Node to convert all callback functions into promise-based functions.

It makes our code shorter, clearer, and easier to read.

const { promisify } = require('util')  
const ejsRenderFile = promisify(require('ejs').renderFile)  
const globP = promisify(require('glob'))
Copy the code

Load the configuration

At the top, we load the site configuration file to inject into the template rendering later.

const config = require('.. /site.config')
Copy the code

The site configuration file itself loads additional JSON data, such as:

const projects = require('./src/data/projects')

module.exports = {  
  site: {  
    title: 'NanoGen'.description: 'Micro Static Site Generator in Node.js',  
    projects  
  }  
}
Copy the code

Clear site folders

We use the emptyDirSync function provided by FS-Extra to clear the generated site folder.

fse.emptyDirSync(distPath)
Copy the code

Copying static Resources

We use the copy function provided by Fs-extra, which copies static resources recursively to site folders.

fse.copy(`${srcPath}/assets`.`${distPath}/assets`)
Copy the code

Compiling page templates

First we recursively read the SRC/Pages folder using glob (promisify) to look for.ejS files.

It returns an array of all files that match the given pattern.

globP('**/*.ejs', { cwd: `${srcPath}/pages` })  
  .then((files) = > {
Copy the code

For each template file we find, we use Node’s path.parse function to separate the various components of the file path (such as directory, name, and extension).

We then create the corresponding folder in the site directory using the mkdirs function provided by FS-Extra.

files.forEach((file) = > {  
  const fileData = path.parse(file)  
  const destPath = path.join(distPath, fileData.dir)

 // create destination directory  
  fse.mkdirs(destPath)
Copy the code

We then compile the file using EJS and take the configuration data as data parameters.

Since we are using the ejs.renderFile function of the promisify, we can return the result of the call and process the result in the next promise chain.

.then((a)= > {  
  // render page  
  return ejsRenderFile(`${srcPath}/pages/${file}`.Object.assign({}, config))  
})
Copy the code

In the next THEN block, we get the compiled page content.

Now we compile the layout file and pass in the page content as the body property.

.then((pageContents) = > {  
  // render layout with page contents  
  return ejsRenderFile(`${srcPath}/layout.ejs`.Object.assign({}, config, { body: pageContents }))  
})
Copy the code

Finally, we get the compiled result (HTML for layout + page content) that we have generated and then save to the corresponding HTML file.

.then((layoutContent) = > {  
  // save the html file  
  fse.writeFile(`${destPath}/${fileData.name}.html`, layoutContent)  
})
Copy the code

Debug server

To make viewing the results easier, we added a simple static server to the scripts of package.json.

"serve": "serve ./public"
Copy the code

Run the NPM run serve command and open http://localhost:5000 to see the results.

Further exploration

Markdown

Most static document generators support writing content in Markdown format.

They also support adding some metadata at the top in YAML format, as shown below:

---  
title: Hello World  
date: 2013/ 7/13 20: captives  
---
Copy the code

With a few modifications, we can support the same functionality.

First, we must add two dependencies:

  • marked

    Compile markdown into HTML

  • front-matter

    Extract metadata from Markdown (front matter).

We then update the glob’s matching pattern to include.md files and retain.EJs to support rendering complex pages.

If you want to deploy some pure HTML pages, you also need to include.html.

globP('**/*.@(md|ejs|html)', { cwd: `${srcPath}/pages` })
Copy the code

For each file, we must load the file content so that metadata can be extracted at the top.

.then((a)= > {  
  // read page file  
  return fse.readFile(`${srcPath}/pages/${file}`.'utf-8')})Copy the code

We pass the loaded content to front-matter.

It returns an object where the attribute attribute is the extracted metadata.

We then use this data to augment the site configuration.

.then((data) = > {  
  // extract front matter  
  const pageData = frontMatter(data)  
  const templateConfig = Object.assign({}, config, { page: pageData.attributes })
Copy the code

Now we compile the page content into HTML from the file name extension.

If it is.md, the marked function is used to compile;

If it’s.ejS, we’ll continue compiling using EJS;

If it’s.html, you don’t need to compile.

let pageContent  

switch (fileData.ext) {  
  case '.md':  
    pageContent = marked(pageData.body)  
    break  
  case '.ejs':  
    pageContent = ejs.render(pageData.body, templateConfig)  
    break  
  default:  
    pageContent = pageData.body  
}
Copy the code

Finally, we render the layout as before.

One of the most obvious implications of adding metadata is that we can set a separate title for each page, as follows:

---  
title: Another Page  
---
Copy the code

And let the layout render this data dynamically:

<title><% = page.title? ` ${page.title} | ` :"' % ><% = site.title% ></title>
Copy the code

This way, each page will have a unique

tag.

Multiple layout support

Another interesting exploration is the use of different layouts for specific pages.

For example, create a unique layout for the first page of your site:

---  
layout: minimal  
---
Copy the code

We need separate layout files, which I put in the SRC /layouts folder:

src/layouts/  
   default.ejs  
   mininal.ejs
Copy the code

If front Matter has a layout attribute, render it using a template file of the same name in the layouts folder. If not, the default template is used for rendering.

const layout = pageData.attributes.layout || 'default'

return ejsRenderFile(`${srcPath}/layouts/${layout}.ejs`.Object.assign({}, templateConfig, { body: pageContent })
)
Copy the code

Even with these new features, the build script is only 60 lines long.

The next step

If you want to take this one step further, you can add some easy extras:

  • Hot-reloadable debug server

    You can do this using modules like live-Server (built-in auto-reload) or Chokidar (watch for file changes to automatically trigger build scripts).

  • Automatic deployment

    Add scripts to deploy the site to a common hosting service like GitHub Pages, or just upload the files to your own server via SSH (using commands like SCP or rsync).

  • Supports CSS/JS preprocessor

    Add some preprocessors (SASS compiles to CSS,ES6 compiles to ES5, etc.) before the static files are copied to the site files.

  • Better log printing

    Add some console.log output to better analyze what’s going on.

    You can complete this by using the Chalk package.

Feedback? Any suggestions? Please feel free to comment or contact me!


conclusion

A full example of this article can be found here: github.com/doug2k1/nan…

After a while, I decided to convert the project to a CLI module to make it easier to use, which is in the Master branch linked above.

Translator:

Today Japan wants to write a ants(a high-performance Goroutine pool) source code analysis, but the environment is too noisy, quiet heart, then stop.

This is an article I came across a few days ago. Although it was written in 17 years, it still gave me some thoughts after reading it.

Hope this article has been helpful.

A rotten tomato, please do not use it for any commercial purposes.