JSX-powered SSG for Node.js. Write logic like React with a directory-structure like Next.js but generate plain HTML with no client side JS.
Getting started is very simple. You can use the create-dhow-app
npm package to quickly bootstrap a project based on a template.
npx create-dhow-app my-app # Optionally specify a template like this: `--template blog`
# For older versions of npm
npm i -g create-dhow-app
create-dhow-app my-app
The default template will show you the basic structure of a Dhow app but using something like the blog template will show you everything Dhow can offer.
If you would like you can also create a project from scratch without using create-dhow-app
. Let's walk through it.
# make a directory for your project
mkdir my-app
# change your directory
cd my-app
# initialize npm (optionally using `-y`)
npm init -y
# install dhow
npm i dhow
# create a `./pages` directory
mkdir pages
Once you're at this point add a few .js
files to the ./pages
directory. After that we can set up our scripts in package.json
. We're gonna add two scripts dev
to start the Dhow dev server & build
to build the files a single time.
{
"name": "my-app",
"version": "1.0.0",
"description": "Basic example using Dhow as a Static Site Generator",
"main": "index.js",
+ "scripts": {
+ "dev": "dhow dev",
+ "build": "dhow build"
+ },
"author": "",
"license": "MIT",
"dependencies": {
"dhow": "^1.2.1"
}
}
Dhow is basically a transpiler. It takes a .js
file like this:
import Dhow, { Head } from 'dhow'
export default () => (
<main>
<Head>
<title>Home page</title>
</Head>
<h3>This is my home</h3>
<p>On the internet obviously</p>
</main>
)
and converts it into a static HTML file like this:
<!DOCTYPE html>
<html>
<head>
<title>Home page</title>
</head>
<body>
<div class="dhow">
<main>
<h3>This is my home</h3>
<p>On the internet obviously</p>
</main>
</div>
</body>
</html>
You can also export an (optionally) async getProps
function from your file to fetch data. This will be run during build time & the props that it returns will be passed to your Head
component & default component.
import Dhow, { Head } from 'dhow'
import fetch from 'node-fetch'
export default ({ posts }) => (
<main>
<Head>
<title>Blog Posts</title>
</Head>
<h1>All the blog posts</h1>
<ul>
{posts.map((post) => (
<li>
<h3>{post.title}</h3>
</li>
))}
</ul>
</main>
)
export const getProps = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/posts')
const data = await res.json()
return { posts: data }
}
To generate multiple files using a single .js
file you can export an (optionally) async getPaths
function from your file. It should return an array of strings. Each of them will replace your filename in the end result. Each of the paths will also be passed to your getProps
function if you do export one.
import Dhow, { Head } from 'dhow'
import { readFile, readdir } from 'fs/promises'
import { join } from 'path'
import matter from 'gray-matter'
import marked from 'marked'
export default ({
post: {
content,
data: { title, date, description },
},
}) => (
<article>
<Head>
<title>{title}</title>
<meta name="description" content={description} />
</Head>
<h2>{title}</h2>
<p>
<small>{new Date(date).toDateString()}</small>
</p>
<p>{description}</p>
<h4>―</h4>
<div html={content}></div>
</article>
)
export const getPaths = async () => {
const files = await readdir('./content')
return files.map((path) => path.slice(0, path.length - 3))
}
export const getProps = async (slug) => {
let post = await readFile(join('./content', `${slug}.md`), 'utf-8')
post = matter(post)
post.content = marked(post.content)
return { post }
}
Dhow uses PostCSS under the hood to process all your CSS files. This means you can create a postcss.config.js
file in the root of your directory, and Dhow will use the plugins you use in that file (you can see this in the TailwindCSS example).
Note: Dhow unlike some bundlers (like Parcel) uses no plugins by default
To manage the contents of the document head you can use the Head
component that Dhow exports. Import it like this:
import Dhow, { Head } from 'dhow'
And then whatever you put inside it will be inserted into the paage head at build time:
import Dhow, { Head } from 'dhow'
export default () => (
<main>
<Head>
<title>Hello there</title>
</Head>
<h1>Hello world</h1>
</main>
)
/* Will become this: */
<html>
<head>
<title>Hello there</title>
</head>
<body>
<div class="dhow">
<h1>Hello world</h1>
</div>
</body>
<html>
The Head
component prioritizes children components. Do if you have a Head
component on the parent & on the child. The childs Head
contents will completely override the page head. Example:
import Dhow, { Head } from 'dhow'
const Child = () => (
<div>
<Head>
<title>Hello there from the child</title>
</Head>
<p>I'm a nested component</p>
</div>
)
export default () => (
<main>
<Head>
<title>Hello there</title>
</Head>
<h1>Hello world</h1>
<Child />
</main>
)
/* Will become this: */
<html>
<head>
<title>Hello there from the child</title>
</head>
<body>
<div class="dhow">
<h1>Hello world</h1>
<p>I'm a nested component</p>
</div>
</body>
<html>
This is similar to the behaviour in other libraries like Helmet
Similarly to Next.js, Dhow optionally allows you to specify a custom document for shared document-level markup. Just like Next.js you do this by creating a _document.js
file in your pages directory. This file can export any sort of JSX, with the only requirement being that you must have one element with a class
of dhow
nested somewhere within. That element is where Dhow will insert all your page markup during build time.
Here's an example of a custom document used to add some global CSS & scripts to all pages:
/* ./pages/_document.js */
import Dhow from 'dhow'
const Document = () => (
<html lang="en">
<head>
<link rel="stylesheet" href="/global.css" />
</head>
<body>
<div class="dhow">
{/* this is where your page content will end up */}
</div>
<script src="/global.js"></script>
</body>
</html>
)
export default Document
Behind the scenes Dhow is actually pretty simple, it uses min-document
& esbuild
to create fake DOM nodes from your JSX.
As a CLI tool Dhow takes .js
files from your ./pages
directory & uses esbuild to compile it into non-JSX. Then it calls your default export function and appends the element it returns to a .dhow
div in the document. If you do export a Head
function then the contents of that are added to the <head>
of the document. Then the outerHTML
of this document is saved into an html
file corresponding to the path of your source file.
If you export a getProps
function then the results of that function are passed to your default & Head
component. If you export a getPaths
function then the same file is evalauated once for each path. Each path is also passed to getProps
(if it exists) so you can fetch path-specific data. While it is not necessary you can use square brackets around the name of a file that exports a getPaths
function to remain true to Next.js (e.g [fileName].js
)
Feel free to add any features you might find useful. Just open an issue and we can go there. If you find a bug you can also open an issue but please make sure to include details like your system, node version, etc.