Blog Engine with Fediverse and Newsletter support, WYSIWYG editor, and more.
  • JavaScript 81.5%
  • Go 11.9%
  • Go Template 3.9%
  • Python 1.5%
  • Less 1%
  • Other 0.1%
Find a file
loweel 176ba023f6
All checks were successful
continuous-integration/drone/push Build is passing
Configure Docker deployments through environment variables
2026-07-23 13:20:53 +02:00
.github Initial Blogfrei import 2026-06-12 23:34:46 +02:00
appstats Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
author Initial Blogfrei import 2026-06-12 23:34:46 +02:00
blogfrei Render Blogfrei horizontal rules 2026-06-20 13:44:29 +02:00
cmd/writefreely Add Bilberry theme and complete Blogfrei rebrand 2026-06-13 17:28:46 +02:00
config Configure Docker deployments through environment variables 2026-07-23 13:20:53 +02:00
db Initial Blogfrei import 2026-06-12 23:34:46 +02:00
key Initial Blogfrei import 2026-06-12 23:34:46 +02:00
keys Initial Blogfrei import 2026-06-12 23:34:46 +02:00
less Add owner read count toggle 2026-06-28 21:24:03 +02:00
LICENSES Strengthen mixed-license documentation 2026-06-14 19:16:26 +02:00
mailer Send newsletter posts one email at a time 2026-07-03 16:19:26 +02:00
migrations Rotate detailed view stats monthly 2026-07-06 09:19:42 +02:00
oauth Initial Blogfrei import 2026-06-12 23:34:46 +02:00
page Add configurable public footer snippets 2026-07-03 14:14:21 +02:00
pages Add Bilberry theme and complete Blogfrei rebrand 2026-06-13 17:28:46 +02:00
parse Initial Blogfrei import 2026-06-12 23:34:46 +02:00
prose Update DOMPurify security fix 2026-07-23 12:47:58 +02:00
scripts Remove phone-home and maintain ActivityPub subscribers 2026-06-13 10:05:52 +02:00
smoketest Show stats percentages before counts 2026-07-14 11:16:41 +02:00
spam Initial Blogfrei import 2026-06-12 23:34:46 +02:00
static Add default theme pagination arrows 2026-06-29 08:41:46 +02:00
templates Show stats percentages before counts 2026-07-14 11:16:41 +02:00
testdata Initial Blogfrei import 2026-06-12 23:34:46 +02:00
themes/default Let custom footer replace default branding 2026-07-03 14:59:41 +02:00
.dockerignore Initial Blogfrei import 2026-06-12 23:34:46 +02:00
.drone.yml Use Docker Hub token in Drone 2026-07-23 12:56:50 +02:00
.editorconfig Initial Blogfrei import 2026-06-12 23:34:46 +02:00
.gitignore Add Blogfrei editor image storage and container pipeline 2026-06-13 00:14:12 +02:00
.golangci.yml Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
account.go Group referrer stats by domain 2026-07-14 11:09:40 +02:00
account_import.go Add Hugo archive export and import 2026-06-14 18:42:57 +02:00
activitypub.go Add federation domain blocklist 2026-06-14 02:51:21 +02:00
activitypub_post_test.go Use post titles as federated summaries 2026-06-19 19:14:39 +02:00
activitypub_test.go Initial Blogfrei import 2026-06-12 23:34:46 +02:00
admin.go Add newsletter email send limits 2026-07-05 21:32:48 +02:00
admin_config_test.go Add newsletter email send limits 2026-07-05 21:32:48 +02:00
AGENTS.md Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
ap_subscriber_maintenance.go Remove phone-home and maintain ActivityPub subscribers 2026-06-13 10:05:52 +02:00
ap_subscriber_maintenance_test.go Remove phone-home and maintain ActivityPub subscribers 2026-06-13 10:05:52 +02:00
app.go Configure Docker deployments through environment variables 2026-07-23 13:20:53 +02:00
auth.go Initial Blogfrei import 2026-06-12 23:34:46 +02:00
AUTHORS.md Define Blogfrei licensing and first milestone 2026-06-12 23:43:47 +02:00
bootstrap_test.go Add newsletter email send limits 2026-07-05 21:32:48 +02:00
cache.go Initial Blogfrei import 2026-06-12 23:34:46 +02:00
collections.go Add database-backed newsletter email settings 2026-07-03 11:30:40 +02:00
CREDITS.md Strengthen mixed-license documentation 2026-06-14 19:16:26 +02:00
database-lib.go Initial Blogfrei import 2026-06-12 23:34:46 +02:00
database-no-sqlite.go Initial Blogfrei import 2026-06-12 23:34:46 +02:00
database-sqlite.go Initial Blogfrei import 2026-06-12 23:34:46 +02:00
database.go Rotate detailed view stats monthly 2026-07-06 09:19:42 +02:00
database_activitypub.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
database_test.go Initial Blogfrei import 2026-06-12 23:34:46 +02:00
docker-compose.prod.yml Configure Docker deployments through environment variables 2026-07-23 13:20:53 +02:00
docker-compose.yml Configure Docker deployments through environment variables 2026-07-23 13:20:53 +02:00
docker-setup.sh Initial Blogfrei import 2026-06-12 23:34:46 +02:00
Dockerfile Strengthen mixed-license documentation 2026-06-14 19:16:26 +02:00
Dockerfile.prod Update Drone registry for Forgejo 2026-06-19 16:23:05 +02:00
email.go Add newsletter email send limits 2026-07-05 21:32:48 +02:00
errors.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
export.go Add Hugo archive export and import 2026-06-14 18:42:57 +02:00
federation_blocklist.go Add federation domain blocklist 2026-06-14 02:51:21 +02:00
federation_blocklist_test.go Add federation domain blocklist 2026-06-14 02:51:21 +02:00
feed.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
go.mod Update DOMPurify security fix 2026-07-23 12:47:58 +02:00
go.sum Update DOMPurify security fix 2026-07-23 12:47:58 +02:00
gopher.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
handle.go Serve CSS with the correct MIME type 2026-06-13 17:49:47 +02:00
hostmeta.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
hugo_archive.go Add Hugo archive export and import 2026-06-14 18:42:57 +02:00
hugo_archive_test.go Add Hugo archive export and import 2026-06-14 18:42:57 +02:00
images.go Use blog avatar as favicon 2026-07-03 13:38:55 +02:00
images_test.go Use blog avatar as favicon 2026-07-03 13:38:55 +02:00
instance.go Initial Blogfrei import 2026-06-12 23:34:46 +02:00
invites.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
jobs.go Add newsletter email send limits 2026-07-05 21:32:48 +02:00
keys.go Bootstrap keys and database automatically 2026-06-13 09:26:37 +02:00
LEGAL.md Strengthen mixed-license documentation 2026-06-14 19:16:26 +02:00
LICENSE Strengthen mixed-license documentation 2026-06-14 19:16:26 +02:00
main_test.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
Makefile Add Hugo archive export and import 2026-06-14 18:42:57 +02:00
monetization.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
nodeinfo.go Add Bilberry theme and complete Blogfrei rebrand 2026-06-13 17:28:46 +02:00
oauth.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
oauth_generic.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
oauth_gitea.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
oauth_gitlab.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
oauth_signup.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
oauth_slack.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
oauth_test.go Add Blogfrei editor image storage and container pipeline 2026-06-13 00:14:12 +02:00
oauth_writeas.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
ossl_legacy.cnf Initial Blogfrei import 2026-06-12 23:34:46 +02:00
outbound.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
outbound_test.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
pad.go Remove phone-home and maintain ActivityPub subscribers 2026-06-13 10:05:52 +02:00
pad_test.go Remove phone-home and maintain ActivityPub subscribers 2026-06-13 10:05:52 +02:00
pages.go Add Bilberry theme and complete Blogfrei rebrand 2026-06-13 17:28:46 +02:00
post_list_read_count_test.go Restore default theme post pin controls 2026-07-03 13:12:32 +02:00
post_title_test.go Set fallback titles for published posts 2026-06-19 19:18:07 +02:00
postrender.go Render Blogfrei horizontal rules 2026-06-20 13:44:29 +02:00
postrender_test.go Fix newsletter subscription smoke coverage 2026-07-03 12:43:19 +02:00
posts.go Fix newsletter subscription smoke coverage 2026-07-03 12:43:19 +02:00
posts_test.go Initial Blogfrei import 2026-06-12 23:34:46 +02:00
profile.go Add federated profile settings 2026-06-13 23:26:06 +02:00
read.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
README.md Configure Docker deployments through environment variables 2026-07-23 13:20:53 +02:00
request.go Initial Blogfrei import 2026-06-12 23:34:46 +02:00
routes.go Allow owners to remove newsletter subscribers 2026-07-03 20:38:31 +02:00
routes_test.go Serve CSS with the correct MIME type 2026-06-13 17:49:47 +02:00
schema.sql Fix newsletter subscription smoke coverage 2026-07-03 12:43:19 +02:00
SECURITY.md Initial Blogfrei import 2026-06-12 23:34:46 +02:00
semver.go Initial Blogfrei import 2026-06-12 23:34:46 +02:00
session.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
sitemap.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
smoketest.py Add Blogfrei editor image storage and container pipeline 2026-06-13 00:14:12 +02:00
sqlite.sql Initial Blogfrei import 2026-06-12 23:34:46 +02:00
subscriber_stats.go Add ActivityPub follower instance summary 2026-06-15 09:18:51 +02:00
subscriber_stats_test.go Add ActivityPub follower instance summary 2026-06-15 09:18:51 +02:00
templates.go Add database-backed newsletter email settings 2026-07-03 11:30:40 +02:00
unregisteredusers.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00
users.go Add Blogfrei editor image storage and container pipeline 2026-06-13 00:14:12 +02:00
view_stats.go Group referrer stats by domain 2026-07-14 11:09:40 +02:00
view_stats_test.go Show stats percentages before counts 2026-07-14 11:16:41 +02:00
webfinger.go Eliminate inherited technical debt 2026-06-13 00:48:50 +02:00

