A self-hosted markdown blog system built with SvelteKit and Bun. Designed for personal writing and content management, with a focus on simplicity and performance.
- SSR + PWA — Server-side rendering with offline support via service workers
- Responsive Design — Optimized for both desktop and mobile devices
- Markdown Editor — Full-featured editor built on CodeMirror 6 with custom toolbar and paste-to-upload
- Syntax Highlighting — Code blocks styled with highlight.js
- Article Management — Create, edit, and organize markdown posts with version diff history
- Tags — Categorize content with a tag system; Chinese titles auto-generate pinyin slugs
- Comments — Built-in comment management with moderation support
- File Management — Upload and manage static files and images
- Image Viewer — Click-to-zoom image preview powered by Viewer.js
- Image Compression — Automatic client-side image compression on upload
- Visit Statistics — Track page views (PUV) with an admin dashboard
- Blog Mode — Publish posts publicly or keep them private
- RSS Feed — Auto-generated RSS feed for public posts
- Sitemap & robots.txt — Auto-generated for search engine indexing
- Turnstile Integration — Cloudflare Turnstile CAPTCHA with verified bot bypass
- Firewall — IP-based access control, bot detection, custom rules with time scheduling
- UA Collection Detection — Detect distributed crawlers by grouping IPs sharing the same User-Agent
- Geo IP Location — Display visitor geographic information based on IP2Location Lite
- Cloudflare Integration — Auto-push blocked IPs to Cloudflare IP Lists for edge-level filtering
- IP Aggregation — Automatically merge blacklist IPs into /24 and /16 CIDR blocks to reduce list size
- Backup & Restore — Export and import data for disaster recovery
- Self-Hosted — Runs on your own server, all data stays with you
| Category | Technology |
|---|---|
| Framework | SvelteKit (SSR) |
| Runtime | Bun |
| Database | SQLite via bun:sqlite |
| Styling | SCSS + clsx |
| Fonts | SUIT · Noto Sans SC |
| Markdown Editor | CodeMirror 6 + svelte-codemirror-editor |
| Markdown Renderer | marked + highlight.js |
| Type Checking | TypeScript |
- Bun (latest)
# Clone the repository
git clone https://github.com/aolose/emm.git
cd emm
# Install dependencies
bun install
# Build for production
bun run build
# Start the server
bun run previewThe application will be available at http://localhost:4173.
| Variable | Default | Description |
|---|---|---|
PORT |
4173 |
Server listen port |
ORIGIN |
http://localhost:4173 |
Public origin URL (required for RSS / sitemap) |
Set them before starting the server:
PORT=3000 ORIGIN=https://blog.example.com bun run previewOn first run, visit the /config page to set up your admin username and password. No registration system — single admin account.
All settings are managed through the Admin UI (/admin/setting), stored in the SQLite database:
- Blog Info: blog name, bio, SEO keywords/description, social links
- Upload/Thumbnail Directories: configurable storage paths for uploaded files and generated thumbnails
- Geo Location: ip2location lite token and database directory for IP-based country blocking
- Comments: enable/disable comments, moderation toggle
- Firewall Rules: IP/header/path-based access rules, rate limiting, custom responses
Runtime files and directories:
- SQLite database — configured path (stores posts, tags, config, firewall rules, etc.)
- Upload directory — configured in admin settings (defaults to
data/upload) - Thumbnail directory — configured in admin settings (defaults to
data/thumb)
bun install
bun run build
# The built output is in the `dist/` directory
bun run dist/index.jsFor long-running production use, consider a process manager:
# systemd example (/etc/systemd/system/emm.service)
[Unit]
Description=EMM Blog
After=network.target
[Service]
Type=simple
User=emm
WorkingDirectory=/home/emm/app
Environment=PORT=3000
Environment=ORIGIN=https://blog.example.com
ExecStart=bun run dist/index.js
Restart=on-failure
[Install]
WantedBy=multi-user.targetOr with PM2:
pm2 start dist/index.js --name emm --interpreter bunEMM integrates Cloudflare Turnstile CAPTCHA and includes a built-in search engine crawler whitelist bypass logic.
If you use Cloudflare proxy and have Turnstile enabled, you need to configure a Transform Rule to ensure SEO crawlers are not blocked.
For detailed steps, see: doc/turnstile.md
Security Reminder: Make sure to protect your origin server to prevent attackers from bypassing Cloudflare and directly accessing the origin IP to forge request headers. It is recommended to use Cloudflare Tunnel or restrict your origin firewall to only accept Cloudflare IP ranges.
Issues and pull requests are welcome. Before submitting a PR:
- Fork the repository
- Create a feature branch (
git checkout -b feature/your-feature) - Commit your changes
- Push and open a pull request
MIT © Aolose











