img
Go back

Read

Published on: Jun 1, 2025

Programming

Making a tool to preview static sites

When you’re developing a React, Vue, Django… well, basically any type of web app, either back or front, you always need to set up a development server, a way to serve all of the files inside of your project so you can preview the stuff you’re working on.

Modern libraries have this feature built in, hell, even some IDEs like VSCode have extensions that help you with this.

But what if you’re using something like Vim, or simply you’re developing a static site with some HTML and JS files, then everything else seems kind of overkill.

I develop a lot of static sites, and sometimes things can get a little bit crazy, and I have to keep uploading new versions of those sites to their respective servers.

Sometimes there’s no time to open an IDE to preview them, and this can lead to me making some mistakes and uploading the wrong version or files to production.

It can be a mess.

So I developed a tool so i can preview any folder from any internet navigator as long as i have the terminal open using Deno.

The Idea

The goal was simple: create a static file server that:

  1. Serves any folder of HTML files.
  2. Automatically maps files to routes based on the folder structure.
  3. Opens the served folder in the default browser.
  4. Requires minimal setup and dependencies.

The Code

1. Entry Point

The entry point of the application parses command-line arguments and starts the server.

import { parseArgs } from "jsr:@std/cli/parse-args";
import Parse from "./parse.ts";
import Directory from "./directory.ts";
import Serve from "./serve.ts";

const PATH = "path";
const PORT = "port";

async function main(): Promise<null | boolean> {
  const { path, port } = parseArgs(Deno.args, { string: [PATH, PORT] });
  console.clear();
  const [parsed_path, parsed_port] = [Parse.path(path), Parse.port(port)];
  const directory = new Directory();
  await directory.read(parsed_path);
  Serve.dir(parsed_path, directory.endpoints, parsed_port);
  console.log(`Folder served at http://localhost:${parsed_port}`);
  return true;
}

main();

Here’s what happens:

  • The “parseArgs” function extracts the “—path” and “—port” arguments.
  • Default values are provided using the “Parse” utility.
  • The “Directory” class scans the folder and maps routes.
  • The “Serve” class starts the HTTP server.

2. Parsing Arguments

This utility ensures that the “path” and “port” arguments are valid

export default class Parse {
  static port(str?: string | undefined): number {
    return str !== undefined ? Number(str) : 9999;
  }
  static path(srt?: string | undefined): string {
    if (srt === undefined) {
      return Deno.cwd();
    }
    return srt;
  }
}
  • If no “—path” is provided, it defaults to the current working directory.
  • If no “—port” is provided, it defaults to “9999”.

3. Directory Traversal

This class scans the folder recursively and maps “.html” files to routes.

export default class Directory {
  endpoints: { [key: string]: Array<string> } = {};
  regex = new RegExp(/.html/);
  async read(path: string) {
    await this.get_endpoints(path, "/");
  }
  async get_endpoints(path_name: string, folder_name: string): Promise<void> {
    const endpoint_arr: Array<string> = [];
    for await (const file of Deno.readDir(path_name)) {
      if (file.isDirectory) {
        await this.get_endpoints(
          `${path_name}/${file.name}`,
          `${folder_name}${file.name}/`,
        );
      } else if (file.name.match(this.regex)) {
        let endpoint = file.name.split(this.regex)[0];
        if (endpoint === "404") {
          endpoint = "(.*)";
        }
        endpoint_arr.push(endpoint);
      }
    }
    endpoint_arr.sort((b, a) => a.localeCompare(b));
    if (endpoint_arr.length > 0) this.endpoints[folder_name] = endpoint_arr;
  }
}
  • The “read” method starts scanning the folder.
  • The “get_endpoints” method:
    • Recursively traverses directories.
    • Maps “.html” files to routes.
    • Special handling for “404.html” (catch-all route).

The logic behind my naming convention is to make sense of everything from outside of the class, eg:

  Serve.html();
  Directory.read();
  Serve.dir();

In my option this is just clean.


4. Serving Files

This class initializes the HTTP server and registers routes.

import { Application, Context } from "@oak/oak";
import { Route, RouteFactory } from "./route.ts";

export default class Serve {
  static regex = new RegExp(/\/+/g);
  static dir(
    path: string,
    endpoints: { [key: string]: Array<string> },
    port: number,
  ) {
    const app = new Application();
    const r = new Route();
    const entries = Object.entries(endpoints);

    this.loop(entries, (item) => {
      const [inner_path, routes] = item;
      this.loop(routes, (route) => {
        const route_data = new RouteFactory(route, path, inner_path, port);
        r.append_to(route_data.route_name, route_data.full_name);
      });
    });

    app.use(r.get().routes());
    app.use(async (context, next) => {
      try {
        await context.send({ root: path });
      } catch {
        next();
      }
    });
    app.listen({ port });
  }

  static loop<T>(arr: T[], func: (t: T) => void) {
    let i = 0;
    while (i < arr.length) {
      func(arr[i]);
      i++;
    }
  }

  static async html(ctx: Context, full_name: string) {
    try {
      const decoder = new TextDecoder("utf-8");
      const file = await Deno.readFile(full_name);
      const text = decoder.decode(file);
      ctx.response.body = text;
      ctx.response.type = "text/html";
      ctx.response.status = 200;
    } catch (err) {
      console.log("error loading file");
      console.log(err);
    }
  }
}
  • The “dir method:
    • Registers routes dynamically based on the directory structure.
    • Serves static files directly if no route matches.
  • The “html method reads and serves HTML files.

5. Opening the Browser

This class opens the served folder in the default browser.

export default class CommandFactory {
  public command: Deno.Command;
  constructor(os_name: string, port: number) {
    const url = `http://localhost:${port}/`;
    const args_arr = [url];
    let command_name = "";
    switch (os_name) {
      case "windows":
        command_name = "cmd";
        args_arr.push("/c", "start");
        break;
      case "darwin":
        command_name = "open";
        break;
      default:
        command_name = "xdg-open";
        break;
    }
    this.command = new Deno.Command(command_name, { args: args_arr });
  }
  execute() {
    this.command.spawn();
  }
}
  • Detects the operating system.
  • Constructs the appropriate command to open the browser.

Why This Tool?

This tool is perfect for:

  • Quickly previewing static sites without setting up a full development environment.
  • Fast workflows.
  • Avoiding mistakes when uploading files to production.

Give it a try and simplify your static site development workflow!

Check it out on Github

You may like: