This commit is contained in:
Stewart Pidasso 2025-07-07 02:07:58 +00:00
commit c021228161
24 changed files with 1509 additions and 0 deletions

26
.env.example Normal file
View file

@ -0,0 +1,26 @@
#This is designed to be used with network ACLs.
TRUSTED_IPS=123.123.123.123/32,123.123.123.123/32
#Jekyll Config file to Run in the bundler, values are Dev,Prod,Both
BUILD_TARGET=prod
#This contols the admin panel in jekyll-buidler. If you are happy doing dev and testing on your own machine or you dont want the markdown you can just remove the whole jekyll-builder container.
ADMIN_MODE=true
# Change the line below to your user name and password //
# echo $(htpasswd -nB [your-user-name]) | sed -e s/\\$/\\$\\$/g
JEKYLL_ADMIN_USER_PASS=stewart:$$2y$$05$$y1DN2vwS5cL48efnsUrnS.cuLy2ugNAdWWqIepiVSDKBmCBZqVk6e
#Certs
#Linode is just who I used. Hetzner is another great cloud provider.
LINODE_API_KEY=dfjkafjdklajfklsdajfkadj
EMAIL=for-the-certs@pm.me
DOMAIN=motherfuckingblog.com
# CrowdSec Configuration
# See crowdsec setup md doc.
CROWDSEC_BOUNCER_API_KEY=31974091270phid
#Analytics
UMAMI_PG_PWD=hfdanmvnu29phnmvda791bvak
UMAMI_HASH_SALT=kdsAkfNlSwoiSfdQWRnvdksVxse22edsa

15
.gitignore vendored Normal file
View file

