Claude
Skills
Sign in
Back

file-based-routing-for-static-sites

Included with Lifetime
$97 forever

Learn how to create a multi-page static site with file-based routing using TypeScript, allowing for quick updates and easy content management.

Writing & Docs

What this skill does


# File Based Routing for Static Sites


In this article I will go over how to use file based routing to output as a static site multi page application.

> **TLDR** The final source [here](https://github.com/rodydavis/static-site-file-based-routing) and an online [demo](https://rodydavis.github.io/static-site-file-based-routing/).

## Step 1 

Create a new folder called “static-site-file-based-routing” and open it up in VSCode.

```
mkdir static-site-file-based-routing
cd static-site-file-based-routing
code .
```

## Step 2 

Create a `tsconfig.json` and replace it with the following:

```
{
  "compilerOptions": {
    "incremental": true,
    "target": "es5",
    "module": "es2020",
    "outDir": "dist",
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "typeRoots": [
      "node_modules/@types",
      "src/@types"
    ]
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules",
    "dist"
  ]
}
```

## Step 3 

Create a `package.json` and update it with the following:

```
{
  "name": "static-site-file-based-routing",
  "version": "1.0.0",
  "description": "File based routing for HTML MPA",
  "type": "module",
  "scripts": {
    "start": "wds --node-resolve --root-dir build --base-path /static-site-file-based-routing --open --watch",
    "postinstall": "npm run tsc",
    "build": "node dist/main.js --inputDir ./example --outputDir ./build",
    "dev": "node dist/main.js --inputDir ./example --outputDir ./build -w",
    "tsc": "tsc",
    "tsc:watch": "tsc -w"
  },
  "devDependencies": {
    "@pkgjs/parseargs": "^0.10.0",
    "@types/markdown-it": "^12.2.3",
    "@types/node": "^18.7.23",
    "@web/dev-server": "^0.1.34",
    "chokidar": "^3.5.3",
    "highlight.js": "^11.6.0",
    "html-format": "^1.0.2",
    "markdown-it": "^13.0.1",
    "parse5": "^7.1.1",
    "typescript": "^4.8.4"
  }
}
```

Then run `npm install` to install all the dependencies.

> I am using `@web/dev-server` to serve the site locally. You can use any server you want.

These dependencies are used for various file transformations such as markdown to HTML, HTML formatting, and file watching.

## Step 4 

Create a `src` folder and add 4 files:

### `src/build.ts` 

```
import * as fs from "fs";
import { compileDir, compileTarget } from "./compile.js";
import * as path from "path";
import chokidar from "chokidar";
import { publicDirectory } from "./static.js";

interface Options {
    inputDir?: string;
    outputDir?: string;
    publicDir?: string;
    watch?: boolean;
    clean?: boolean;
}

export default async function build(options: Options) {
    const inputDir = options.inputDir || "www";
    const outputDir = options.outputDir || "build";
    const publicDir = options.publicDir || "public";
    const watch = options.watch || false;
    const clean = options.clean || false;

    if (!fs.existsSync(inputDir)) {
        throw new Error(`Input directory ${inputDir} does not exist`);
    }

    if (clean) {
        if (fs.existsSync(outputDir)) {
            fs.rmdirSync(outputDir, { recursive: true });
        }
    }

    if (!fs.existsSync(outputDir)) {
        fs.mkdirSync(outputDir, { recursive: true });
    }

    if (watch) {
        console.log("Watching for changes...");
        chokidar.watch(inputDir).on("all", async (event, inputFile) => {
            console.log(event, inputFile);
            if (fs.existsSync(inputFile)) {
                const relativePath = path.relative(inputDir, inputFile);
                const outputFile = `${outputDir}/${relativePath}`;

                const stat = fs.statSync(inputFile);
                if (stat.isDirectory()) {
                    await compileDir(inputFile, outputFile);
                } else if (stat.isFile()) {
                    const filename = path.basename(inputFile);
                    if (filename === "layout.html") {
                        // Rebuild all related directories
                        const dir = path.dirname(inputFile);
                        const inDir = path.relative(inputDir, dir);
                        const outDir = `${outputDir}/${inDir}`;
                        await compileDir(dir, outDir);
                    } else {
                        await compileTarget(inputFile, outputFile);
                    }
                }
            }
        });
    } else {
        await compileDir(inputDir, outputDir);
    }

    if (publicDir.split(',').length > 1) {
        for (const dir of publicDir.split(',')) {
            publicDirectory(dir, outputDir, watch);
        }
    } else {
        publicDirectory(publicDir, outputDir, watch);
    }
}
```

### `src/compile.ts` 

```
import * as fs from "fs";
import * as path from "path";
import MarkdownIt from "markdown-it";
import hljs from "highlight.js";
import * as parse5 from "parse5";
import type { Document } from "parse5/dist/tree-adapters/default.js";
import format from 'html-format';

function compile(file: string) {
    const raw = fs.readFileSync(file, "utf-8");
    const ext = path.extname(file);

    switch (ext) {
        case ".md":
        case ".markdown":
            const md = new MarkdownIt({
                html: true,
                linkify: true,
                typographer: true,
                highlight: function (str, lang) {
                    if (lang && hljs.getLanguage(lang)) {
                        try {
                            return (
                                '<pre class="hljs"><code>' +
                                hljs.highlight(str, { language: lang, ignoreIllegals: true })
                                    .value +
                                "</code></pre>"
                            );
                        } catch (__) {
                            console.error(__);
                        }
                    }
                    return "";
                },
            });
            return parse5.parse(md.render(raw));
        case ".html":
            return parse5.parse(raw);
        default:
            break;
    }

    return raw;
}

function createHtml(options?: { head?: string; body?: string; }) {
    return `
<!DOCTYPE html>
<html lang="en">
<head>
${options?.head ?? ""}
</head>
<body>
${options?.body ?? "<slot></slot>"}
</body>
</html>
`;
}

export async function compileFile(file: string, target: string) {
    // Use regex to check if ends with index.*
    const isIndex = /index\.([a-z]+)$/i.test(file);

    if (!isIndex && !fs.statSync(file).isDirectory()) {
        // Skipping for nested layouts
        return;
    }

    const parent = path.dirname(target);

    // Render up the directory tree until we hit the root directory
    const files: string[] = [file];
    let filePath = file;
    while (filePath !== parent) {
        filePath = path.dirname(filePath);
        // Check for root level layout
        const layout = path.join(filePath, "layout.html");
        if (fs.existsSync(layout)) {
            files.unshift(layout);
        }
        if (filePath === ".") break;
    }

    // Check for root level index markdown or html
    const layout = path.join(parent, "layout.html");
    if (fs.existsSync(layout)) {
        if (
            fs.existsSync(path.join(parent, "index.html")) ||
            fs.existsSync(path.join(parent, "index.md")) ||
            fs.existsSync(path.join(parent, "index.markdown"))
        ) {
            files.unshift(layout);
        }
    }

    let output = createHtml();

    for (const item of files) {
        const doc = compile(item);
        if (typeof doc !== 'string') {
            const content = parse5.serialize(doc);
            output = mergeDocuments(output, content);
        }
    }

    // Replace extension
    const ext = path.extname(file);
    const newFile = target.replace(ext, '.html');

    // Check if parent directory exists
    const parentDir = path.dirname(newFile);
    if (!fs.e

Related in Writing & Docs