Blogfrei

Blogfrei is a self-hosted publishing platform focused on independent writing, simple operation, and a modern editing experience.

The project starts as a fork of WriteFreely, but follows its own direction from this point onward.

First Implementation Milestone

The first Blogfrei implementation milestone replaces the inherited default editor with TOAST UI Editor, an open-source Markdown WYSIWYG editor licensed under the MIT License.

The first milestone also includes image handling for posts:

  • Writers can upload and insert images directly from the editor.
  • Blogfrei saves uploaded image files as images/<md5>.<extension>.
  • The editor inserts the URL of the stored image into the canonical Markdown post instead of embedding base64 image data.
  • Blogfrei serves the stored images directly.

The container exposes /go/images as a volume. Uploaded images are available from the application at /images/<md5>.<extension>.

Select the editor under [app] in config.ini:

[app]
editor = wysywig

wysywig and wysiwyg select the Blogfrei TOAST UI editor and are the default when the option is absent. pad and bare keep the inherited plain Markdown editors available.

Public Theme

Blogfrei bundles an MIT-licensed default public theme derived from the Bilberry Hugo Theme. All static assets are vendored in the repository, so public pages do not need to contact an external CDN.

Themes follow Hugo's directory conventions under themes/<name>/layouts. Select the public theme under [app]:

[app]
blog_theme = default

default is used when the option is absent. Existing bilberry values are migrated transparently for compatibility. The inherited theme configuration option and per-blog Custom CSS editor are no longer used; public appearance belongs to the selected Hugo-style theme.

Network Policy

Blogfrei does not perform vendor update checks, telemetry, or load remote embedded content by default. In particular, it does not contact WriteFreely, write.as, or GitHub for updates or telemetry.

Outbound traffic is limited to features explicitly configured or requested: ActivityPub federation, OAuth providers, email delivery, Web Monetization, and links opened by a user. When federation is enabled, Blogfrei verifies remote ActivityPub subscribers in the background immediately after startup and every eight hours. Valid actors have their inboxes and public keys refreshed; unreachable or invalid actors and their local follow records are removed.

The ActivityPub followers page summarizes followers by originating instance before the complete follower list. Instances with one follower are grouped as Others; repeated instances receive their own pie-chart segment, and the summary table shows at most ten entries.

Email Newsletters

Admins configure newsletter delivery from Admin → Settings → Email Delivery. The SMTP relay, newsletter sender address, and sender display title are stored in the application database; the SMTP password is encrypted and stored as text safe for both SQLite and MySQL.

Daily and monthly newsletter send limits can be configured from the same form. The limits count calendar days and calendar months; 0 or a blank field means unlimited. Newsletter emails above the current slot remain in the database FIFO queue and are sent when the next daily or monthly slot has capacity.

