A modern self-hosted markdown blog system powered by SvelteKit, Bun, and DeepSeek AI. Features built-in security firewall and smart writing assistant.
- Screenshots
- Features
- AI Assistant
- Tech Stack
- Getting Started
- Turnstile Anti-Crawl Setup
- Contributing
- License
- 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
- Mermaid Diagrams — Render flowcharts and diagrams in posts with SSR support
- 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
- R2 Storage — Upload files to Cloudflare R2 (S3-compatible) with per-file sync tracking and hash-based URLs
- 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
- AI Assistant — DeepSeek-powered writing assistant with editor-integrated tool-calling
- Self-Hosted — Runs on your own server, all data stays with you
The editor includes a DeepSeek AI assistant that can read your document and apply edits directly. Available from the admin write page toolbar (✦ button).
- Go to Admin → Settings → AI Integration
- Enter your DeepSeek API Key
- Choose a model (default:
deepseek-chat) - Click Test Connection to verify, then Save
The AI uses function calling to interact with the editor:
Read tools — inspect the document:
getSelection, getCurrentLine, getCurrentParagraph, getCurrentSection, getFullDocument, getTitle
Write tools — modify the document:
replaceText, replaceCurrentLine, replaceCurrentParagraph, replaceFullDocument, insertAtCursor, setTitle
All write operations show an inline confirmation card ([Apply] / [Dismiss]) in the chat before executing. No modal dialogs.
- Open the admin write page and select a post
- Click the ✦ button in the editor toolbar
- Ask the AI — e.g. "fix this line", "suggest a title", "polish this article"
- Review the proposed changes and click Apply or Dismiss
- AI handles configuration checks automatically. If the API key is missing or invalid, a prompt will guide you to Settings
| 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 |
| AI | DeepSeek API (function calling) |
| Diagrams | Mermaid (SSR + client-side) |
| 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
- AI Integration: DeepSeek API key and model selection
- 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.
EMM supports uploading files directly to Cloudflare R2 instead of local disk. R2 offers zero egress fees and S3-compatible API, making it ideal for image-heavy blogs.
- Create an R2 bucket in the Cloudflare Dashboard
- Create an R2 API Token with Object Read & Write permissions for your bucket
- Bind a custom domain to the bucket (e.g.,
cdn.example.com) and enable public access - Go to Admin → Settings → R2 Storage and fill in:
- Account ID — your Cloudflare Account ID
- Access Key ID / Secret Access Key — from the R2 API token
- Bucket — bucket name
- Public Domain — custom domain bound to the bucket
- Toggle Enable R2 storage on
- Click Save
Once enabled, new uploads go directly to R2 and the API returns https://cdn.example.com/<hash> URLs. Existing local files continue to serve from disk via /res/<id>.
Run the migration script to move existing local files to R2 and replace all /res/ references in articles:
bun run scripts/migrate-to-r2.tsWhat it does:
- Uploads all local files to R2 (using hash-based keys derived from MD5)
- Verifies each upload with a HEAD request before deleting the local copy
- Replaces
/res/<id>and/res/_<id>references in post content with R2 URLs - Backfills
r2Keyandr2Syncedcolumns on theRestable
The script is idempotent — running it multiple times is safe; already-migrated files are skipped.
- Hash-based URLs: R2 keys use the first 6 characters of the file's MD5 hash (
a3f2b1), so re-uploading the same file keeps the same URL, while different content automatically gets a new URL (no CDN cache issues) - Per-file sync tracking:
r2Syncedflag on each resource determines whether to use the R2 URL or fall back to local/res/— no global toggle affecting all files at once - Thumbnails: Auto-generated thumbnails (300px WebP) use
_prefix keys (e.g.,_a3f2b1) - Graceful fallback:
/res/<id>endpoint redirects to R2 (301) when local file is missing andr2Syncedis set
# Basic R2 connectivity (PUT / HEAD / DELETE / public URL)
bun run scripts/test-r2.ts
# Migration logic (upload, verify, delete local, idempotency)
bun run scripts/test-r2-migrate.ts
# Full migration E2E (DB setup, article replacement, verification, cleanup)
bun run scripts/test-r2-full-migrate.tsIssues 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
This project is open source and available under the MIT License. See the LICENSE file for details.












