Write me a simple static website generator based on NodeJS

Prompt: I want to build a static website generator that will compile my local files into another folder named ‘build’, including HTML partials, images, CSS and JS and be able to view the complied website on my localhost. I want to use as simple NodeJS as possible?

To build a simple static website generator using Node.js, you can use a minimal number of dependencies. In this example, we’ll use the following libraries:

  1. express to serve your files on localhost.
  2. fs and path, which are built-in Node.js modules to interact with the file system and handle file paths.
  3. handlebars as a templating engine to handle HTML partials.

First, make sure you have Node.js installed. You can download it from https://nodejs.org/.

Create a new folder for your project, and initialize it with npm:

mkdir simple-static-generator
cd simple-static-generator
npm init -y

Install the required dependencies:

npm install express handlebars chokidar

Create a folder structure like this:

simple-static-generator/
|-- src/
|   |-- partials/
|   |-- css/
|   |-- js/
|   |-- img/
|-- build/
|-- server.js

Put your HTML, CSS, JS, and image files in the respective folders under src/.

Now, create server.js:

const express = require('express');
const fs = require('fs');
const path = require('path');
const Handlebars = require('handlebars');
const chokidar = require('chokidar');

const app = express();
const srcDir = path.join(__dirname, 'src');
const buildDir = path.join(__dirname, 'build');

// Function to compile HTML files with partials
function compileHTMLFile(file) {
  const filePath = path.join(srcDir, file);
  const content = fs.readFileSync(filePath, 'utf-8');
  const template = Handlebars.compile(content);

  // Register all partials in the partials folder
  fs.readdirSync(path.join(srcDir, 'partials')).forEach(partial => {
    const partialPath = path.join(srcDir, 'partials', partial);
    const partialContent = fs.readFileSync(partialPath, 'utf-8');
    const partialName = path.basename(partial, '.html');
    Handlebars.registerPartial(partialName, partialContent);
  });

  return template({});
}

// Function to copy a directory
function copyDir(src, dest) {
  if (!fs.existsSync(dest)) {
    fs.mkdirSync(dest);
  }

  const files = fs.readdirSync(src);

  for (const file of files) {
    const srcPath = path.join(src, file);
    const destPath = path.join(dest, file);

    if (fs.statSync(srcPath).isDirectory()) {
      copyDir(srcPath, destPath);
    } else {
      fs.copyFileSync(srcPath, destPath);
    }
  }
}

// Function to clear a directory
function clearDirectory(dirPath) {
  if (fs.existsSync(dirPath)) {
    const files = fs.readdirSync(dirPath);
    files.forEach(file => {
      const curPath = path.join(dirPath, file);
      if (fs.lstatSync(curPath).isDirectory()) {
        clearDirectory(curPath);
      } else {
        fs.unlinkSync(curPath);
      }
    });
  }
}

// Function to build the project
function build() {
  // Clear the build directory
  clearDirectory(buildDir);

  fs.readdirSync(srcDir).forEach(file => {
    const srcPath = path.join(srcDir, file);
    const buildPath = path.join(buildDir, file);

    if (fs.statSync(srcPath).isDirectory()) {
      copyDir(srcPath, buildPath);
    } else if (path.extname(file) === '.html') {
      const compiledHTML = compileHTMLFile(file);
      fs.writeFileSync(buildPath, compiledHTML);
    }
  });

  console.log('Project rebuilt.');
}

// Function to watch for changes in the src folder and rebuild
function watchAndRebuild() {
  const watcher = chokidar.watch(srcDir, { ignored: /(^|[\/\\])\../, persistent: true });

  watcher
    .on('change', (path) => {
      console.log(`File ${path} has been changed.`);
      build();
    })
    .on('add', (path) => {
      console.log(`File ${path} has been added.`);
      build();
    })
    .on('unlink', (path) => {
      console.log(`File ${path} has been removed.`);
      build();
    });

  console.log(`Watching for changes in ${srcDir}...`);
}

// Call the build and watchAndRebuild functions
build();
watchAndRebuild();

// Serve the build folder
app.use(express.static(buildDir));

// Start the server
const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server running on http://localhost:${port}`);
});

Now, run the server:

node server.js

Your website should be accessible at http://localhost:3000.

This is a simple static website generator that should work for basic use-cases. You can customize and extend it according to your requirements.

Prompt: This worked! Now how do I use my partials in the HTML pages?

Great! Now, let’s see how to use partials in your HTML pages. Handlebars allows you to include partials using the {{> partialName }} syntax, where partialName is the name of the partial without the file extension.

Assuming you have the following file structure:

simple-static-generator/
|-- src/
|   |-- partials/
|   |   |-- header.html
|   |   |-- footer.html
|   |-- css/
|   |-- js/
|   |-- img/
|   |-- index.html
|-- build/
|-- server.js

You can use the header.html and footer.html partials in your index.html file like this:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Static Website</title>
    <link rel="stylesheet" href="css/styles.css">
</head>
<body>
    {{> header }}

    <main>
        <h1>Welcome to My Static Website</h1>
        <p>This is an example of a simple static website using Node.js and Handlebars.</p>
    </main>

    {{> footer }}

    <script src="js/scripts.js"></script>
</body>
</html>

In this example, the {{> header }} syntax will include the contents of header.html, and {{> footer }} will include the contents of footer.html.

Here’s an example of what your header.html and footer.html partials could look like:

header.html:

<header>
    <nav>
        <a href="/">Home</a>
        <a href="/about.html">About</a>
        <a href="/contact.html">Contact</a>
    </nav>
</header>

footer.html:

<footer>
    <p>&copy; 2023 My Static Website. All rights reserved.</p>
</footer>

After you run the server.js script, the partials will be included in the compiled index.html file within the build folder. The partials can be used in any other HTML file in the same way.

Let's connect

Get in touch if you have a new project or just want to say hi.

© by Torresian Crow Consulting Pty Ltd.