When email delivery is configured and a blog enables email subscriptions, the public subscription form is available at /subscribe for single-user blogs and /<blog>/subscribe for multi-user blogs. Newsletter emails contain the complete rendered post HTML, including Markdown images rewritten with absolute image URLs.

Audience Statistics

The Stats page retains the inherited per-post view and ActivityPub like counts and adds top-ten audience distributions with local CSS pie charts:

  • browser families derived from the visitor's user-agent header;
  • mobile or computer device classes derived from the user-agent header;
  • primary browser languages, with regional variants grouped together; and
  • referrer domains with links, grouping protocol and subdomains together.

Audience percentage tables display two decimal places.

Only visits already classified as human are recorded. Blogfrei stores neither visitor IP addresses nor complete referrer URLs: referrers are reduced to their registrable domain (for example, m.facebook.com becomes facebook.com) before storage. Detailed statistics begin accumulating after the database reaches migration V19. Browser, device, language, and referrer metadata is retained only for the current calendar month; older detailed view-stat rows are purged automatically as the stats table rotates.

The referrer distribution includes the blog domain and applies the same grouping to it as every other source.

Language values are intentionally synthetic: for example, de, de-DE, and de-AT are all recorded as de.

Container Compatibility

The Blogfrei container is initially a drop-in replacement for the WriteFreely container. An existing deployment should be able to replace its image with loweel/blogfrei:latest while keeping the same configuration file, database, keys, mounted paths, port, and startup command. The public container image is available on Docker Hub.

Until an explicit migration is provided, changes must preserve compatibility with existing WriteFreely installations and data.

The container is built with Go 1.26.4 and uses Debian 13 as its runtime base.

Goals

Develop new Blogfrei work under the EUPL

New original Blogfrei work is licensed under the European Union Public Licence v1.2 (EUPL-1.2). Code inherited from WriteFreely, and modifications derived from that code, remain covered by the GNU Affero General Public License v3.0 (AGPL-3.0).

Add a true WYSIWYG editor

Replace the current editing experience with TOAST UI Editor, providing a real JavaScript WYSIWYG editor whose canonical output remains Markdown.

The editor should provide direct visual formatting while preserving clean, portable content and a focused writing experience. Blogfrei will preserve the TOAST UI Editor attribution and MIT licence notice.

Manage uploaded images

Allow writers to insert and upload images directly from the WYSIWYG editor.

Blogfrei should save uploaded images in a dedicated, configurable folder and serve them directly as part of the published blog. Image files should remain easy to inspect, back up, restore, and migrate together with Markdown content.

Store posts as Markdown files

Store blog posts primarily as Markdown files on the filesystem instead of in the database.

Each post should have a stable path based on its author and identifier, for example:

data/<author>/<post-id>.md

The Markdown files are the source of truth and should be easy to inspect, version, copy, restore, and migrate. The database may still hold accounts, configuration, and rebuildable indexes, but not the canonical post content.

Export portable archives

Export creates a persistent Hugo-compatible tree under /go/export/<user>/content. Every post is a Markdown file with standard Hugo front matter plus Blogfrei restore metadata, including its user and blog. Referenced local images are copied to Hugo's static/images tree. Subsequent exports add missing posts and images and atomically refresh changed posts before creating the downloadable ZIP.

Import accepts this ZIP, safely extracts it under /go/import, and restores or updates posts in the database from the front matter. The declared user and blog must belong to the logged-in account.

An export should not require a database dump. Its contents should remain readable and usable without Blogfrei, making backup and migration simple.

