A custom static website generator for https://derekenos.com.
-
Not too many featuresSeriously, it's gone off the rails with the Microdata and responsive images stuff. -
No third-party libraries
-
Aspires to accessibility and SEO best practices
-
Search engines don't hate it
-
Optionally redirect large static files to a cloud object store
-
Implements responsive images
-
Supports RelMeAuth
-
Intergrates Microdata markup to provide structured data for search engines (example)
-
Intergrates OpenGraph tags for rich embeds, e.g.:
Python 3.9
bash
inotifywait
Dependency | Tested Version | Needed For |
---|---|---|
Gimp | 2.10.22 | image width detection and derivative generation |
VLC | 3.0.8 | video poster image generation |
LSS Type | Dependencies |
---|---|
S3 | aws-cli |
This project includes a couple of my other repos as submodules:
- htmlephant - Lazy HTML Generator
- femtoweb - Async HTTP & Web Application Server
Clone the generic branch:
git clone --recurse-submodules -b generic https://github.com/derekenos/derekenos.com-generator
The generic branch includes some space-themed content which you can view by launching the dev server:
cd derekenos.com-generator
./dev.sh
you should see:
Wrote 17 pages to: site/
Serving at: http://0.0.0.0:5000
Surfing over to http://localhost:5000 should look something like:
or
Edit context.json and static/shared.css to your liking.
Define an array of project images in the context file as follows:
<project>: {
...
"images": [
{
"filename": "<original-image-normalized-filename>",
"name": "<image-name>",
"description": "<image-description>"
}
]
}
Where <original-image-normalized-filename>
names the original, full-size image file in static/
(or custom static dir) and conforms to the normalized_image_filename_template
defined in context.json
.
For example, given the template:
"{item_name}-{asset_id}-{width}px-original{extension}"`
A valid filename is:
project-static-site-generator-4cb061e3-1146px-original.webp
The current code does not present original image files, which are assumed to be large and/or not well supported by web browsers, directly. Instead, derivatives of the original, in web-friendly formats (as defined by prioritized_derivative_image_mimetypes) and a variety of sizes (as defined by derivative_image_widths), are assumed to have been generated, and will be included as options from which the browser will select at will.
Derivative filenames must have the same item_name
and asset_id
as the original file and, as defined by derivative_image_filename_template, have the format:
{item_name}-{asset_id}-{width}px{extension}
Given the previous original filename example, a valid derivative filename is:
project-static-site-generator-4cb061e3-750px.webp
Here's an example of how all derivatives are presented in the source for the first image on this page:
<picture>
<source
srcset="https://derekenos-com.nyc3.cdn.digitaloceanspaces.com/project-static-site-generator-4cb061e3-1146px.webp 1146w,
https://derekenos-com.nyc3.cdn.digitaloceanspaces.com/project-static-site-generator-4cb061e3-1000px.webp 1000w,
https://derekenos-com.nyc3.cdn.digitaloceanspaces.com/project-static-site-generator-4cb061e3-750px.webp 750w,
https://derekenos-com.nyc3.cdn.digitaloceanspaces.com/project-static-site-generator-4cb061e3-500px.webp 500w,
https://derekenos-com.nyc3.cdn.digitaloceanspaces.com/project-static-site-generator-4cb061e3-300px.webp 300w,
https://derekenos-com.nyc3.cdn.digitaloceanspaces.com/project-static-site-generator-4cb061e3-100px.webp 100w"
sizes="90vw"
type="image/webp">
<source
srcset="https://derekenos-com.nyc3.cdn.digitaloceanspaces.com/project-static-site-generator-4cb061e3-1146px.jpg 1146w,
https://derekenos-com.nyc3.cdn.digitaloceanspaces.com/project-static-site-generator-4cb061e3-1000px.jpg 1000w,
https://derekenos-com.nyc3.cdn.digitaloceanspaces.com/project-static-site-generator-4cb061e3-750px.jpg 750w,
https://derekenos-com.nyc3.cdn.digitaloceanspaces.com/project-static-site-generator-4cb061e3-500px.jpg 500w,
https://derekenos-com.nyc3.cdn.digitaloceanspaces.com/project-static-site-generator-4cb061e3-300px.jpg 300w,
https://derekenos-com.nyc3.cdn.digitaloceanspaces.com/project-static-site-generator-4cb061e3-100px.jpg 100w"
sizes="90vw"
type="image/jpeg">
<img
src="https://derekenos-com.nyc3.cdn.digitaloceanspaces.com/project-static-site-generator-4cb061e3-750px.jpg"
loading="lazy"
alt="A picture of the Static Site Generator">
</picture>
You can see that the first <source>
offers a bunch of next-gen webp
s because they're rad. The second <source>
offers jpg
s which are not as awesome but are well-supported. The <img>
at the end serves as a fallback for browsers that don't support the <picture>
tag.
The process-assets.py
script can take care of all this nightmare filename normalization and derivative generation for you, and has this sweet auto
action that accepts:
- the path to a directory of misfitly-named, project-specific images and videos
- the name of a project already defined in
context.json
- the path to the website generator static asset directory
and automatically does all of:
- copy files to a mysterious temporary location with normalized names
- generate all required derivatives
- update the project's
images
andvideos
fields incontext.json
- move all the normalized original and derivative files into the static dir
I'm sure I've documented that somewhere in here. 👀
Define an array of project videos in the context file as follows:
<project>: {
...
videos": [
{
"filename": "<original-video-normalized-filename>",
"name": "<video-name>",
"description": "<video-description>"
}
]
}
Like images, videos are expected to have a name that conforms to a template, as defined by normalized_video_filename_template, like:
{item_name}-{asset_id}-original{extension}
A valid filename is:
project-weather-station-82fc52a8-original.mp4
The only required derivative for a video is the poster image, which also has a template, as defined by derivative_video_poster_filename_template, like:
{base_filename}-poster.jpg
Why did I not make that {item_name}-{asset_id}-poster.jpg
? I'll endeavor to fix this.
A valid poster image filename is:
project-weather-station-82fc52a8-original-poster.jpg
Add your images, videos, etc. to static/
.
Execute run.py to build the site.
$ python3.9 run.py --help
usage: run.py [-h] [--development] [--context-file CONTEXT_FILE] [--serve]
[--host HOST] [--port PORT] [--sync-large-static]
optional arguments:
-h, --help show this help message and exit
--development
--context-file CONTEXT_FILE
--serve
--host HOST
--port PORT
--sync-large-static
The generated output files will be written to the site/
directory.
python3.9 run.py --context-file=context.json --development --serve
This script executes a development build, starts the server, and watches for local filesystem changes - when a change is detected, it rebuilds the site and triggers a refresh in the browser, allowing you to see changes in real time without having to manually refresh.
./dev.sh
python3.9 run.py --context-file=context.json
mode | Live Dev | Google Analytics |
---|---|---|
development | yes | no |
production | no | yes |
To avoid making your static site host angry by jamming up its platters with your gratuitous media, the Large Static Store feature allows you to sync static files that exceed some size threshold to a cloud object store of your choosing.
The value of Context.STATIC_LARGE_FILE_THRESHOLD_MB determines the size threshold (in MBs) over which static files will be redirected to the remote store.
Define a large_object_store
property in your context file.
Here's an example of my S3 store config:
"large_static_store": {
"type": "S3",
"aws_profile": "digitalocean",
"aws_endpoint": "https://nyc3.digitaloceanspaces.com",
"s3_url": "s3://derekenos-com/",
"endpoint": "https://derekenos-com.nyc3.cdn.digitaloceanspaces.com"
},
Currently, only the S3 storage class is defined, but it should be fairly straight-forward to define others, making sure that your config specifies type
+ whatever properties the class requires.
Specifying the --sync-large-static
CLI option to run.py
will sync your local, large static files to the remote store.
Example:
python3.9 run.py --sync-large-static
Once you've synced your large files to the remote, you can delete the local copy and development builds will continue to work as expected (providing you have network access) by automatically resolving missing local files to their URLs in the remote store. In fact, when you execute a development build (i.e. run.py --development
or dev.sh
), if any large files exist both locally and in the remote store, you'll see a warning like the following:
$ python3.9 run.py --development
Large, local file "weather_station.mp4" exists in the LSS.
Wrote new files to: site/
During a build, large_static_store.Store.exists() is invoked to check whether a local, large static file exists in the remote store. For more efficient lookups, the class creates and updates a local manifest file named (by default) .lss_manifest.json
.
Calling Store.exists()
makes a HEAD
request to the remote store endpoint for a specified path to check whether the object exists.
Response headers for found objects are saved in the manifest to serve as proof of existence and to provide metadata required at build time.
This file has the format:
{
"<store-endpoint>": {
"<object-key>": {
<HEAD-response-header>,
<HEAD-response-header>,
...
},
...
}
Example:
{
"https://derekenos-com.nyc3.cdn.digitaloceanspaces.com": {
"weather_station.mp4": {
"Date": "Thu, 21 Jan 2021 15:17:59 GMT",
"Content-Length": "80709540",
"Content-Type": "video/mp4",
"Last-Modified": "Thu, 21 Jan 2021 03:15:50 GMT"
...
}
}
}
If your local manifest is out-of-sync with the remote, simple delete the file and the it will be created anew, dispatching fresh queries to the remote, on the next build.