Skip to content

Latest commit

 

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

This is deno-static

A minimal, value-based static site generator (SSG) for Deno.

deno-static favors a direct, WYSIWYG functional style over behavior through convention.

Read more:

  • Design - the ideas behind the library

  • Usage - how to set up your site

  • Helpers - various functions for common use cases

  • Patterns - ideas on how to structure your code effectively

  • Examples - sites built using deno-static

Design

The core data type is a recursive tree data structure represented by a record whose keys are URL path segments (e.g. /segment-1/segment-2/segment-3) and whose values are either plain Web API Response objects or further tree nodes. (Note: only the Response's body is relevant.)

type Tree = {
  [key: PathSegment]: TreeNode;
};

type PathSegment = string;

type TreeNode = Tree | TreeLeaf;

type TreeLeaf = Response;

(These types are simplified for demonstration.)

For example:

import { Tree } from "deno-static/mod.ts";

const tree = {
  "pokemon": {
    "pikachu.html": new Response("pika!"),
    "charizard.png": new Response(await Deno.readFile("charizard.png")),
  },
  "blog": {
    "posts": {
      "first-post.html": new Response("<h1>Hello!</h1>"),
    },
  },
} satisfies Tree;

Represents the following file hierarchy:

/pokemon/pikachu.html
/pokemon/charizard.png
/blog/posts/first-post.html

Additionally, there is a special key symbol index used to denote "pretty URLs" (e.g. /posts/first-post/). For example:

const tree = {
  [index]: new Response("home"),
  "blog": {
    [index]: new Response("My Blog!"),
  },
};

Represents:

/
/blog/

Finally, the site() function takes a Tree and 'renders' it to the file system.

Usage

  1. Set up your deno.json file:
{
  "imports": {
    "deno-static/": "https://cdn.jsdelivr.net/gh/garciat/deno-static/"
  },
  "tasks": {
    "build": "deno run --allow-all src/main.tsx",
    "serve": "deno run --allow-all --watch=./src/ src/main.tsx --dev"
  },
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "npm:react@^19",
    "strict": true
  },
  "lint": {
    "rules": {
      "exclude": ["no-import-prefix"]
    }
  }
}
  1. Write your site definition:
// src/main.tsx

import { index, jsx, site } from "deno-static/mod.ts";

await site({
  [index]: jsx(<h1>Hello, world!</h1>),
});
  1. Build your site:
deno task build
  1. Check the output:
# /_site/index.html

<h1>Hello, world!</h1>
  1. Alternatively, browse the site live (intended for development/debugging):
deno task serve
  1. Configure a GitHub workflow to build and deploy your static site.

    See .github/workflows/deploy.yml

Helpers

jsx

Renders a ReactNode into a Response. Supports asynchronous components.

(See renderToReadableStream)

import { index, jsx } from "deno-static/mod.ts";

await site({
  [index]: jsx(<h1>Welcome!</h1>),
});

json

Just a wrapper for Response.json().

import { json } from "deno-static/mod.ts";

await site({
  "data.json": json([
    { id: 1, name: "Jake" },
    { id: 2, name: "John" },
  ]),
});

response

Can be used to construct Responses from any object conforming to BodyInit.

import { response } from "deno-static/mod.ts";

await site({
  "feed.xml": response(`<rss><channel /></rss>`),
  "favicon.ico": response(generateIconToBuffer()),
});

Remember: the served file's MIME type is derived from its file extension. (See docs)

file

Lazily reads a local file into a Response.

import { file } from "deno-static/mod.ts";

await site({
  "favicon.ico": file(import.meta.resolve("./assets/favicon.ico")),
});

tree

Constructs a Tree from dynamic entries. For example:

import { jsx, tree } from "deno-static/mod.ts";

await site({
  "dynamic": tree(
    function* () {
      yield ["a.html", jsx(<h1>Hello!</h1>)];
      yield ["b.html", jsx(<h1>Bye!</h1>)];
    },
  ),
});

Also supports async generators.

treeMap

Map a sequence of inputs into Tree components.

import { jsx, treeMap } from "deno-static/mod.ts";
import { slugify } from "@std/text/unstable-slugify";

const posts = [
  { title: "Hello World", body: "This is a post" },
  // ...
];

await site({
  "posts": treeMap(
    posts,
    (post) => slugify(post.title),
    (post) => ({ [index]: jsx(<main>{post.body}</main>) }),
  ),
});

directory

Constructs a Tree from the specified file system directory.

import { directory, index, jsx } from "deno-static/mod.ts";

await site({
  [index]: jsx(<Page />),
  "assets": directory(import.meta.resolve("./assets")),
});

helpers.url

Adjusts an absolute path based on the BASE_URL environment variable.

It can also generate absolute URLs. (Useful for <link rel="canonical"> tags.)

⚠️ This is necessary when your site gets deployed under a directory. (See docs)

Patterns

Compute, then render

Idea: separate the concerns of data computation from site rendering.

// src/main.tsx

import { file, index, jsx, site, tree } from "deno-static/mod.ts";

import { computeAllSiteData } from "./data.ts";

import { HomePage } from "./pages/home.tsx";
import { PostPage } from "./pages/post.tsx";

// fetch & compute all of the data the site needs
const data = await computeAllSiteData();

// render it into files
await site({
  [index]: jsx(<HomePage posts={data.posts} />),
  "posts": tree(
    data.posts.map((post) => [post.slug, jsx(<PostPage post={post} />)]),
  ),
  "sitemap.xml": response(XML.stringify(data.sitemap)),
});

Centralized paths

Idea: avoid hardcoded URL paths.

// src/paths.ts

import { Post } from "./types.ts";

export const paths = {
  slugs: {
    posts: "posts",
    sitemap: "sitemap.xml",
  },
  home() {
    return "/" as const;
  },
  post(post: Post) {
    return `/${this.slugs.posts}/${post.slug}/`;
  },
};
// src/main.tsx

// (redacted)

await site({
  [index]: jsx(<HomePage posts={data.posts} />),
  [paths.slugs.posts]: tree(
    data.posts.map((post) => [post.slug, jsx(<PostPage post={post} />)]),
  ),
  [paths.slugs.sitemap]: response(XML.stringify(data.sitemap)),
});
// src/pages/home.tsx

import { helpers } from "deno-static/mod.ts";

import { paths } from "../paths.ts";
import { Post } from "../types.ts";

type HomePageProps = {
  posts: Post[];
};

export const HomePage: React.FC<HomePageProps> = ({ posts }) => (
  <main>
    <h1>
      <a href={helpers.url(paths.home())}>My Blog!</a>
    </h1>
    <ul>
      {posts.map((post) => (
        <li>
          <a href={helpers.url(paths.post(post))}>{post.title}</a>
        </li>
      ))}
    </ul>
  </main>
);

More

Check out the Examples for other emerging patterns.

Examples

Alternatives

  • Lume is a brilliant, batteries-included, Deno-native SSG solution.

Future Ideas

  • Compile/bundle TS/TSX code with esbuild

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages