This is stupid. And simple. And it generates static HTML sites. Using Bash. Yikes. I'm only sharing it here so the world knows how bad of an idea this is. You have been warned.
Mostly me. But really anyone who likes HTML/CSS and getting stuff done without databases, package managers, toolchains, dependencies, build pipelines and the like. S4G can generate a fully-functional very simple site with just s4g init.
Not much. Mostly a blog. Maybe a photo gallery. Maybe a blog WITH a photo gallery. You could even forego the homepage "river-of-news" thing and create a completely custom homepage. If you, unlike me, are adept at CSS you can even make it look really nice. If you, unlike me, like Javascript, you can add whatever JS you'd like. Just customize the auto-generated header.html after you run s4g init the first time.
Me. For three sites (soon to be four!). They are all stupid. Just like this project.
- clone the repo and
cdinto it. chmod +x s4g.sh./s4g.sh init(answer the questions, the only ones you likely care about are Site Title and Site Description. For the rest you can/should accept the defaults)- (optional)
python3 -m http.server 8000
Bask in the glory of your new site.
-
./s4g.sh init- Asks you a couple questions (except for site title and site description, just accept the defaults...you'll dig it), then generates the bare bones structure of the site:config.yml sitemap.xml index.html --all-photos index.html --css style.css critical.css --feeds feed.xml feed.json --posts --hello-world index.md index.html --rss index.html --tags index.html --example index.html --intro index.html --templates header.html footer.html -
./s4g.sh scaffold- Scaffolds a post. Takes title as an argument. so,./s4g.sh scaffold "My Summer Vacation"creates a new folder inpostscalledmy-summer-vacationwith an index.md in there. Edit that and tell us all what you did last summer, little Johnny. Then run... -
./s4g.sh build- Builds the site. Basically, it goes through each post in/posts, converts the .md to .html by wrapping the contents of the markdown in the header and footer templates. It also keeps track of the tags and generates tag pages. It also generates the RSS feeds, the RSS feed index page (/RSS) and the sitemap.xml. It also takes any photos included in the post and adds it to the /all-photos page(s). You'll want to run./s4g.sh buildwhenever you update a .md file or style.css. Or whenever you want. Who cares?
There's nav in header.html. You need to manually edit it. I'm not going to decide for you what goes in your nav. MAKE IT YOURS! You can also edit footer.html. The only time the script writes to those files is in the init process.
If you edit your style.css (recommended), during the build process it gets a unique filename (for cache-busting purposes) and jammed into the header.
A lot of stuff is manual. That's intentional. I'm old. I like doing things the hard way.
You can then upload this entire folder to a web server somewhere or do python3 -m http.server 8000 or whatever to view your site. Note how super fast it is.
There's some other things
Each post .md file has some metadata at the top:
---
title: my summer photos
description:
<!--HTML-->
We had some fun this summer.

date: 2025-09-27
tags: photos, summer, hoagiefest, wawa
section: photos
featured_image: IMG_3535.jpeg
hide_from_feed: 0
photo_page: 1
---
some of these fields (title, date, tags) are obvious. others warrant some explaining:
- description will accept markdown. This is what gets output on the main /index.html and on the tag pages (like /tags/summer/index.html). You can also put straight HTML in here which may be your best bet because the parser is a little wonky...just do something like
description:
<!--HTML-->
<p class="content-body"><img src="/img/example.jpg" alt="An image" /></p>
- section whatever you add here just gets added as a class to the body tag of that page for easier CSS targeting, if you're into that kind of thing...so
<body class="photos"> - hide_from_feed set this to 1 if you don't want this to display on the main /index.html or in the main RSS feed...useful for about pages and the like.
- photo_page set this to 1 if you want the page to include a grid of photos (placed as a sub-folder of your post folder. So
/posts/my-summer-vacation/photos/The script will shrink them down and create athumbssub-folder in there. The filenames are used as captions. Not ideal, but, yeah. You could always make the grid yourself in your .md file if you want to get fancy (you can mix HTML and Markdown...probably). - featured_image Set this to specify the image used for Open Graph link previews. If it's not set, the fallback is
SITE_IMAGEfrom config.xml. - draft Set this to 1 for this post to be ignored/not published when you run
s4g.sh build.
- RSS - RSS feeds are auto-generated for the main index as well as individual tags AS WELL as the /all-photos stream. In addition, a page linking all available RSS feeds is generated at /rss. I'm a big fan of RSS.
- Photos - As alluded to previously, setting photo_page: 1 in the post index.md metadata will create thumbnails of each photo in the photos folder in an individual post and output a list of them in the index.html of said post. You can style them however you like.
- All photos - In addition to the above, a page is generated at /all-photos which lists (in a paginated form) all of the photos linked from posts on the site. There is also an RSS feed for this page.
- Link previews - Each post has Open Graph metadata added to it for neato link previews when sharing on the socials...like all the cool kids have.
- Moon phases - Let's make things fun. When
s4g buildruns, as it loops through the post .md files and generates the .html, it calculates the moon phase at the date of the post and outputs it at the bottom. You can hide it with CSS if you don't like this. I should probably set an optional per-post metadata setting for this, but for now, the moon phases will continue until morality improves.
I'm stupid and simple. Honestly, I just like HTML and the web. And accessibility. All this stupid thing does is generate HTML files. In a really inefficient and stupid way. With Bash, because I hate Python even more than I hate Javascript.
It works on my machine.
It may or may not work on yours.
Patches welcome.