@ -0,0 +1,15 @@
# Old files that are no longer needed
setup.sh
deploy-page
# Forgejo data
forgejo-data/
# Only need to sync Jekyll-source
sites/jekyll-source/prod-site/*
sites/jekyll-source/dev-site/*
sites/jekyll-source/jekyll-cache/*
# Environment variables
.env
.idea.env

115
CROWDSEC_SETUP.md Normal file
View file

@ -0,0 +1,115 @@
# CrowdSec Setup Instructions
This document provides instructions for setting up CrowdSec with Traefik in your environment.
## Initial Setup
1. First, start the services with a temporary API key:
```bash
# Set CROWDSEC_BOUNCER_API_KEY to a temporary value in .env
docker-compose up -d
```
2. Generate a bouncer API key:
```bash
docker exec -it crowdsec cscli bouncers add traefik-bouncer
```
3. Copy the generated API key and add it to your `.env` file:
```
CROWDSEC_BOUNCER_API_KEY=your_generated_key_here
```
4. Restart the services to apply the API key:
```bash
docker-compose down
docker-compose up -d
```
## Verify CrowdSec Installation
1. Check if CrowdSec is running properly:
```bash
docker exec -it crowdsec cscli metrics
```
2. List installed collections:
```bash
docker exec -it crowdsec cscli collections list
```
3. Test the CrowdSec setup:
```bash
# Check if CrowdSec is properly connected to Traefik
docker logs traefik | grep -i crowdsec
# Check if there are any decisions (blocks) in CrowdSec
docker exec -it crowdsec cscli decisions list
```
## Additional Security Configurations
### Install Additional Collections
You can install additional security collections for better protection:
```bash
docker exec -it crowdsec cscli collections install crowdsecurity/http-cve
docker exec -it crowdsec cscli collections install crowdsecurity/nginx
docker exec -it crowdsec cscli collections install crowdsecurity/wordpress
```
### Configure Custom Rules - Untested and from LLM
If you need custom security rules, you can create them in the CrowdSec configuration:
1. Create a custom rule file:
```bash
docker exec -it crowdsec touch /etc/crowdsec/parsers/s00-custom/custom-rules.yaml
```
2. Edit the file with your custom rules.
3. Restart CrowdSec:
```bash
docker restart crowdsec
```
## Troubleshooting
### Check Logs
If you encounter issues, check the logs:
```bash
# CrowdSec logs
docker logs crowdsec
# Traefik logs (includes bouncer plugin logs)
docker logs traefik
```
### Common Issues
1. **API Key Issues**: If the bouncer can't connect to CrowdSec, verify the API key is correct.
2. **No Decisions**: If CrowdSec isn't blocking anything, check if it's receiving logs:
```bash
docker exec -it crowdsec cscli metrics
```
3. **False Positives**: If legitimate traffic is being blocked, you can add exceptions:
```bash
docker exec -it crowdsec cscli decisions delete --ip 192.168.1.100
```

219
README.md Normal file
View file

@ -0,0 +1,219 @@
# Self-Hosted Blog Platform
A secure, privacy-focused blogging starter kit that keeps your data away from big tech. Deploy a complete blog platform with version control, analytics, and security in minutes.
## What This Is
A **self-hosted starter kit** for creating blogs that are safer and respect end-users without dependencies on big tech platforms. Entirely free and open source. Do What the Fuck You Want To Public License (Standard WTFPLv2), plus the licenses of the underlying software.
**For 99% of users**: A fast, secure blog with no ads, no tracking, and complete data ownership.
**For developers**: A Jekyll-based static site generator with modern DevOps practices baked in.
**For privacy advocates**: 1st party analytics, no cookies, and complete control over your data.
## Features
- **🚀 Jekyll**: Static site generator with automated builds. Less-style Theme fork included.
- **🔒 Traefik**: Reverse proxy for segmentation with automatic SSL, security headers, and IP allowlisting
- **📊 Umami**: Privacy-focused analytics (no cookies, 1st party data)
- **🛡️ CrowdSec**: Crowdsourced WAF with real-time threat intelligence
- **📝 Jekyll Admin**: WYSIWYG markdown editor so your boss's nephew can ~~justify his existence~~ write blog posts.
- **🔧 Forgejo**: Self-hosted Git platform for version control
- **⚡ Nginx**: High-performance static file serving
- **🔐 Security**: IP allowlisting, TLS certificates, and redundant access controls
## Who This Is For
This platform satisfies the needs of four key audiences:
### 🧑‍💻 Developers
- Trusted Jekyll ecosystem with static deployment
- Runs efficiently on small cloud instances
- Version control through Forgejo (optional but included)
- Docker-based deployment for consistency
### ✍️ Content Creators
- WYSIWYG Markdown editor with live preview
- SEO optimizations built-in
- Automated site building on content changes
- Separate dev/prod environments
### 📈 Analytics & Data Teams
- 1st party analytics without cookies
- Complete data ownership
- Privacy-compliant user tracking
- No dependency on Google Analytics
### 🔒 Security Teams
- Crowdsourced WAF protection
- IP allowlisting for admin interfaces
- Automatic TLS certificate management
- Redundant access controls
## Quick Start
1. **Clone and configure**:
```bash
git clone <this-repo>
cd self-hosted-blog
cp .env.example .env
vim .env # Configure your domain and settings
```
2. **Deploy**:
```bash
docker compose up -d
```
3. **Access your services**:
- **Blog**: `https://yourdomain.com` (public)
- **Admin**: `https://admin.yourdomain.com:333` (IP restricted)
- **Analytics**: `https://analytics.yourdomain.com:333` (IP restricted)
- **Git**: `https://git.yourdomain.com:333` (IP restricted)
## System Requirements
- **Minimum**: 1 shared CPU, 2GB RAM
Tested successfully on Linode shared instances.
## Configuration
### Environment Variables
Key variables to configure in your `.env` file:
```bash
# Domain Configuration for Routes and LetsEncrypt
DOMAIN=yourdomain.com
EMAIL=admin@yourdomain.com
# Security
TRUSTED_IPS=192.168.1.0/24,10.0.0.0/8 # Your allowed IP ranges
JEKYLL_ADMIN_USER_PASS=admin:$2y$10$... # Generated with htpasswd
# SSL Configuration (Linode DNS example)
LINODE_API_KEY=your_linode_api_key
# Analytics
UMAMI_PG_PWD=secure_password_here
UMAMI_HASH_SALT=random_salt_here
# CrowdSec
CROWDSEC_BOUNCER_API_KEY=your_crowdsec_key
```
### DNS & Firewall Setup
1. **DNS Records**: Point your domain and subdomains to your server IP
2. **Firewall**:
- Open ports 80, 443 (public)
- Restrict port 333 to trusted IPs only
3. **SSL**: Configured automatically via Let's Encrypt
## Content Management
### Creating Content
**Option 1: Web Interface** (Recommended)
- Access Jekyll Admin at `https://admin.yourdomain.com:333/admin`
- Create/edit posts with live preview
- Automatic site rebuilding
**Option 2: Direct File Editing**
- Edit files in `sites/jekyll-source/_posts/`
- Follow naming: `YYYY-MM-DD-title.md`
- Add front matter for metadata
### Jekyll Build Environments
Control build targets with the `BUILD_TARGET` environment variable:
- `BUILD_TARGET=prod` - Production site only
- `BUILD_TARGET=dev` - Development site only
- `BUILD_TARGET=both` - Both environments
## Security Features
### Multi-Layer Protection
1. **Network Level**: Cloud provider firewall + IP allowlisting
2. **Application Level**: Traefik IP allowlisting + basic auth
3. **WAF Level**: CrowdSec real-time threat detection
4. **Transport Level**: Automatic TLS with security headers
### Admin Interface Security
- **IP Allowlisting**: Only trusted IPs can access admin interfaces
- **Custom Port**: Non-standard port 333 for admin access
- **Basic Auth**: HTTP authentication for Jekyll Admin
- **TLS**: All traffic encrypted with Let's Encrypt certificates
### Best Practices
- Use a VPN for remote administration
- Regularly update the trusted IP list
- Monitor CrowdSec logs for blocked attempts
- Keep Docker images updated
## Customization
### Changing Jekyll Theme
1. Edit `sites/jekyll-source/Gemfile`
2. Update `_config.yml` theme setting
3. Run `bundle install` in the Jekyll container
### Adding Custom CSS/JS
- CSS: Edit `assets/css/style.scss`
- JavaScript: Edit `assets/js/custom.js`
### Modifying Services
The Docker Compose stack is modular. Remove or modify services as needed:
- Don't need Git? Remove the `forgejo` service
- Want different analytics? Replace `umami` with your preferred solution
- Need additional security? Add more CrowdSec collections
## Troubleshooting
### Common Issues
**Can't access admin interfaces**:
- Check your IP is in `TRUSTED_IPS`
- Verify firewall rules on your cloud provider
- Ensure port 333 is accessible from your location
**SSL certificate errors**:
- Verify DNS records are pointing to your server
- Check API credentials for your DNS provider
- Review Traefik logs: `docker logs mf-traefik`
**Site not building**:
- Check Jekyll container logs: `docker logs jekyll-builder`
- Verify file permissions in `sites/jekyll-source/`
- Ensure `BUILD_TARGET` is set correctly
- Ensure your nginx volume is mounted at the correct site directory
### Getting Help
1. Check container logs: `docker logs <container-name>`
2. Verify service status: `docker ps`
3. Review configuration files for syntax errors
4. Check DNS resolution: `nslookup yourdomain.com`
## Contributing
This is a starter kit meant to be forked and customized. Take what you need, modify or remove everything else.
**Known Issues**:
- Jekyll Admin interface may show harmless error messages
- Some themes may require additional configuration
- Mobile responsiveness varies by theme choice
## License
Open source - take it, modify it, make it yours.

326
docker-compose.yml Normal file
View file

@ -0,0 +1,326 @@
services:
traefik:
image: traefik:v3
container_name: mf-traefik
restart: unless-stopped
env_file: .env
ports:
- "80:80"
- "443:443"
- "333:333"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./docker/traefik/:/letsencrypt
- traefik-config:/etc/traefik
- traefik-logs:/var/log/traefik
command:
- --api.dashboard=false
- --api.insecure=false
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.custom.address=:333
# Enable Traefik logs for CrowdSec
- --accesslog=true
- --accesslog.filepath=/var/log/traefik/access.log
# CrowdSec Bouncer Plugin configuration
- --experimental.plugins.crowdsec-bouncer-traefik-plugin.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
- --experimental.plugins.crowdsec-bouncer-traefik-plugin.version=v1.4.2
# HTTP to HTTPS redirect
- --entrypoints.web.http.redirections.entryPoint.to=websecure
- --entrypoints.web.http.redirections.entryPoint.scheme=https
# Let's Encrypt configuration
- --certificatesresolvers.linodedns.acme.email=${EMAIL}
- --certificatesresolvers.linodedns.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.linodedns.acme.dnschallenge=true
- --certificatesresolvers.linodedns.acme.dnschallenge.provider=linode
environment:
- LINODE_TOKEN=${LINODE_API_KEY}
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`traefik.${DOMAIN}`)"
- "traefik.http.routers.traefik.entrypoints=custom"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.services.traefik.loadbalancer.server.port=8080"
- "traefik.http.routers.traefik.tls=true"
- "traefik.http.routers.traefik.tls.certresolver=linodedns"
# CrowdSec Bouncer Plugin middleware configuration
- "traefik.http.middlewares.crowdsec-bouncer.plugin.crowdsec-bouncer-traefik-plugin.crowdsecLapiKey=${CROWDSEC_BOUNCER_API_KEY}"
- "traefik.http.middlewares.crowdsec-bouncer.plugin.crowdsec-bouncer-traefik-plugin.crowdsecLapiHost=crowdsec:8080"
- "traefik.http.middlewares.crowdsec-bouncer.plugin.crowdsec-bouncer-traefik-plugin.crowdsecLapiScheme=http"
- "traefik.http.middlewares.crowdsec-bouncer.plugin.crowdsec-bouncer-traefik-plugin.logLevel=INFO"
# Security Headers Middleware
- "traefik.http.middlewares.sec-headers.headers.customResponseHeaders.Strict-Transport-Security=max-age=31536000; includeSubDomains; preload"
- "traefik.http.middlewares.sec-headers.headers.customResponseHeaders.X-Content-Type-Options=nosniff"
- "traefik.http.middlewares.sec-headers.headers.customResponseHeaders.X-Frame-Options=DENY"
- "traefik.http.middlewares.sec-headers.headers.customResponseHeaders.X-XSS-Protection=1; mode=block"
- "traefik.http.middlewares.sec-headers.headers.customResponseHeaders.Referrer-Policy=strict-origin-when-cross-origin"
forgejo:
image: codeberg.org/forgejo/forgejo:11
container_name: forgejo
restart: unless-stopped
env_file: .env
environment:
- USER_UID=1000
- USER_GID=1000
- FORGEJO__server__DOMAIN=git.${DOMAIN}
- FORGEJO__server__ROOT_URL=https://git.${DOMAIN}:333
- FORGEJO__server__HTTP_PORT=3000
- FORGEJO__server__STATIC_ROOT_PATH=/data/gitea/public
- FORGEJO__service__ENABLE_BASIC_AUTHENTICATION=true
- FORGEJO__ui__DEFAULT_THEME=gitea
- FORGEJO__repository__ENABLE_PUSH_CREATE_USER=true
- FORGEJO__repository__ENABLE_PUSH_CREATE_ORG=true
- FORGEJO__repository__PREFERRED_LICENSES=MIT,Apache-2.0,GPL-3.0
- FORGEJO__service__DISABLE_REGISTRATION=false
- FORGEJO__service__REQUIRE_SIGNIN_VIEW=false
- FORGEJO__picture__DISABLE_GRAVATAR=false
- FORGEJO__repository__ENABLE_PUSH_CREATE=true
- FORGEJO__repository__ACCESS_CONTROL_ALLOW_ORIGIN=*
volumes:
- forgejo-data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.forgejo.rule=Host(`git.${DOMAIN}`)"
- "traefik.http.routers.forgejo.tls=true"
- "traefik.http.routers.forgejo.tls.certresolver=linodedns"
- "traefik.http.routers.forgejo.entrypoints=custom"
- "traefik.http.services.forgejo.loadbalancer.server.port=3000"
- "traefik.http.middlewares.forgejo-allowlist.ipallowlist.sourcerange=${TRUSTED_IPS}"
- "traefik.http.routers.forgejo.middlewares=forgejo-allowlist,sec-headers,crowdsec-bouncer"
jekyll:
image: ruby:3.1
container_name: jekyll-builder
restart: unless-stopped
volumes:
- ./sites/jekyll-source:/srv/jekyll
- ./sites/jekyll-source/prod-site:/srv/jekyll/prod-site
- ./sites/jekyll-source/dev-site:/srv/jekyll/dev-site
environment:
- JEKYLL_ENV=${JEKYLL_ENV:-development}
- BUILD_TARGET=${BUILD_TARGET:-prod}
- ADMIN_MODE=${ADMIN_MODE:-false}
command: >
/bin/sh -c '
apt-get update && apt-get install -y build-essential libffi-dev zlib1g-dev &&
cd /srv/jekyll &&
gem install bundler jekyll jekyll-admin &&
bundle install &&
set -e # Exit on any error
echo "Build target: $BUILD_TARGET"
echo "Admin mode: ${ADMIN_MODE:-false}"
if [ "$BUILD_TARGET" = "prod" ]; then
echo "Building production site..."
if [ "$ADMIN_MODE" = "true" ]; then
echo "Starting Jekyll server in admin mode (prod)..."
bundle exec jekyll serve \
--host 0.0.0.0 \
--port 4000 \
--config _config.yml,_config_prod.yml,_config_admin.yml \
--force_polling
--destination ./prod-site
else
bundle exec jekyll build \
--config _config.yml,_config_prod.yml \
--destination /srv/jekyll/prod-site \
--force_polling
fi
elif [ "$BUILD_TARGET" = "dev" ]; then
if [ "$ADMIN_MODE" = "true" ]; then
echo "Starting Jekyll server in admin mode (dev)..."
bundle exec jekyll serve \
--host 0.0.0.0 \
--port 4000 \
--config _config.yml,_config_dev.yml,_config_admin.yml \
--force_polling
--destination ./dev-site
else
echo "Building development site..."
bundle exec jekyll build \
--watch \
--force_polling \
--config _config.yml,_config_dev.yml \
--destination /srv/jekyll/dev-site
fi
elif [ "$BUILD_TARGET" = "both" ]; then
echo "Building both dev and prod sites..."
# PROD (non-watching)
bundle exec jekyll build \
--config _config.yml,_config_prod.yml \
--destination /srv/jekyll/prod-site &
# DEV (non-serving, non-watching)
bundle exec jekyll build \
--config _config.yml,_config_dev.yml \
--destination /srv/jekyll/dev-site &
wait
else
echo "❌ Unknown or missing BUILD_TARGET: $BUILD_TARGET"
echo "Please set BUILD_TARGET to dev, prod, or both"
exit 1
fi
'
labels:
- "traefik.enable=true"
- "traefik.http.routers.jekyll-admin.rule=Host(`admin.${DOMAIN}`)"
- "traefik.http.routers.jekyll-admin.tls=true"
- "traefik.http.routers.jekyll-admin.tls.certresolver=linodedns"
- "traefik.http.routers.jekyll-admin.entrypoints=custom"
- "traefik.http.services.jekyll-admin.loadbalancer.server.port=4000"
- "traefik.http.middlewares.jekyll-allowlist.ipallowlist.sourcerange=${TRUSTED_IPS}"
- "traefik.http.middlewares.jekyll-admin-auth.basicauth.users=${JEKYLL_ADMIN_USER_PASS}"
- "traefik.http.routers.jekyll-admin.middlewares=jekyll-allowlist,jekyll-admin-auth,crowdsec-bouncer"
nginx:
image: nginx:alpine
container_name: site-server
restart: unless-stopped
volumes:
- ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./sites/jekyll-source/prod-site:/usr/share/nginx/html/prod:ro
- ./sites/jekyll-source/dev-site:/usr/share/nginx/html/dev:ro
labels:
- "traefik.enable=true"
# Main site configuration
- "traefik.http.routers.site.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.site.entrypoints=web,websecure"
- "traefik.http.services.site.loadbalancer.server.port=80"
- "traefik.http.routers.site.middlewares=sec-headers,crowdsec-bouncer,umami-cors,custom-server-header"
- "traefik.http.routers.site.tls=true"
- "traefik.http.routers.site.tls.certresolver=linodedns"
# Main site CORS
- "traefik.http.middlewares.prod-site-header.headers.accesscontrolallowmethods=GET,OPTIONS,PUT"
- "traefik.http.middlewares.prod-site-header.headers.accesscontrolallowheaders=*"
- "traefik.http.middlewares.prod-site-header.headers.accesscontrolalloworiginlist=https://fuckbigbro.${DOMAIN},https://${DOMAIN}"
- "traefik.http.middlewares.prod-site-header.headers.accesscontrolmaxage=100"
- "traefik.http.middlewares.prod-site-header.headers.addvaryheader=true"
# Wildcard certificate definition
- "traefik.http.routers.site.tls.domains[0].main=${DOMAIN}"
- "traefik.http.routers.site.tls.domains[0].sans=*.${DOMAIN}"
# Don't advertise your server stack unless you intend to:
- "traefik.http.middlewares.custom-server-header.headers.customresponseheaders.Server=FuckOff"
# Dev site configuration
- "traefik.http.routers.site-dev.rule=Host(`dev.${DOMAIN}`)"
- "traefik.http.routers.site-dev.entrypoints=custom"
- "traefik.http.middlewares.site-allowlist.ipallowlist.sourcerange=${TRUSTED_IPS}"
- "traefik.http.routers.site-dev.middlewares=site-allowlist,sec-headers,crowdsec-bouncer"
crowdsec:
image: crowdsecurity/crowdsec:latest
container_name: crowdsec
restart: unless-stopped
environment:
- GID=1000
- UID=1000
- COLLECTIONS=crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/nginx
volumes:
- crowdsec-data:/var/lib/crowdsec/data
- crowdsec-config:/etc/crowdsec
- ./docker/crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml:ro
- traefik-logs:/var/log/traefik:ro
depends_on:
- traefik
#########################################
### BEGIN UMAMI ANALYTICS SECTION ###
#########################################
umami_db:
image: postgres:16-alpine
restart: always
volumes:
- umami-db-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=umami
- POSTGRES_USER=umami
- POSTGRES_PASSWORD=${UMAMI_PG_PWD} # Set this in your .env
healthcheck:
test: ["CMD-SHELL", "pg_isready -U umami"]
interval: 10s
timeout: 5s
retries: 5
start_period: 1m
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
restart: always
depends_on:
umami_db:
condition: service_healthy
environment:
- DATABASE_URL=postgresql://umami:${UMAMI_PG_PWD}@umami_db:5432/umami
- DATABASE_TYPE=postgresql
- HASH_SALT=${UMAMI_HASH_SALT}
- TRACKER_SCRIPT_NAME=script.js
labels:
- "traefik.enable=true"
# Main Umami interface (admin dashboard)
- "traefik.http.routers.umami.rule=Host(`fuckbigbro.${DOMAIN}`)"
- "traefik.http.routers.umami.tls=true"
- "traefik.http.routers.umami.tls.certresolver=linodedns"
- "traefik.http.services.umami.loadbalancer.server.port=3000"
- "traefik.http.routers.umami.entrypoints=custom"
- "traefik.http.middlewares.umami-allowlist.ipallowlist.sourcerange=${TRUSTED_IPS}"
- "traefik.http.routers.umami.middlewares=umami-allowlist,sec-headers,crowdsec-bouncer"
# Public script endpoint - accessible via websecure (443) or web (80)
- "traefik.http.routers.umami-script.rule=Host(`fuckbigbro.${DOMAIN}`) && Path(`/script.js`) && Method(`GET`)"
- "traefik.http.routers.umami-script.entrypoints=websecure"
- "traefik.http.routers.umami-script.service=umami"
- "traefik.http.routers.umami-script.middlewares=crowdsec-bouncer"
- "traefik.http.routers.umami-script.tls=true"
- "traefik.http.routers.umami-script.tls.certresolver=linodedns"
# Public API endpoint for collecting data
- "traefik.http.routers.umami-api.rule=Host(`fuckbigbro.${DOMAIN}`) && PathPrefix(`/api/send`)"
- "traefik.http.routers.umami-api.entrypoints=websecure"
- "traefik.http.routers.umami-api.service=umami"
- "traefik.http.routers.umami-api.middlewares=crowdsec-bouncer,sec-headers,umami-cors"
- "traefik.http.routers.umami-api.tls=true"
- "traefik.http.routers.umami-api.tls.certresolver=linodedns"
# CORS middleware for API endpoints
- "traefik.http.middlewares.umami-cors.headers.accesscontrolallowmethods=GET,POST,OPTIONS"
- "traefik.http.middlewares.umami-cors.headers.accesscontrolalloworiginlist=https://fuckbigbro.${DOMAIN},https://${DOMAIN},https://www.${DOMAIN}"
- "traefik.http.middlewares.umami-cors.headers.accesscontrolallowheaders=Content-Type,Authorization,X-Requested-With,X-Umami-Cache"
- "traefik.http.middlewares.umami-cors.headers.accesscontrolmaxage=86400"
- "traefik.http.middlewares.umami-cors.headers.addvaryheader=true"
# CORS Preflight OPTIONS handler
- "traefik.http.routers.umami-options.rule=Host(`fuckbigbro.${DOMAIN}`) && PathPrefix(`/api/send`) && Method(`OPTIONS`)"
- "traefik.http.routers.umami-options.entrypoints=websecure"
- "traefik.http.routers.umami-options.middlewares=umami-cors"
- "traefik.http.routers.umami-options.tls=true"
- "traefik.http.routers.umami-options.tls.certresolver=linodedns"
- "traefik.http.routers.umami-options.service=noop@internal"
volumes:
umami-db-data:
driver: local
forgejo-data:
driver: local
traefik-config:
driver: local
crowdsec-data:
driver: local
crowdsec-config:
driver: local
traefik-logs:
driver: local
networks:
default:
name: traefik

View file

@ -0,0 +1,4 @@
filenames:
- /var/log/traefik/access.log
labels:
type: traefik

33
docker/nginx/nginx.conf Normal file
View file

@ -0,0 +1,33 @@
server {
listen 80;
server_name motherfuckingblog.com;
root /usr/share/nginx/html/prod;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# Handle Hugo's(Not necessary with Jekyll) pretty URLs
#location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
# expires 1y;
# add_header Cache-Control "public, immutable";
#}
}
server {
listen 80;
server_name dev.motherfuckingblog.com;
root /usr/share/nginx/html/dev;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# Handle pretty URLs
#location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
# expires 1y;
# add_header Cache-Control "public, immutable";
#}
}

28
docker/traefik/acme.json Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,30 @@
source "https://rubygems.org"
# Jekyll and plugins
gem "jekyll", "~> 4.3.2"
# Plugins
group :jekyll_plugins do
gem "jekyll-paginate"
gem "jekyll-sitemap"
gem "jekyll-feed"
gem "jekyll-seo-tag"
gem "jekyll-admin"
end
# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
# and associated library.
platforms :mingw, :x64_mingw, :mswin, :jruby do
gem "tzinfo", ">= 1", "< 3"
gem "tzinfo-data"
end
# Performance-booster for watching directories on Windows
gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin]
# Lock gem to on JRuby builds since newer versions of the gem
# do not have a Java counterpart.
gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby]
# When running Jekyll locally
gem "webrick", "~> 1.8"

View file

@ -0,0 +1,126 @@
GEM
remote: https://rubygems.org/
specs:
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
base64 (0.3.0)
colorator (1.1.0)
concurrent-ruby (1.3.5)
em-websocket (0.5.3)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0)
eventmachine (1.2.7)
ffi (1.17.2-aarch64-linux-gnu)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
forwardable-extended (2.6.0)
http_parser.rb (0.8.0)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
jekyll (4.3.4)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
i18n (~> 1.0)
jekyll-sass-converter (>= 2.0, < 4.0)
jekyll-watch (~> 2.0)
kramdown (~> 2.3, >= 2.3.1)
kramdown-parser-gfm (~> 1.0)
liquid (~> 4.0)
mercenary (>= 0.3.6, < 0.5)
pathutil (~> 0.9)
rouge (>= 3.0, < 5.0)
safe_yaml (~> 1.0)
terminal-table (>= 1.8, < 4.0)
webrick (~> 1.7)
jekyll-admin (0.12.0)
jekyll (>= 3.7, < 5.0)
rackup (~> 2.0)
sinatra (~> 4.0)
sinatra-contrib (~> 4.0)
jekyll-feed (0.17.0)
jekyll (>= 3.7, < 5.0)
jekyll-paginate (1.1.0)
jekyll-sass-converter (2.2.0)
sassc (> 2.0.1, < 3.0)
jekyll-seo-tag (2.8.0)
jekyll (>= 3.8, < 5.0)
jekyll-sitemap (1.4.0)
jekyll (>= 3.7, < 5.0)
jekyll-watch (2.2.1)
listen (~> 3.0)
kramdown (2.5.1)
rexml (>= 3.3.9)
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
liquid (4.0.4)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
logger (1.7.0)
mercenary (0.4.0)
multi_json (1.15.0)
mustermann (3.0.3)
ruby2_keywords (~> 0.0.1)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (5.1.1)
rack (3.1.16)
rack-protection (4.1.1)
base64 (>= 0.1.0)
logger (>= 1.6.0)
rack (>= 3.0.0, < 4)
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rackup (2.2.1)
rack (>= 3)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
rexml (3.4.1)
rouge (3.30.0)
ruby2_keywords (0.0.5)
safe_yaml (1.0.5)
sassc (2.4.0)
ffi (~> 1.9)
sinatra (4.1.1)
logger (>= 1.6.0)
mustermann (~> 3.0)
rack (>= 3.0.0, < 4)
rack-protection (= 4.1.1)
rack-session (>= 2.0.0, < 3)
tilt (~> 2.0)
sinatra-contrib (4.1.1)
multi_json (>= 0.0.2)
mustermann (~> 3.0)
rack-protection (= 4.1.1)
sinatra (= 4.1.1)
tilt (~> 2.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
tilt (2.6.0)
unicode-display_width (2.6.0)
webrick (1.9.1)
PLATFORMS
aarch64-linux
universal-darwin-24
x86_64-linux
DEPENDENCIES
http_parser.rb (~> 0.6.0)
jekyll (~> 4.3.2)
jekyll-admin
jekyll-feed
jekyll-paginate
jekyll-seo-tag
jekyll-sitemap
tzinfo (>= 1, < 3)
tzinfo-data
wdm (~> 0.1.1)
webrick (~> 1.8)
BUNDLED WITH
2.3.25

View file

@ -0,0 +1,76 @@
# Jekyll configuration with less-style-please theme
title: Motherfucking Blog
tagline: Sick of All the Shit
description: >-
Turning a joke into a self-hosted starter kit for motherfucking blogs.
url: ''
baseurl: ''
lang: en-US
timezone: UTC
# Author information
author:
name: Stewart Pidasso
email: stu[at][this-site].com
links:
- https://github.com/Steward-Pidasso
- https://twitter.com/username
# GitHub repository
repository: username/repo-name
# Jekyll settings
markdown: kramdown
highlighter: rouge
permalink: /:year/:month/:day/:title/
paginate: 10
# Plugins
plugins:
- jekyll-paginate
- jekyll-sitemap
- jekyll-feed
- jekyll-seo-tag
# Exclude files/folders
exclude:
- Gemfile
- Gemfile.lock
- node_modules
- vendor
- _site
- .sass-cache
- .jekyll-cache
- gemfiles
- vendor/bundle/
- vendor/cache/
- vendor/gems/
- vendor/ruby/
jekyll_admin:
#You will likely need to change this depending on your template.
metadata:
_posts:
- name: "layout"
field:
element: "hidden" # Change from "text" to "hidden"
value: "post"
- name: "title"
field:
element: "text"
label: "Post title"
- name: "date" # Add this field
field:
element: "text"
label: "Date"
value: "CURRENT_DATETIME" # This will use the current date/time
- name: "categories"
field:
element: "text"
label: "Categories (comma-separated)"
placeholder: "category1, category2"
- name: "tags"
field:
element: "text"
label: "Tags (comma-separated)"
placeholder: "tag1, tag2"

View file

@ -0,0 +1,3 @@
# Jekyll Admin specific configuration
url: 'http://admin.localhost:333'
baseurl: ''

View file

@ -0,0 +1,9 @@
# Development environment configuration
url: 'http://dev.localhost'
baseurl: ''
environment: development
destination: dev-site
# Additional development-specific settings
safe: false
future: true
unpublished: true

View file

@ -0,0 +1,14 @@
# Production environment configuration
url: 'http://localhost'
baseurl: 'https://motherfuckingblog.com'
environment: production
# Additional production-specific settings
safe: true
future: false
unpublished: false
destination: prod-site
# Analytics and SEO settings for production
google_analytics: # Don't Add your Google Analytics ID here. They are a bad company.

View file

@ -0,0 +1 @@
<script defer src="https://fuckbigbro.motherfuckingblog.com/script.js" data-website-id="7a8d87bd-5232-42fb-8aad-5f05283b7115"></script>

View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="{{ site.lang | default: "en-US" }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% if page.title %}{{ page.title | escape }} - {{ site.title | escape }}{% else %}{{ site.title | escape }}{% endif %}</title>
<meta name="description" content="{{ page.description | default: site.description | strip_html | normalize_whitespace | truncate: 160 | escape }}">
<link rel="stylesheet" href="{{ "/assets/css/style.css" | relative_url }}">
{% feed_meta %}
{% seo %}
{% include analytics/umami.html %}
</head>
<body>
<header>
<h1><a href="{{ "/" | relative_url }}">{{ site.title | escape }}</a></h1>
<p>{{ site.tagline | escape }}</p>
</header>
<main>
{{ content }}
</main>
<footer>
<p>&copy; {{ site.time | date: '%Y' }} {{ site.author.name | default: site.title }}</p>
</footer>
</body>
</html>

View file

@ -0,0 +1,24 @@
---
layout: default
---
<div class="home">
{{ content }}
<h2>Recent Posts</h2>
<ul class="post-list">
{% for post in site.posts limit:10 %}
<li>
<h3>
<a class="post-link" href="{{ post.url | relative_url }}">{{ post.title | escape }}</a>
</h3>
<p class="post-meta">{{ post.date | date: "%b %-d, %Y" }}</p>
{% if post.description %}
<p>{{ post.description }}</p>
{% endif %}
</li>
{% endfor %}
</ul>
<p><a href="{{ "/archive" | relative_url }}">View all posts</a></p>
</div>

View file

@ -0,0 +1,46 @@
---
layout: default
---
<article class="post">
<header class="post-header">
<h1 class="post-title">{{ page.title | escape }}</h1>
<p class="post-meta">
<time datetime="{{ page.date | date_to_xmlschema }}">{{ page.date | date: "%b %-d, %Y" }}</time>
{% if page.author %}
<span>{{ page.author }}</span>
{% endif %}
</p>
</header>
<div class="post-content">
{{ content }}
</div>
{% if page.tags.size > 0 %}
<div class="post-tags">
<p>Tags:
{% for tag in page.tags %}
<a href="{{ site.baseurl }}/tags/#{{ tag | slugify }}">{{ tag }}</a>{% unless forloop.last %}, {% endunless %}
{% endfor %}
</p>
</div>
{% endif %}
</article>
<br>
<h2>Recent Posts</h2>
<ul class="post-list">
{% for post in site.posts limit:10 %}
<li>
<h3>
<a class="post-link" href="{{ post.url | relative_url }}">{{ post.title | escape }}</a>
</h3>
<p class="post-meta">{{ post.date | date: "%b %-d, %Y" }}</p>
{% if post.description %}
<p>{{ post.description }}</p>
{% endif %}
</li>
{% endfor %}
</ul>
<p><a href="{{ "/archive" | relative_url }}">View all posts</a></p>

View file

@ -0,0 +1,88 @@
---
title: Boilerplate Jekyll How-to
layout: post
date: '2025-05-01 12:00:00 +0000'
categories:
- jekyll
tags:
- getting-started
---
# Welcome to Jekyll!
This is your first post using Jekyll with the less-style theme. Jekyll is a static site generator that transforms your plain text into static websites and blogs.
## How to Create Posts
To create a new post, simply add a file in the `_posts` directory that follows the naming convention `YYYY-MM-DD-title.md`, where `YYYY-MM-DD` is the date of your post and `title` is the title of your post.
At the top of each post, you need to include what's called "front matter" - this is YAML that tells Jekyll how to process the file. Here's an example:
```yaml
---
layout: post
title: "Your Post Title"
date: 2023-05-01 12:00:00 -0000
categories: [category1, category2]
tags: [tag1, tag2]
---
```
## Markdown Formatting
Jekyll uses Markdown for formatting. Here are some examples:
### Headers
```markdown
# H1
## H2
### H3
```
### Emphasis
```markdown
*italic* or _italic_
**bold** or __bold__
```
### Lists
```markdown
- Item 1
- Item 2
- Subitem 2.1
- Subitem 2.2
1. First item
2. Second item
```
### Links and Images
```markdown
[Link text](URL)
![Image alt text](image-url)
```
### Code
```markdown
`inline code`
```python
# Code block with syntax highlighting
def hello_world():
print("Hello, world!")
```
```
## Next Steps
1. Customize your site by editing `_config.yml`
2. Create new posts in the `_posts` directory
3. Add pages by creating new markdown files in the root directory
4. Customize the theme by overriding CSS in `assets/css/style.scss`
Happy blogging with Jekyll!

View file

@ -0,0 +1,83 @@
---
title: WTF Is this? Gimme the deets.
layout: post
date: '2025-07-06 15:11:26'
---
[TL;DR Github Repo](http://git.motherfuckingblog.com)
For **99% of users**:
- a <20kb blog about web infrastructure that they don't understand
- they found high due to its 99% SEO Scores
- seems different, but they don't quite know why?
- "yeah its whatever. no ads was nice." - Average Chad
For **OG [MotherfuckingWebsite.com](https://motherfuckingwebsite.com)** enjoyers:
- A way to learn about static site generators like **Jekyll** (Check out Hugo or 11ty too if this concept is new to you)
-- A way to systemize and organize if you plan on having more than a handful of posts.
-- Extensible, but not in the gross Wordpress way. For example, add search, table of contents, and more.
- Some cool other shit in docker compose that makes it easier to own your data and your infrastructure.
For **fucks sake whats all this other shit**:
- Isn't it weird products don't ship with **security** natively included? It's opinionated, but its safer than default in the othr shit.
- *Traefik* - Reverse proxy your management interfaces and IP Allowlist them in an easy way. Secure yourself against any dumb idea you are bound to add later.
- *Crowdsec WAF* - Crowdsec is legit innovating and deserves more attention. Crowdsourced blacklisting and malicious signature detection.
- Data Privacy:
- *Forgejo* - FOSS Github. Better Post Revision History than Wordpress. But
- *Umami* - FOSS Google Analytics Alternative. Privacy forward, 1st party.
## How TF Do I Make A Blog
Configure your DNS and Firewall on your cloud provider. You should restrict port `333` to trust IP addresses only.
In the example I have used Linode API to get a wildcard cert. If you don't care about TLS, you are wrong, but I get it. Traefik will create self signed certs for you, but you will likely need to remove or modify the TLS settings in the compose file.
Once you have set up your cloud provider:
```
git clone thisRepo
cd thisRepo
cp .env.example .env
vim .env # make it match your environment
docker compose up
```
### Deployment
It is capable of running on a 1 shared CPU, with 2GB of RAM as tested on Linode.
Running on 2 shared CPUs with 4 GB of ram is sufficient for the full stack to run as smooth as butter for 99% of all users. On my personal fork unrelated to motherfuckingblog.com, I keep my Forgejo instance on another server with a Nextcloud instance and additional hardening. This isn't a bible, its a starter kit to make it easier for normal people to iterate from. Take what you need and throw the rest out.
### Security
Products should ship secure by default or at the least have a small handful of options that are easy to configure and harden.
This comes with Crowdsec WAF which will share limited data with crowdsec. If strict data isolation is essential. You need to remove this and consider a different WAF. There are alternatives, but at least be aware of what you are losing. Crowdsec with AppSec protects against human laziness. Essentially it acts as a real-time updated block list that matches against known malicious signatures in outdated software that you probably haven't patched yet, ya filthy animal.
All Admin Interfaces are restricted to an IP Allowlist. Almost every Org has VPNs, this is what they are meant for.
**If you are a noob** and you just `curl icanhazip.com ` then paste in your IP. *Prepare to temporarily lose access*. Your ISP will rotate your IP and you will think you are SOL. You are not. You need your own private VPN (which is actually easy to do), or you need to bind these to the local interface then perform ssh port forwarding anytime you want to perform maintenance. That sounds scary, but I promise it is like two simple commands. I'm pretty sure you can just change the port binding at the top of the compose file from `333:333` to `127.0.0.1:333:333`. Then you simply `ssh user@remote -L 333:127.0.0.1:333`
The Jekyll Admin interface had no authentication by default so basic http authentication was added via Traefik.
Additionally there is a strong benefit to using a non-standard port / custom entry point in Traefik (port 333 in this example) in the docker-compose file for this project. This allows for redudant whitelisting. On my cloud provider I also whitelist my IP address to those ports and deny all others. This means even if traefik IPAllowlisting is bypassed via some hacker black magic, I have a secondary defense.
#### Random Aside
It's just insane to me we just leave admin login portals open in the wild. WTF are we doing?
> Please guess employee passwords on my public website an unlimited number of times.
> \- **Every Chief Information Security Officer**
### Issues
##### It's ugly!
While, I find this beautiful. You may perfer one of the thousands of free themes for Jekyll which will make your site look exactly like the corporate garbage flooding the internet.
##### X Feature is Broken
Yep! Personally I know of the error that appears, but is incorrect in jekyll admin interface. But honestly that is at least half the point. If you properly restrict your broken tech behind network level access controls it mitigates the risk for a large portion of people.
Also this isn't a pick it up and make an identical blog. It is a starter kit. I know half of you are going to rip Jekyll admin out. If you plan on using it you are welcome to open issues with them or figure out the way to use. It works for my purposes and allows me to create, edit, and tag posts. I need nothing else.

View file

@ -0,0 +1,56 @@
---
title: Obligatory Rant/Background
layout: post
---
All I wanted was a simple small af static blog where I owned the stack and the data. But the **problems**:
* Thinking about CSS between paragraphs ruins my ability to write.
* TinyMCE's minified and compressed core is [100x larger](https://github.com/tinymce/tinymce/issues/4028) than my entire front end codebase.
* Managing more than a dozen posts is a nightmare.
* Wordpress is somehow even worse.
* Need to add search, tags, or any other features? Almost everything requires Node, PHP, or something insane that is probably millions of times larger than my entire codebase.
* Hosting? Every Jekyll post is how to host on Microsoft's Github. (Surely Microsoft would [never](https://arstechnica.com/information-technology/2025/02/copilot-exposes-private-github-pages-some-removed-by-microsoft/) [misuse](https://www.wired.com/story/github-commercial-ai-tool-built-open-source-code/) your data. )
* Want to know if anyone even looked at your site? Good thing Google hasn't been using your data to literally facilitate a genocide or anything... [oh wait](https://web.archive.org/web/20240720084622/https://www.wired.com/story/amazon-google-project-nimbus-israel-idf/).
So this is my opinionated attempt to move the needle the other direction.
**If you didn't understand anything I just said** throw it in an LLM/"AI" and ask it to explain. You can understanding anything you set your mind to. Don't doubt yourself.
**For the Nerds who are still with me**: This is a ***self-hosted*** **starter kit** for people sick of big bro. It is at the point where it could be taken several directions.
- Dev and Prod versions of the site for development? Check.
- 10kb static site? Check
- Layers of security? Check
- First Party data? Check
Check out other posts for some ideas or to learn more. In short, this is an opinionated **Docker Compose** project that uses:
- **Traefik** for reverse proxying all admin interfaces.
- **Jekyll** the same tech behind Github Pages, to build static sites.
- **Nginx** as the lightweight webserver.
- **Forgejo** for version control.
- **Umami Analytics** we are all a little vain.
- **Crowdsec WAF** for additional security.
This gives me a clean, cloud-based markdown writing environment with proper version control and monitoring.
### Deployment
It is capable of running on a 1 shared CPU, with 2GB of RAM. RAM is the limiting factor.
Running on 2 shared CPUs with 4 GB of ram is sufficient for the full stack to run as smooth as butter for 99.9% of all users.
On my personal fork unrelated to motherfuckingblog.com, I keep my Forgejo instance on another server with a Nextcloud instance and additional hardening. This isn't a bible, its a starter kit to make it easier for normal people to iterate from. Take what you need and throw the rest out.
### Security
Products should ship secure by default or at least have a small handful of options that are easy to configure and harden the product.
This comes with Crowdsec WAF which will share limited data with crowdsec. If strict data isolation is essential. You need to remove this and consider a different WAF. There are alternatives, but at least be aware of what you are losing. Crowdsec with AppSec protects against human laziness. Essentially **it acts as a real-time updated block list** that matches against known malicious signatures in outdated software that you probably haven't patched yet, ya filthy animal.
***All Admin Interfaces are restricted to an IP Allowlist.*** Almost every Org has VPNs, this is what they are meant for.
*If you are a complete noob*. It is okay. You will need to learn a bit to use this project, but it is within grasp. Don't discount yourself. **There is a big gotcha here.** If you just `curl icanhazip.com ` then paste in your IP on the allowlist. It will work. YAY! Party time! But *prepare to temporarily lose access*. Your ISP will rotate your IP and you will think you are shit outta luck. You are not. Ideally you would use your own private VPN (which they literally have scripts to deploy, it's easy to do), or you need to bind these to the local interface then perform ssh port forwarding anytime you want to perform maintenance/ view your dashboards. That sounds scary, but I promise it is like two simple commands. Though some additional config is probable.
The **Jekyll Admin interface had no authentication by default so basic http authentication was added** via Traefik. For God's sake, keep your admin interfaces from being externally accessible to anyone but trusted individuals.
Lastly, there is a strong benefit to **using a non-standard port / custom entry point in Traefik** (port 333 in this example) in the docker-compose file for this project. This allows for redudant whitelisting. On my cloud provider I also whitelist my IP address to those ports and deny all others. This means even if Traefik `IPAllowlist` is bypassed via some hacker black magic, I have a secondary defense.

View file

@ -0,0 +1,32 @@
---
layout: default
title: Archive
---
# Archive
{::nomarkdown}
{% assign current_year = "" %}
{% for post in site.posts %}
{% assign post_year = post.date | date: "%Y" %}
{% if post_year != current_year %}
{% unless forloop.first %}
</ul>
{% endunless %}
<h2 id="y{{ post_year }}">{{ post_year }}</h2>
<ul class="post-list">
{% assign current_year = post_year %}
{% endif %}
<li>
<span class="post-meta">{{ post.date | date: "%b %-d" }}</span>
<a href="{{ post.url | relative_url }}">{{ post.title }}</a>
</li>
{% if forloop.last %}
</ul>
{% endif %}
{% endfor %}
{:/}

View file

@ -0,0 +1,102 @@
---
---
// less-style-please theme styling
// Inspired by https://feeshy.github.io/less-style-please/
html {
font-size: 100%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.5;
color: #333;
background-color: #fff;
}
body {
max-width: 650px;
margin: 0 auto;
padding: 2rem 1rem;
}
header, footer {
text-align: center;
margin: 2rem 0;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 2rem;
margin-bottom: 1rem;
font-weight: 600;
}
a {
color: #0366d6;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
pre, code {
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-size: 0.9em;
background-color: #f6f8fa;
border-radius: 3px;
}
pre {
padding: 1rem;
overflow-x: auto;
}
code {
padding: 0.2em 0.4em;
}
blockquote {
margin-left: 0;
padding-left: 1rem;
border-left: 4px solid #ddd;
color: #666;
}
img {
max-width: 100%;
height: auto;
}
table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
}
th, td {
padding: 0.5rem;
border: 1px solid #ddd;
}
th {
background-color: #f6f8fa;
}
.post-list {
list-style: none;
padding: 0;
}
.post-list li {
margin-bottom: 1rem;
}
.post-meta {
color: #666;
font-size: 0.9em;
}
.pagination {
display: flex;
justify-content: space-between;
margin: 2rem 0;
}

View file

@ -0,0 +1,26 @@
---
title: Motherfucking Blog
layout: home
---
## What if it wasn't satire?
The OG motherfuckingwebsite.com (and the spinoffs) all insisted it was satire, but what if it wasn't? Seriously, look at all this unvarnished negative space. It is beautiful. Does your brain even know what to do when it isn't being inundated with ads or upsells?
<div markdown="0"><br><br></div>
##### Just stop and dwell in the empty space.
<div markdown="0"><br><br><br></div>
This isn't meditation. This is respect for you as a living being. You have probably never seen it on the internet.
You came here to see one madman's ravings and that is all you are getting. No insane nagging for a login or subscription. No selling you some shit you don't need. No cookies or their pain in the ass banners. No giving your data to other people. ***It's just you and an idea. It's fucking magical.*** I want it to happen more often so I put it in an easy to replicate package.
At a high-level, this is **self-hosted starter-kit of a blog platform** which makes it easier for tech and tech-curious people to create a diversity of blogs which are safer and respect end-users without any explicit dependencies on big tech. Oh and of course it is entirely free.
It satisfies the basic needs of four teams:
- **Devs**: Trusted Jekyll ecosystem with statically deployed pages that can run on a very small instance. Version control through Forgejo optional, but included.
- **Content Creators**: WYSIWYG Markdown editor with SEO optimizations.
- **Analytics and Data**: 1st party data without cookies.
- **Security**: Crowdsourced WAF. IP Allowlisting. TLS. Reverse Proxy. Redudant controls.