Support Hugo themes

Allow blogs to load and use themes in the Hugo theme format.

This should make it possible to reuse the existing Hugo theme ecosystem while keeping theme installation and selection straightforward for Blogfrei administrators and writers.

Current Status

Blogfrei is at the beginning of its independent development. The current codebase provides the technical foundation inherited from WriteFreely; project identity, documentation, user experience, and deployment practices are being reworked for Blogfrei.

Development

Blogfrei is written primarily in Go, with JavaScript and LESS used for the web interface.

make
make test

Docker and Compose files are also available for local development and testing.

Installation with Docker Compose

Use the public Docker Hub image in either included Compose file:

docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d

The Compose files configure Blogfrei entirely with BLOGFREI_* environment variables, so a fresh clone starts without a config.ini or a manual configuration step. It is a local, single-user setup at http://localhost:8080, with federation disabled. Blogfrei creates its keys and database schema automatically on first start.

Before exposing an instance publicly, edit BLOGFREI_HOST to the public HTTPS URL and set strong BLOGFREI_DB_PASSWORD and MYSQL_ROOT_PASSWORD values in the selected Compose file. The same database values configure the application and MariaDB, so their credentials remain aligned.

Both Compose files list every supported container setting explicitly: BLOGFREI_BIND, BLOGFREI_PORT, BLOGFREI_HOST, BLOGFREI_SITE_NAME, BLOGFREI_SINGLE_USER, BLOGFREI_FEDERATION, BLOGFREI_BLOG_THEME, BLOGFREI_EDITOR, BLOGFREI_DB_TYPE, BLOGFREI_DB_HOST, BLOGFREI_DB_PORT, BLOGFREI_DB_NAME, BLOGFREI_DB_USER, and BLOGFREI_DB_PASSWORD. The default values are included only as YAML comments beside each explicit value, making the effective stack configuration visible.

No Forgejo credentials or access to the private source repository are required to install or run Blogfrei.

CI container publishing

The Drone pipeline publishes latest to the public Docker Hub repository loweel/blogfrei. Configure these Drone secrets to enable the publishing step:

  • DOCKER_USERNAME: Docker Hub username;
  • DOCKER_TOKEN: Docker Hub access token; and
  • DOCKER_REPO: Docker image repository, for example loweel/blogfrei (not an https:// URL).

At normal startup Blogfrei automatically creates any missing encryption keys under /go/keys and initializes an empty configured database. Existing keys and initialized databases are left unchanged. Persist /go/keys, /go/images, /go/export, /go/import, and the database storage as shown in the included Compose files; do not use docker compose down -v unless those volumes should be deleted.

For NFS-backed volumes, /go/keys, /go/images, /go/export, and /go/import must be writable by the container runtime user daemon (numeric UID/GID 1:1). Database storage requires an NFS server and mount options that provide reliable POSIX locking; do not share one live database volume between multiple database containers.

Definition of Done

Every Blogfrei change is complete only when:

  • The requested implementation is finished.
  • Relevant automated tests have been added or updated.
  • The complete test suite passes.
  • The race detector passes.
  • Go source files pass gofmt.
  • golangci-lint passes.
  • gosec passes.
  • The completed change is committed and pushed to the canonical repository.

Run the local quality checks with:

make dod

The full gate includes the smoke-test suite in smoketest/. It verifies the editor, valid and invalid image storage cases, zero gosec findings, and the built container contract. Run it separately with ./smoketest.py; use ./smoketest.py --build to include an additional clean Docker build.

The security gate requires zero gosec findings, zero reachable Go vulnerabilities reported by govulncheck, and zero known npm vulnerabilities. golangci-lint enforces govet, errcheck, staticcheck, and ineffassign.

License

Blogfrei uses a mixed-licence model:

  • New original Blogfrei work: EUPL-1.2.
  • Code inherited from WriteFreely and derivative modifications: AGPL-3.0.
  • Independent third-party components: their respective licences.

See LICENSE, LEGAL.md, CREDITS.md, and the licence texts under LICENSES/. Copyright for inherited and third-party material remains with its respective copyright holders.