| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
minimalistic static web/log cms
A minimalist, file-based web/log engine. Posts are plain JSON files, the site is a static build, and authoring happens through a CLI/wizard -- no database, no admin server, no PHP.
Who it's for: one person writing their own blog, at home in a terminal, who wants to own the whole archive -- including everything they already wrote somewhere else. Twenty-two import sources bring it in, from a WordPress export to a blog whose platform no longer exists. It deploys to Surfer, rsync, git-pages, rclone, SFTP or a local directory.
Who it isn't for: several authors sharing one site, anyone who needs a web admin interface, or a workflow where publishing isn't a command. It grew around a single deployment (sean.cz) and still fits that shape best, but it installs and runs as-is -- setup.sh asks the questions, and blogsh.app is a second site running the same unmodified engine.
See it running: blogsh.app is this engine publishing its own documentation -- every page there was built and deployed by ./blog.sh itself.
MIT licensed (see LICENSE); the fonts and brand marks it ships belong to other people, and NOTICE says who.
| Light | Dark |
|---|---|
![]() |
![]() |
The default blue palette -- both modes come entirely from the 7-key colors: section in config/site.yml (see install.md → The palette and the header's type).
Contents: Why this exists · What it does · Stack · Structure · Requirements · Getting started · Authoring · Configuration · Importing existing content · Exporting · Deploy · Roadmap · Example deployments
The build, the authoring tool and the deploy step are one program, built around one workflow rather than around the general case.
What you give up: themes, plugins, and a second author.
Each part says where it is described in full.
blog.sh Main tool -- CLI and interactive wizard (see below)
setup.sh Setup wizard -- identity, address, comments network, deploy target
style.sh Appearance wizard -- palette, banner, menu, about, footer, sidebar
import.sh Import wizard -- pick a source, preview, confirm (see below)
build/ Build script (JSON posts -> static HTML)
scripts/ Ruby CLI, import/deploy scripts, and their .sh wrappers:
deploy-web.sh standalone deploy of public.nosync/ to the configured target (no rebuild)
refresh-sidebar.sh cron: refreshes only the sidebar widgets (no site rebuild)
publish-scheduled.sh cron: publishes whatever the queue has come due
receive.sh takes a whole post over one SSH connection into incoming/
migrate_*.rb one per import source, scriptable alternative to import.sh
lib/ Shared Ruby libraries (Surfer client, fetchers, post writer, i18n, ...)
lib/import/ Import adapters plus the layer they share (media, run, CLI)
locales/ UI strings for the generated site and the CLI (en.yml, cs.yml, de.yml)
templates/ ERB templates (layout, post, index, search, partials)
+ markdown-cheat-sheet.<lang>.md, the /markdown/ page's source
assets/ CSS/JS/fonts (drop your own images into assets/images/)
write/ The page served at /write/ when write: true -- the editor itself, its
locale sources and the script that turns them into i18n.js
config/site.yml.example Documented config template -- copy to config/site.yml (gitignored) per deployment
env.sh.example Documented secrets/env template -- copy to env.sh (gitignored) per deployment
docs/ Install, operations, importing, architecture, decisions, skinning and
localization guides; this README's screenshots; and shortcuts/,
the two iOS shortcuts that send a post from a phone
content.nosync/, media.nosync/, public.nosync/, incoming/, trash/, drafts/, env.sh, config/site.yml
Per-deployment/generated, not part of the engine -- see .gitignore
content.nosync/posts/ (the posts themselves) and media.nosync/ (their images/videos) are deliberately not part of this repo: they're personal content, not code, specific to whoever deploys this engine for their own site. Likewise config/site.yml -- only the documented config/site.yml.example template is committed. The .nosync suffix also excludes both directories from iCloud sync on a Mac; on a server, where iCloud doesn't exist, it's just a name.
With Ruby 2.7+ on the machine. Coming from zero, there is a copy-paste path per platform in install.md → Quick start.
./setup.sh # title, address, timezone, comments network, deploy target
./style.sh # palette, banner, bio, footer, sidebar
./blog.sh doctor # is the configuration sound?
./blog.sh add # write the first post
./blog.sh rebuild # build and deploy
./blog.sh preview # or look at it locally first, on :8000Both wizards show a diff and write nothing until you confirm, and re-running either is how you change any of it later. By hand instead: copy config/site.yml.example and env.sh.example (chmod 600 env.sh) and edit them -- an unedited pair is already a working local site.
Everything beyond the core -- analytics, each sidebar widget, comments and the announcement -- activates only when its config section is present. Replace assets/images/favicon.png with your own artwork; it and the banner are gitignored, so they survive git pull and nothing else keeps a copy of them.
The whole path -- server install, every deploy backend, the phone workflow, the comments network -- is install.md; day-to-day usage is operations.md. What changed between versions is CHANGELOG.md, and ./blog.sh version says what you are running.
./blog.sh # interactive wizard (menu)
./blog.sh add [<file>] [--json] [--untrusted]
# creates a draft, shows a preview, asks what's next;
# with a markdown file it asks nothing, and --json answers as data;
# --untrusted refuses a picture reference that is not a bare filename
./blog.sh edit [<slug>] # without a slug, offers the last 50 posts
./blog.sh props [<slug>] # a post's state and its actions; [e] changes what the post IS --
# its series and part, tags, type, and the unlisted/hero/toc flags
./blog.sh publish [<slug>] [--yes] [--no-announce] [--json]
# shows the draft's preview, asks what's next;
# --yes publishes without asking, --no-announce keeps it off Mastodon and Bluesky;
# --json (with --yes) answers as one object, for a script or a phone
./blog.sh schedule [<slug>] # asks for a date, then auto-publishes the draft when it arrives
./blog.sh queue # what is scheduled, when each one goes, and the slots it uses
./blog.sh unpublish [<slug>] # moves a published post back to draft (also deletes its announcement)
./blog.sh delete [<slug>] # deletes a post to trash/
./blog.sh restore [<slug>] # restores a post from trash
./blog.sh empty trash # deletes everything in the trash, for good
./blog.sh empty versions # keeps each post's newest version, removes the older ones
./blog.sh toot [<slug>] # (re-)sends the comment toot (Mastodon sites)
./blog.sh bluesky [<slug>] # (re-)sends the announcement (Bluesky sites)
./blog.sh rebuild [--full] # rebuilds and deploys the whole site;
# --full builds every page again instead of only the changed ones
./blog.sh preview [<port>] # serves public.nosync locally (default 8000)
./blog.sh browse [--type=image] [--tag=foo] [--drafts]
# the archive on screen: filters, search, preview, Enter opens the post
./blog.sh list [--type=image] [--tag=foo] [--drafts]
# the same, printed one line per post
./blog.sh doctor [--online] # reads the configuration and says what is wrong with it
./blog.sh doctor --strip-location
# removes the place of capture from photos already in the archive
./blog.sh check [--online] [--json] [--repair]
# walks the archive and says what is broken in it;
# --json prints every finding as data instead of a screenful;
# --repair offers, per finding, the one repair that finding allows
./blog.sh export [<dir>] [--no-drafts] [--dry-run] [--force]
# writes the whole archive out as a tree of markdown files
./blog.sh stats [--json] # counts the archive: posts by year and kind, words, tags, media, sources
./blog.sh version # which version this installation is running
./blog.sh helpCopy env.sh.example to env.sh (gitignored) and fill it in -- chmod 600 env.sh, since it holds live credentials. It lives wherever the site is built from: on a server for a deployed site, or just on your laptop for a local one.
export SITE_BASE_URL=https://example.com
export MASTODON_ACCESS_TOKEN=... # comment toots (optional)
export TUMBLR_API_KEY=... # importing a Tumblr blog (wizard or script)
export DEPLOY_BACKEND=... # surfer (default) | local | rsync | git | rclone | sftp
export SURFER_URL=... # surfer backend
export SURFER_TOKEN=...
export SURFER_REMOTE_DIR=...
export DEPLOY_TARGET_DIR=... # local backend
export RSYNC_TARGET=... # rsync backend (+ optional RSYNC_SSH)
export GIT_PAGES_REMOTE=... # git backend (+ optional GIT_PAGES_BRANCH/_CNAME)
export RCLONE_TARGET=... # rclone backend (+ optional RCLONE_ARGS)
export SFTP_TARGET=... # sftp backend (+ optional SFTP_REMOTE_DIR/SFTP_ARGS)./import.sh # pick a source, preview, confirmAn imported post is indistinguishable from one you typed: same block schema, media downloaded locally. The wizard previews in dry-run before it writes, and re-running is safe -- posts are matched on their source id and overwritten in place.
| Source | Needs | Scope |
|---|---|---|
| beehiiv | the posts CSV export | newsletters and drafts; paid issues arrive as drafts |
| Blogger | the Atom backup file | posts and drafts; comments skipped, images at full size |
| Bluesky | nothing (public API) | your standalone posts; replies, reposts, quotes skipped |
| an unpacked export | your posts with their photos and video; crossposts skipped | |
| Ghost | the JSON export + the live site's URL | every post and page, drafts included |
| an unpacked export | your grid and IGTV; stories and archived posts skipped | |
| Jekyll/Hugo | the site tree (or any markdown folder) | posts and drafts with front matter; relative image paths come from the tree, absolute URLs download one request each -- import while the old host answers |
| LiveJournal | LJ_PASSWORD | every entry via the API; friends-only arrive as drafts |
| Mastodon | an unpacked account archive | standalone posts; boosts and replies skipped |
| Medium | an unpacked export | posts and drafts; responses to others become drafts |
| Movable Type/TypePad | the MT export file | posts and drafts; redirects need a URL pattern |
| Pixelfed | a statuses export | standalone posts with their photos |
| Podcast | a feed URL | every episode, audio and artwork hosted locally |
| Squarespace | the "WordPress format" XML export | posts, drafts and pages, feature images included |
| Substack | an unpacked export | newsletters and podcasts; paid posts tagged substack-paid |
| Threads | an unpacked export | your standalone posts; replies to others skipped |
| Tumblr | TUMBLR_API_KEY | every published post; a reblog keeps its trail |
| Twitter/X | an extracted archive export | standalone tweets; replies, RTs and quotes skipped |
| Wix | the blog CSV export | posts and drafts; unsupported node types counted by name |
| Wayback Machine | the dead blog's old URL | whatever the Archive saved, reassembled oldest-first |
| WordPress | a WXR export file | every post and page with its featured image and captions |
| RSS/Atom | a feed URL | whatever the feed carries, usually its last few dozen items |
One thing to decide before the first run: KEEP_PERMALINKS=1 writes the redirect_from list that keeps the old site's links working. Without it an archive imports with no redirects at all, and the only way back is another import.
What each source skips and why, the traps per platform, the variables, the non-interactive form and how to undo an import: importing.md.
./blog.sh export [<dir>] [--no-drafts] [--dry-run] [--force]An archive you cannot take with you is not yours. The whole thing comes out as markdown with YAML front matter, in the layout most other engines read -- and ./import.sh reads that tree back, posts keeping their identity, series, redirects and media, so export plus re-import is also how an installation moves. → operations.md
ruby build/build_blog.rb # rebuild into public.nosync/
./scripts/deploy-web.sh # uploads only new/changed files (SHA256 manifest)
./scripts/deploy-web.sh --prune # also deletes orphaned files on the target./blog.sh rebuild does both steps at once.
The sidebar widgets and per-post stats are refreshed by scripts/refresh-sidebar.sh -- it fetches the data, rewrites only the JSON files (six at most: toots, Pixelfed, commits, Bluesky, RSS, stats) and uploads just the ones the site's configured widgets produce, no site rebuild. Run it from cron wherever the site is built:
*/30 * * * * /path/to/blog.sh/scripts/refresh-sidebar.sh
Every 30 minutes is plenty -- the data it refreshes (recent toots, Pixelfed posts, commits, like/boost counts) doesn't move faster than that. Skip the cron entirely if no widgets are configured -- unless comments are moderated (comments.approval: fav): approved replies reach the site through this same job (it writes comments.json too), so a moderated site needs it with or without widgets.
A second, optional job powers ./blog.sh schedule -- it publishes scheduled drafts whose date has arrived (and does nothing otherwise):
*/15 * * * * /path/to/blog.sh/scripts/publish-scheduled.sh >/dev/null
What is not built, and why.
sean.cz is the blog this engine was extracted from -- Daniel Šnor's own, and the reference for what a fully configured deployment looks like.
blogsh.app is the project's own site, running the same unmodified engine and publishing its documentation as ordinary posts.
blog.elegantlich.com is the first deployment in hands other than the author's -- the planning blog of the Elegant Lich magazine. Its owner's first-install feedback is in the engine: an announcement that cannot happen now says why.
arch-linux.cz is the community blog of the Czech Arch Linux user group: the stock engine under its own stylesheet, no template edits, and the first site to pair it with GoToSocial -- which is where the GoToSocial comments in 1.4 were found.
blog.oscloud.cz is the news blog of OSCloud, a Czech self-hosting community, skinned the same way. The tag index and the copy button on code blocks were both their requests -- a blog of terminal how-tos wanted them first.
archive.bierfaristo.com is the largest archive we know of running this engine, some 13,700 posts -- a working answer to "does it scale". Its owner has reported more than anyone else outside.
| Back | FazBrowse Home | New Git URL |