ssgo v1.0.6
ssgo
A minimalist, unconfigurable static site generator.
⚠️ This is a projet I maintain on my spare time and that I haven't fully tested yet. It most surely has unknown issues. It has to be used with care.
Installation
yarn add ssgo -D
Usage
At the root of your project, to generate a bundle:
npx ssgo
The site will be built and outputed inside of the dist
directory.
The following options are accepted:
option | description |
---|---|
--watch | watch files and rebuild on change |
--serve | serve the bundle on localhost:3003 |
How does it work ?
ssgo
assumes that your projet directory has the following structure:
/
├── pages/
├── components/
└── static/
The pages
folder:
This is the only mandatory folder. ssgo
needs it to work.
pages
is the directory where lies the scrips that will generate your pages. For example, the content of pages
for a static blog project could be:
└── pages/
├── templates
| ├── index.html
| ├── contact.html
| └── post.html
├── index.ts
├── contact.js
└── blog-posts/
└── post.js
ssgo
will recursively search for .js
files inside of pages
directory, and execute the function it exports, giving it a single argument, the buildPage
function.
Note: Nothing forces you to have multiple files inside of
pages
directory. One can have a single file, with as much data fetching as wanted and as much calls tobuildPage
as wanted.
The post.js
(ts
is also supported) file would look something like that:
const { fetchPostsFromApi } = require(api.ts);
module.exports = async (buildPage) => {
// path must be relative to the root of the project directory
const template = "/templates/post.html";
// here, fetch all the datas you need to build your pages
// from an API, the filesystem or even a database !
const blogPosts = await fetchPostsFromApi();
// each call to buildPage will parse the template file,
// do the interpolation and evaluation work,
// and create the built html file inside of dist directory
for (let post of posts) {
const postContext = {
utils: {
isMinPlural: (time) => time > 1,
capitalize: (str) => str.charAt(0).toUpperCase() + str.slice(1),
},
post: post,
};
buildPage(template, postContext, {
filename: post.slug,
directory: "posts",
});
}
};
While the template it uses, post.html
, would look like that:
<!-- here, let's assume the `post` object had a title, a description, a readTime, and a content. -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- see section `The attributes` to understand about eval: and static: -->
<meta name="description" eval:content="description" />
<link rel="stylesheet" static:href="../../static/stylesheets/style.scss" />
<title>{post.title}</title>
</head>
<body>
<!-- custom component given a `rt` prop: see `The components directory` section -->
<my-header rt="post.readTime"></my-header>
<h1>{{utils.capitalize(post.title)}}</h1>
<small>Read time: {post.readTime} minute{utils.isMinPlural && 's'}</small>
<h2>{post.description}</h2>
<p
eval:class="{ 'text-sm': post.content.lenght > 500, 'font-bold': post.content.lenght > 300 }"
>
{post.content}
</p>
</body>
</html>
ssgo
supports TypeScript out of the box for pages/
scripts.
Templates paths given to the buildPage
function will be assumed as relative to the root of your project directory.
The components
directory:
components
is the directory where lies your custom components. Following the same 'blog' example from before, we could have the following components
directory:
└── components/
├── my-header.html
├── my-footer.html
└── post-preview.html
ssgo
will search for .html files at the root of this directory (not recursively yet), and make components usable inside of pages templates.
The post-preview.html
file could look something like that:
<div eval:id="'post-preview__' + index ">
<b class="post-preview__title">{post.title}</b>
<i>{post.readTime} minute{plural && 's'} read</i>
</div>
And we could use it, for example in the index.html
page template as following:
<!-- [...] -->
<div id="previews">
<!-- see section `The attributes` to understand about 'for' and 'of' attributes -->
<post-preview
post="post"
plural="post.readTime > 1"
for="post"
of="blogPosts"
></post-preview>
</div>
<!-- [...] -->
The static
directory:
static
is the directory where lies all the static ressources your site might need, like scripts, stylesheets, images... Following the same 'blog' example from before, we could have the following static
directory:
└── static/
├── scripts/
| ├── index.ts
| ├── vendor.js
| └── why-not.coffee
├── stylesheets/
| └── style.scss
└── media/
└── images/
└── logo.png
All this static ressources could be used inside of any page template as following:
<!-- [...] -->
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" static:href="../../static/stylesheets/style.scss" />
<script static:src="../../scripts/index.ts"></script>
<script static:src="../../scripts/vendor.js"></script>
<script static:src="../../scripts/why-not.coffee"></script>
</head>
<!-- [...] -->
<img static:src="../../media/images/logo.png" />
<!-- [...] -->
The static:
attribute prefix will make these files to be resolved, minified and added to the bundle inside of the dist/static
directory, thanks to the awesome Parcel bundler.
The attributes:
ssgo
makes available the following attributes:
if
Evaluates the expression it is given and conditionally lets the node inside of the template. Example:
<!-- The template... -->
<p if="true">eeny</p>
<p if="false">meeny</p>
<p if="Math.random() > 0.5">miny</p>
<p if="anyVarInContext === true">moe</p>
<!-- Will output... -->
<p>eeny</p>
<!-- sometimes not ;) -->
<p>miny</p>
<!-- only if 'anyVarInContext' equals 'true' -->
<p>moe</p>
for
/ of
Iterates over the evaluation of what is given to of
and creates the key given to for
inside of the context. Example:
<!-- The template... -->
<p for="fruit" of="fruitsBasket">{index + 1} - {fruit.name}</p>
<!-- Will output... -->
<p>1 - Banana</p>
<p>2 - Apple</p>
<p>3 - Kiwi</p>
The index
key, representing the actual item index, will also automatically be added to context during iteration. Caution, if an index
already exists into your context, it will override the one produced by for
/ of
.
for
/ of
can be used on custom components as well.
eval:
(prefix)
Evaluates the expression it is given and assign the following attribute's value the result. Example:
<!-- The template... -->
<p eval:foo="foo.toUpperCase()"></p>
<!-- Will output... -->
<p foo="HELLO WORLD"></p>
It can be used on every tag, and with every attribute, except on custom components. Props passed to custom components are already automatically evaluated.
It can also be paired with static:
to allow resolving paths stored inside context variables. For example, eval:static:src="scriptPathStoredInContext"
, will first evaluate scriptPathStoredInContext
and then resolve the path it contains.
static:
(prefix)
Resolves the path it is given and adds it to files to be bundled. Example:
<!-- The template... -->
<link rel="stylesheet" static:href="../../static/style.scss" />
<!-- Will output... -->
<link rel="stylesheet" href="/static/style.css" />
What about runtime ?
ssgo
is a static site generator, so it doesn't bother about runtime at all. But, is your app needs to make stuff modern frameworks allow you to do, you can use the excellent AlpineJS that will pair nicely with ssgo
.
For example, you can eval expression to be given to alpine's x-data
at build by doing eval:x-data="{ foo: foo, bar: bar }"
, that will output x-data="{ foo: 'hello', bar: 'world' }"
.
Remaining work
- Support TS out of the box for pages/ files
- Add a flag to allow minification of output HTML
- Add a flag to serve only without rebuilding
- Allowing nodes inside of custom-components
- Parsing nested custom components inside of
components/
folder - Improve path resolving for template files
- Allow merging
class
andeval:class
- Handle errors for two static files resolving to the same exact path
- Handle errors for two buildPage calls to the same output file
- Add caching for data given to buildPage, and rebuilt with the old data when file changes concerns a template / component file