A lightweight, file-based CMS written in Go for managing markdown content with YAML frontmatter. Designed to work alongside a Next.js static site.
Live at: marcbachan.com
- Multi-content type support - Manage posts, photos, and custom content types from one dashboard
- Side-by-side live preview - Editor on left, real-time rendered preview on right
- Expandable inline previews - Click any item in the list to expand and preview without leaving the page
- Drag-and-drop image upload with automatic file organization
- Config-driven content types - Add new content types via JSON config, no code changes needed
- Markdown rendering with
marked.js - HTMX-enhanced UI for smooth interactions
- Session-based authentication
- Docker support
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ CMS Editor โ โ Markdown Files โ โ Next.js Site โ
โ (Go backend) โโโโโโถโ (_posts/, _photos/) โโโโโโโ (Frontend) โ
โ localhost:8080โ โ + images โ โ localhost:3000โ
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
- CMS Editor (this project) - A Go web app for creating/editing content
- Markdown Files - Content stored as
.mdfiles with YAML frontmatter - Next.js Site - Reads the markdown files and renders the public website
Each content item is a markdown file with YAML frontmatter:
---
title: "My Post Title"
excerpt: "A brief description"
coverImage: "/assets/img/my-post/cover.jpg"
date: "2025-01-17"
ogImage:
url: "/assets/img/my-post/cover.jpg"
tags: [art, featured]
---
Your markdown content here...cms/
โโโ main.go # App entrypoint and routes
โโโ config/
โ โโโ config.go # Config loader with content type support
โ โโโ config.json # Content type definitions
โโโ handlers/
โ โโโ auth.go # Login/logout handlers
โ โโโ blog.go # Legacy post handlers (kept for compatibility)
โ โโโ content.go # Generic content type handlers
โโโ model/
โ โโโ post.go # BlogPost struct (legacy)
โ โโโ content.go # Generic Content struct
โโโ storage/
โ โโโ reader.go # Read markdown with frontmatter
โ โโโ writer.go # Write markdown with frontmatter
โโโ templates/
โ โโโ dashboard.html # Content type overview
โ โโโ listcontent.html # List view with expandable previews
โ โโโ editcontent.html # Side-by-side editor
โ โโโ newcontent.html # Create form with live preview
โ โโโ login.html # Authentication
โ โโโ partials/
โ โโโ preview.html # HTMX preview partial
โโโ public/
โ โโโ styles/styles.css # CMS styling
โ โโโ tmp-preview/ # Temporary image uploads
โโโ Dockerfile
โโโ docker-compose.yml
โโโ README.md
- Go 1.24+
- (Optional) Docker + Docker Compose
Create a .env file in the cms/ directory:
CMS_USER="admin"
CMS_PASS="your-password"
SESSION_SECRET="your-secret-key"Edit config.json to define your content types:
{
"postsDir": "../_posts",
"imagesDir": "../public/assets/img",
"contentTypes": [
{
"name": "Posts",
"slug": "posts",
"directory": "../_posts",
"imagesDir": "../public/assets/img",
"icon": "๐"
},
{
"name": "Photos",
"slug": "photos",
"directory": "../_photos",
"imagesDir": "../public/assets/photo",
"icon": "๐ท"
}
]
}cd cms
go run main.goVisit: http://localhost:8080
docker-compose up --buildAfter logging in, you'll see the dashboard with all configured content types displayed as cards. Each card shows the count of items in that collection.
- Click a content type card (e.g., "Posts")
- See all items listed with title, date, and tags
- Click any row to expand an inline preview
- Use tag filters to narrow down the list
- Click "+ Create New" from any content list
- Fill in the form fields (title, excerpt, tags, date)
- Drag and drop an image onto the dropzone
- Write your markdown content
- Watch the live preview update on the right
- Click "Create" to save
- Click any item title to open the editor
- Use the side-by-side view: editor on left, live preview on right
- All changes preview instantly as you type
- Click "Save Changes" when done
- From the list view, click the "X Delete" button
- Confirm the deletion in the prompt
- The markdown file and associated images are removed
To add a new content type (e.g., "Projects"):
mkdir ../_projects{
"contentTypes": [
// ... existing types ...
{
"name": "Projects",
"slug": "projects",
"directory": "../_projects",
"imagesDir": "../public/assets/projects",
"icon": "๐"
}
]
}That's it! The new content type will appear on the dashboard automatically.
| URL | Description |
|---|---|
/ |
Dashboard (after login) |
/login |
Login page |
/{type} |
List all items of a content type |
/{type}/new |
Create new item |
/{type}/edit/{slug} |
Edit existing item |
/{type}/preview/{slug} |
HTMX preview partial |
/api/{type} |
POST - Create item |
/api/{type}/{slug} |
PUT - Update, DELETE - Remove |
/api/upload |
POST - Upload image |
- User drags image to dropzone
- Image uploads to
/public/tmp-preview/with a UUID filename - Preview URL returned immediately for live preview
- On content creation, image moves to final location:
/{imagesDir}/{slug}/{filename} - Temp folder is cleared after successful save
Images are stored relative to the public folder:
/assets/img/my-post/cover.jpg # For posts
/assets/photo/sunset/image.jpg # For photos
This CMS is designed to work with a Next.js site that reads markdown files. The site should:
- Read content from
_posts/,_photos/, etc. - Parse frontmatter with
gray-matter - Render markdown with
remarkandremark-html - Serve images from the
public/folder
In your root package.json:
{
"scripts": {
"dev": "concurrently \"yarn dev:cms\" \"yarn dev:next\"",
"dev:cms": "cd cms && go run main.go",
"dev:next": "next dev"
}
}Then run:
yarn dev- CMS runs on
http://localhost:8080 - Next.js runs on
http://localhost:3000
- Gorilla Mux - HTTP router
- Gorilla Sessions - Session management
- HTMX - Dynamic HTML interactions
- marked.js - Markdown parsing in browser
- gopkg.in/yaml.v3 - YAML parsing
All content uses the same frontmatter schema:
title: string # Required
excerpt: string # Optional description
coverImage: string # Image URL path
date: string # ISO date (YYYY-MM-DD)
ogImage:
url: string # Open Graph image URL
tags: [string] # Array of tagsFrom the cms/ directory:
docker-compose up --buildThis runs just the CMS at http://localhost:8080.
From the project root:
# Copy environment template
cp .env.example .env
# Edit credentials
nano .env
# Run both services
docker-compose up --buildThis starts:
- CMS at
http://localhost:8080 - Next.js at
http://localhost:3000
docker build -t cms .
docker run -p 8080:8080 \
-e CMS_USER=admin \
-e CMS_PASS=password \
-e SESSION_SECRET=secret \
-v $(pwd)/../_posts:/app/_posts \
-v $(pwd)/../_photos:/app/_photos \
-v $(pwd)/../public:/app/public \
-v $(pwd)/config.docker.json:/app/config.json \
cms| File | Purpose |
|---|---|
config.json |
Local development paths (../) |
config.docker.json |
Docker paths (./) |
The Docker setup mounts config.docker.json as config.json automatically.
- Check that the
slugin the URL matches a content type inconfig.json - Ensure the content type's
directoryexists
- Verify the image path starts with
/(e.g.,/assets/img/...) - Check that the Next.js dev server is running to serve images from
public/
- Ensure
.envfile exists withCMS_USER,CMS_PASS, andSESSION_SECRET - Check that environment variables are being loaded (restart the server)
- OAuth or JWT-based auth
- Scheduled/draft post status
- Bulk operations (delete multiple, tag multiple)
- Search across all content
- Custom fields per content type
- Markdown linting and syntax highlighting