# btmx A self-contained hypermedia-driven blogging platform in a single portable binary. Written in Lua for the [RedBean](https://redbean.dev/) webserver, routed with [Fullmoon](https://github.com/pkulchenko/fullmoon), rendered as hypermedia with [HTMX](https://htmx.org/), and stored in SQLite. The build produces one executable (`blog.com`) that runs on Linux, BSD, macOS, and Windows; content lives in `blog.db` next to it. ## Features - **Single binary:** no dependencies, no installation, no configuration files. Just run `./blog.com` and it just works™. - **Markdown support:** posts are Markdown, rendered into HTML. [highlight.js](https://highlightjs.org/) is included for code blocks. - **Radio Station:** web radio included as an optional feature for a given .m3u playlist URL. - **RSS/Atom:** Atom feed is generated automatically at `/atom.xml`. - **Themes:** attempts to respect user preferences for light or dark mode. Also includes a retro theme. - **Admin dashboard:** `/edit` does almost everything needed to manage the blog. ## License This project is licensed under the GPL-3.0 License - see the [LICENSE](LICENSE) file for details. ## Requirements All requirements are pulled during maketime with `curl` using GNU `make` (if Justine has not updated her TLS certs, you may need to grab redbean by hand.) Because it's awesome, the fatbin should work robustly on most OSes; daemons may need to use the `ape` loader, rather than running the binary directly. ## Building ```bash git clone https://git.asperger.pro/hpcdisrespecter/btmx.git cd btmx make # fetch redbean/fullmoon/htmx/highlight.js/fonts and build blog.com make setup # set the admin password (creates blog.db) ``` There is nothing to edit before building: all site identity (title, author, base URL, …) is configured after first login at `/edit/settings`. ## Running ### Configuration Resetting the password and setting up the database: ```bash ./blog.com -i setup_admin.lua ``` ### Development ```bash ./blog.com ``` Runs on `localhost:8080` and attempts to open your default browser to the blog (set `BLOG_NO_BROWSER=1` to suppress). Note that the Lua application is zipped **inside** `blog.com`; after editing anything under `.lua/` or `static/`, run `make` again. ### HTTPS ``` ./blog.com -p 443 \ -K privkey.pem \ -C fullchain.pem \ -J ``` Runs on `localhost:443` with TLS and mandatory HTTPS using the provided certificates. ### Daemon #### Systemd Service First, produce a daemon user and copy the application files to its home directory: ```bash sudo useradd -r -s /usr/sbin/nologin -d /var/lib/blog blog cp blog.com /var/lib/blog/blog.com cp blog.db /var/lib/blog/blog.db sudo chown -R blog:blog /var/lib/blog ``` Then, deploy your TLS certificates to `/var/lib/blog/privkey.pem` and `/var/lib/blog/fullchain.pem` with similar permissions. Consider using a hook in your certificate management tool to automate this process. Finally, make sure your system has a working [ape](https://justine.lol/ape.html) loader. ``` which ape ``` Create `/etc/systemd/system/blog.service`: ```ini [Unit] Description=Hypermedia Blog (Redbean) After=network.target [Service] Type=simple User=blog WorkingDirectory=/var/lib/blog Environment=BLOG_NO_BROWSER=1 ExecStart=ape \ /var/lib/blog/blog.com -p 443 -p 80 \ -K /path/to/privkey.pem \ -C /path/to/fullchain.pem \ -J Restart=on-failure AmbientCapabilities=CAP_NET_BIND_SERVICE CapabilityBoundingSet=CAP_NET_BIND_SERVICE NoNewPrivileges=true [Install] WantedBy=multi-user.target ``` Then enable and start the service: ```bash sudo systemctl enable blog.service sudo systemctl start blog.service ``` #### OpenRC Service As above, you produce a daemon user, a copy of the application files, and deploy your TLS certificates. Then, create `/etc/init.d/blog`: ```bash #!/sbin/openrc-run name="blog" directory="/var/lib/blog" command="/usr/bin/ape" command_args="/var/lib/blog/blog.com -p 443 -p 80 \ -K /path/to/privkey.pem \ -C /path/to/fullchain.pem \ -J" command_group="blog" command_background="yes" pidfile="/run/${RC_SVCNAME}.pid" output_log="/var/log/blog.log" error_log="/var/log/blog.err" export BLOG_NO_BROWSER=1 depend() { need net } ``` Make the script executable and add it to the default runlevel: ```bash sudo chmod +x /etc/init.d/blog sudo rc-update add blog default ``` Finally, start the service: ```bash sudo rc-service blog start ``` ## Notes - **Database**: `blog.db` is created automatically at first request in the working directory. Back it up and you have backed up the whole site (posts, uploads, settings, password hash). - **Admin access**: Visit `/login`, enter your password. A session cookie (HttpOnly, SameSite=Strict, 7-day TTL) is set on success. Sessions are stored in `blog.db` and survive server restarts. `/edit` is the admin dashboard; `/edit/settings` configures the site. - **Atom feed**: set the **Base URL** in Settings to your public origin (e.g. `https://example.com`) so feed IDs and links are absolute. - **Rate limiting**: Login is limited to 5 attempts per IP per 15 minutes (in-memory; resets on restart). - **CSRF tokens**: Every mutating form includes a `csrf_token` hidden field validated against an HttpOnly SameSite=Strict cookie. - **Password changes**: Re-run `./blog.com -i setup_admin.lua` at any time. The new hash overwrites the old one immediately.