{"id":44616932,"url":"https://github.com/olegiv/ocms-go","last_synced_at":"2026-04-02T00:04:05.296Z","repository":{"id":335671588,"uuid":"1146046068","full_name":"olegiv/ocms-go","owner":"olegiv","description":"Lightweight CMS built with Go and SQLite. Features HTMX/Alpine.js admin, REST API, theme \u0026 module systems, multi-language support, webhooks, media library, form builder and SEO tools. Single binary, zero config database.","archived":false,"fork":false,"pushed_at":"2026-03-25T00:12:21.000Z","size":5458,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-03-26T04:08:10.068Z","etag":null,"topics":["alpinejs","cms","content-management-system","form-builder","go","golang","headless-cms","htmx","i18n","media-library","modules","rest-api","seo","sqlc","sqlite","themes","webhooks"],"latest_commit_sha":null,"homepage":"https://ocms.tech","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/olegiv.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-01-30T14:55:04.000Z","updated_at":"2026-03-25T00:12:24.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/olegiv/ocms-go","commit_stats":null,"previous_names":["olegiv/ocms-go"],"tags_count":12,"template":false,"template_full_name":null,"purl":"pkg:github/olegiv/ocms-go","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olegiv%2Focms-go","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olegiv%2Focms-go/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olegiv%2Focms-go/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olegiv%2Focms-go/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/olegiv","download_url":"https://codeload.github.com/olegiv/ocms-go/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olegiv%2Focms-go/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31293163,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T21:15:39.731Z","status":"ssl_error","status_checked_at":"2026-04-01T21:15:34.046Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["alpinejs","cms","content-management-system","form-builder","go","golang","headless-cms","htmx","i18n","media-library","modules","rest-api","seo","sqlc","sqlite","themes","webhooks"],"created_at":"2026-02-14T13:09:40.522Z","updated_at":"2026-04-02T00:04:05.286Z","avatar_url":"https://github.com/olegiv.png","language":"Go","readme":"# oCMS\n\n[![Go](https://github.com/olegiv/ocms-go/actions/workflows/go.yml/badge.svg)](https://github.com/olegiv/ocms-go/actions/workflows/go.yml)\n[![CodeQL](https://github.com/olegiv/ocms-go/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/olegiv/ocms-go/actions/workflows/github-code-scanning/codeql)\n[![Dependency review](https://github.com/olegiv/ocms-go/actions/workflows/dependency-review.yml/badge.svg)](https://github.com/olegiv/ocms-go/actions/workflows/dependency-review.yml)\n\nA lightweight content management system built with Go, featuring a modern admin interface, session-based authentication, SQLite storage, and extensible architecture with themes and modules.\n\n## Features\n\n### Content Management\n- **Page Management**: Create, edit, publish, and version pages with a rich content editor\n- **Scheduled Publishing**: Schedule pages to publish at a future date/time\n- **Media Library**: Upload and manage images, documents, and videos with automatic image processing\n  - Automatic thumbnail and variant generation\n  - Folder organization\n  - Featured image support for pages\n- **Menu Builder**: Create navigation menus with drag-and-drop ordering\n  - Hierarchical menu structures\n  - Link to pages or external URLs\n  - Multiple menu locations\n- **Full-Text Search**: Built-in SQLite FTS5 search for fast content discovery\n\n### Taxonomy\n- **Categories**: Organize content with hierarchical categories\n- **Tags**: Add flat taxonomy tags to pages\n\n### Forms\n- **Form Builder**: Create contact forms, surveys, and data collection forms\n  - Multiple field types (text, email, textarea, select, checkbox, radio)\n  - Form submissions management\n  - Read/unread status tracking\n  - Email notifications\n\n### Theme System\n- **Multiple Themes**: Switch between different frontend themes\n- **Theme Settings**: Configurable theme options (colors, layout, etc.)\n- **Template Override**: Themes can customize page templates\n- **Static Assets**: Theme-specific CSS, JavaScript, and images\n\n### Module System\n- **Extensible Architecture**: Add custom functionality via modules\n- **Module Lifecycle**: Init, routes, admin routes, and shutdown hooks\n- **Module Migrations**: Modules can have their own database migrations\n- **Template Functions**: Modules can add custom template functions\n- **Active Status Toggle**: Enable/disable modules from admin UI without restart\n- **Module Translations**: Modules can embed their own i18n locale files\n\n### REST API\n- **Full CRUD API**: Complete REST API for pages, media, tags, and categories\n- **API Key Authentication**: Secure API access with bearer token authentication\n- **Permission-Based Access**: Fine-grained permissions (read/write per resource)\n- **Rate Limiting**: Per-key and global rate limiting\n- **API Documentation**: Built-in API documentation page\n\n### SEO\n- **Meta Tags**: Custom title, description, and keywords per page\n- **Open Graph**: Full Open Graph and Twitter Card support\n- **Sitemap**: Auto-generated sitemap.xml\n- **Robots.txt**: Configurable robots.txt generation\n- **Canonical URLs**: Set canonical URLs to avoid duplicate content\n- **NoIndex/NoFollow**: Control search engine indexing per page\n\n### Administration\n- **User Management**: Role-based access control (admin/editor)\n- **Authentication**: Secure session-based authentication with argon2id password hashing\n- **Event Logging**: Comprehensive audit trail for all actions\n- **Admin Dashboard**: Modern responsive UI with HTMX and Alpine.js\n  - Statistics overview\n  - Recent submissions widget\n  - Quick actions\n- **Cache Management**: View cache stats and clear cache\n- **API Key Management**: Create and manage API keys\n- **Bulk List Actions**: Multi-select and bulk delete/revoke on paged admin lists (pages, tags, users, API keys, media, and form submissions)\n- **Per-Page Selector**: Choose items per page on delete-capable admin lists (URL query `per_page`, current-page state in URL only)\n- **List Sorting**: Sort delete-capable admin lists by safe whitelisted columns with clear active sort highlighting (URL queries `sort` + `dir`)\n- **SQLite Database**: Zero-configuration embedded database with migrations\n\n### Multi-Language Support\n- **Content Translation**: Translate pages, categories, and tags into multiple languages\n- **Language Management**: Configure site languages with ISO 639-1 codes\n- **Translation Linking**: Link content across languages for seamless switching\n- **Language Switcher**: Built-in frontend component for language navigation\n- **URL Prefixes**: Language-prefixed URLs (e.g., `/ru/about-us`)\n- **RTL Support**: Right-to-left language support\n- **Admin UI Localization**: Translatable admin interface (English + Russian included)\n\n### Webhooks\n- **Event System**: Trigger webhooks on content events (create, update, delete, publish)\n- **Delivery Tracking**: Monitor delivery status and response codes\n- **Retry Logic**: Exponential backoff retry for failed deliveries\n- **HMAC Signatures**: Secure payloads with HMAC-SHA256 signatures\n- **Custom Headers**: Add custom headers to webhook requests\n- **Dead Letter Queue**: Track permanently failed deliveries\n- **Event Debouncing**: Coalesce rapid-fire events to reduce webhook noise\n\n### Import/Export\n- **JSON Export**: Export site content to portable JSON format\n- **ZIP Export**: Include media files in export archives\n- **Selective Export**: Choose which content types to include\n- **JSON Import**: Import content from JSON files\n- **ZIP Import**: Restore media files from archives\n- **Conflict Resolution**: Skip, overwrite, or rename on conflicts\n- **Dry Run Mode**: Preview import changes before applying\n\n### Performance\n- **Multi-Level Caching**: In-memory and optional Redis caching\n- **Redis Support**: Distributed caching for multi-instance deployments\n- **Response Compression**: Gzip compression for HTML and JSON responses\n- **Graceful Shutdown**: Clean shutdown with request draining\n- **Health Check**: `/health` endpoint for monitoring\n\n## Prerequisites\n\n- Go 1.26 or later\n- [Node.js](https://nodejs.org/) (npm) for frontend dependencies\n- [sqlc](https://sqlc.dev/) for SQL code generation\n- [templ](https://templ.guide/) for type-safe HTML templates\n- [goose](https://github.com/pressly/goose) for database migrations\n- [Dart Sass](https://sass-lang.com/dart-sass) for SCSS compilation\n- [libvips](https://www.libvips.org/) for image processing (required for media library)\n\n### Installing libvips\n\n**macOS:**\n```bash\nbrew install vips\n```\n\n**Ubuntu/Debian:**\n```bash\nsudo apt-get install libvips-dev\n```\n\n**Fedora:**\n```bash\nsudo dnf install vips-devel\n```\n\n## Installation\n\n1. Clone the repository:\n   ```bash\n   git clone https://github.com/olegiv/ocms-go.git\n   cd ocms-go\n   ```\n\n2. Install dependencies:\n   ```bash\n   go mod download\n   ```\n\n3. Install required tools:\n   ```bash\n   go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest\n   go install github.com/a-h/templ/cmd/templ@latest\n   go install github.com/pressly/goose/v3/cmd/goose@latest\n   ```\n\n4. Generate code:\n   ```bash\n   sqlc generate\n   templ generate\n   ```\n\n5. Build assets (installs npm dependencies and compiles SCSS):\n   ```bash\n   make assets\n   ```\n\n## Environment Variables\n\n| Variable | Description | Default | Required |\n|----------|-------------|---------|----------|\n| `OCMS_SESSION_SECRET` | Secret key for session encryption (min 32 bytes) | - | Yes |\n| `OCMS_DB_PATH` | Path to SQLite database file | `./data/ocms.db` | No |\n| `OCMS_SERVER_HOST` | Server host address | `localhost` | No |\n| `OCMS_SERVER_PORT` | Server port number | `8080` | No |\n| `OCMS_ENV` | Environment mode (`development`/`production`) | `development` | No |\n| `OCMS_LOG_LEVEL` | Log level (`debug`/`info`/`warn`/`error`) | `info` | No |\n| `OCMS_CUSTOM_DIR` | Directory for custom themes and modules | `./custom` | No |\n| `OCMS_ACTIVE_THEME` | Name of the active theme | `default` | No |\n| `OCMS_DO_SEED` | Seed database with default admin and config | `false` | No |\n| `OCMS_CACHE_TTL` | Default cache TTL in seconds | `3600` | No |\n| `OCMS_REDIS_URL` | Redis URL for distributed caching | - | No |\n| `OCMS_CACHE_PREFIX` | Redis key prefix | `ocms:` | No |\n| `OCMS_CACHE_MAX_SIZE` | Max entries for in-memory cache | `10000` | No |\n| `OCMS_HCAPTCHA_SITE_KEY` | hCaptcha site key for login protection | - | No |\n| `OCMS_HCAPTCHA_SECRET_KEY` | hCaptcha secret key for login protection | - | No |\n| `OCMS_GEOIP_DB_PATH` | Path to GeoLite2-Country.mmdb for country detection | - | No |\n| `OCMS_UPLOADS_DIR` | Directory for uploaded media files | `./uploads` | No |\n| `OCMS_TRUSTED_PROXIES` | Trusted reverse-proxy CIDRs/IPs; forwarding headers are ignored unless peer is trusted | - | No |\n| `OCMS_REQUIRE_TRUSTED_PROXIES` | Fail startup in production if trusted proxy CIDRs/IPs are not configured | `false` (`true` in production when unset) | No |\n| `OCMS_API_ALLOWED_CIDRS` | Global source CIDRs/IPs allowed to use API keys | - | No |\n| `OCMS_REQUIRE_API_ALLOWED_CIDRS` | Fail API key auth when global API source CIDRs are not configured | `false` (`true` in production when unset) | No |\n| `OCMS_REQUIRE_API_KEY_EXPIRY` | Require API keys to have expiration timestamps | `false` (`true` in production when unset) | No |\n| `OCMS_REQUIRE_API_KEY_SOURCE_CIDRS` | Require API keys to have per-key source CIDR restrictions | `false` (`true` in production when unset) | No |\n| `OCMS_REVOKE_API_KEY_ON_SOURCE_IP_CHANGE` | Deactivate API keys when source IP changes and the key has no per-key CIDRs | `false` (`true` in production when unset) | No |\n| `OCMS_API_KEY_MAX_TTL_DAYS` | Maximum API key lifetime in days (`0` disables, max `365`) | `0` (`90` in production when unset) | No |\n| `OCMS_EMBED_ALLOWED_ORIGINS` | Allowed browser origins for public embed proxy routes; required for working browser embed requests in production | - | No |\n| `OCMS_EMBED_ALLOWED_UPSTREAM_HOSTS` | Allowed upstream hosts for embed provider API endpoints | - | No |\n| `OCMS_REQUIRE_EMBED_ALLOWED_ORIGINS` | Fail startup in production if embed proxy is active without origin allowlist | `false` (`true` in production when unset) | No |\n| `OCMS_REQUIRE_EMBED_ALLOWED_UPSTREAM_HOSTS` | Fail startup in production if embed proxy is active without upstream host allowlist | `false` (`true` in production when unset) | No |\n| `OCMS_EMBED_PROXY_TOKEN` | Secret used to mint short-lived signed embed proxy tokens; required for active embed proxy in production | - | No |\n| `OCMS_REQUIRE_EMBED_PROXY_TOKEN` | Enforce embed proxy token requirement in non-production too | `false` | No |\n| `OCMS_REQUIRE_HTTPS_OUTBOUND` | Require HTTPS for outbound integration URLs | `false` (`true` in production when unset) | No |\n| `OCMS_REQUIRE_FORM_CAPTCHA` | Require captcha on all public form submissions | `false` (`true` in production when unset) | No |\n| `OCMS_WEBHOOK_FORM_DATA_MODE` | `form.submitted` payload data mode (`redacted`/`none`/`full`) | `redacted` | No |\n| `OCMS_REQUIRE_WEBHOOK_FORM_DATA_MINIMIZATION` | Fail startup in production when form webhook payload mode is `full` | `false` (`true` in production when unset) | No |\n| `OCMS_WEBHOOK_ALLOWED_HOSTS` | Allowed destination hosts for active webhook deliveries (exact hostname match) | - | No |\n| `OCMS_REQUIRE_WEBHOOK_ALLOWED_HOSTS` | Fail startup in production when active webhooks exist without destination host allowlist | `false` (`true` in production when unset) | No |\n| `OCMS_SANITIZE_PAGE_HTML` | Sanitize page HTML before rendering to visitors | `false` (`true` in production when unset) | No |\n| `OCMS_REQUIRE_SANITIZE_PAGE_HTML` | Fail startup in production if page HTML sanitization is disabled | `false` (`true` in production when unset) | No |\n| `OCMS_BLOCK_SUSPICIOUS_PAGE_HTML` | Reject page writes containing suspicious HTML patterns | `false` (`true` in production when unset) | No |\n| `OCMS_REQUIRE_BLOCK_SUSPICIOUS_PAGE_HTML` | Fail startup in production when suspicious page markup blocking is disabled or existing pages contain suspicious markers | `false` (`true` in production when unset) | No |\n| `OCMS_DEMO_MODE` | Enable demo content seeding (users, pages, media) | `false` | No |\n\n## Development\n\n### Quick Start\n\n```bash\n# Set required environment variable\nexport OCMS_SESSION_SECRET=\"your-secret-key-at-least-32-bytes\"\n\n# Install repository-managed git hooks (run once per clone)\nmake install-hooks\n\n# Run with asset compilation\nmake dev\n\n# Or run without rebuilding assets\nmake run\n```\n\n### Available Make Commands\n\n| Command | Description |\n|---------|-------------|\n| `make dev` | Build assets and run development server |\n| `make run` | Run development server (no asset build) |\n| `make stop` | Stop development server on port 8080 |\n| `make restart` | Stop and restart development server |\n| `make build` | Build binary to `bin/ocms` |\n| `make build-prod` | Build optimized binary (stripped, trimmed) |\n| `make build-linux-amd64` | Cross-compile for Linux AMD64 |\n| `make build-darwin-arm64` | Cross-compile for macOS ARM64 |\n| `make build-all-platforms` | Build for Linux AMD64 and macOS ARM64 |\n| `make test` | Run all tests |\n| `make clean` | Remove build artifacts |\n| `make clean-db` | Remove database files |\n| `make migrate-up` | Apply pending migrations |\n| `make migrate-down` | Rollback last migration |\n| `make migrate-status` | Show migration status |\n| `make migrate-create` | Create new migration file |\n| `make assets` | Install npm deps and compile SCSS |\n| `make sqlc` | Regenerate sqlc code from SQL queries |\n| `make templ` | Regenerate templ Go code from `.templ` files |\n| `make deploy-binary` | Deploy binary to remote server (no custom content) |\n| `make commit-prepare` | Proxy to Claude slash command `/commit-prepare` |\n| `make commit-do` | Proxy to Claude slash command `/commit-do` |\n| `make code-quality` | Proxy to Claude slash command `/code-quality` |\n| `make security-audit` | Proxy to Claude slash command `/security-audit` |\n| `make commit-prepare-local` | Run local commit-prepare shell script |\n| `make commit-do-local` | Run local commit-do shell script |\n| `make code-quality-local` | Run local code quality shell script (`golangci-lint`, `nilaway`, `go test`) |\n| `make security-audit-local` | Run local security audit shell script (writes to `.audit/`) |\n| `make install-hooks` | Configure git to use repository-managed hooks from `.githooks` |\n| `make check-no-absolute-paths` | Fail if tracked files contain local absolute paths (`/Users/...`, `/home/...`, `C:\\Users\\...`) |\n\n### Default Admin Credentials\n\nOn first run with `OCMS_DO_SEED=true`, the application seeds a default admin user:\n- **Email**: admin@example.com\n- **Password**: changeme1234\n\nChange these credentials immediately after first login.\n\n### Demo Mode\n\nWith `OCMS_DEMO_MODE=true` (requires `OCMS_DO_SEED=true`), additional demo content is seeded including sample pages, categories, tags, media, and menu items. Two demo users are created:\n\n- **Admin**: demo@example.com / demo1234demo\n- **Editor**: editor@example.com / demo1234demo\n\nSee [docs/demo-deployment.md](docs/demo-deployment.md) for Fly.io deployment and demo configuration details.\n\n## Docker\n\n### Quick Start with Docker\n\n```bash\n# Clone the repository\ngit clone https://github.com/olegiv/ocms-go.git\ncd ocms-go\n\n# Start with Docker Compose (generates session secret automatically)\nOCMS_SESSION_SECRET=$(openssl rand -base64 32) OCMS_DO_SEED=true docker compose up -d\n\n# View logs\ndocker compose logs -f ocms\n```\n\nAccess the admin panel at http://localhost:8080/admin with:\n- **Email**: admin@example.com\n- **Password**: changeme1234\n\n### Docker Commands\n\n| Command | Description |\n|---------|-------------|\n| `docker compose up -d` | Start oCMS |\n| `docker compose --profile redis up -d` | Start with Redis caching |\n| `docker compose down` | Stop services |\n| `docker compose logs -f ocms` | View logs |\n| `docker compose pull \u0026\u0026 docker compose up -d` | Update to latest |\n\n### Volume Mounts\n\n| Volume | Container Path | Purpose |\n|--------|----------------|---------|\n| `ocms_data` | `/app/data` | SQLite database |\n| `ocms_uploads` | `/app/uploads` | Media uploads |\n| `./custom` | `/app/custom` | Custom themes and modules |\n\n### Building the Docker Image\n\n```bash\n# Build with version info\ndocker build \\\n  --build-arg VERSION=$(git describe --tags --always) \\\n  --build-arg GIT_COMMIT=$(git rev-parse --short HEAD) \\\n  --build-arg BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) \\\n  -t ocms:latest .\n```\n\n### Production Configuration\n\nCreate a `.env` file for production:\n\n```bash\nOCMS_SESSION_SECRET=your-secure-secret-key-at-least-32-bytes\nOCMS_ENV=production\nOCMS_DO_SEED=false\nOCMS_ACTIVE_THEME=default\nOCMS_TRUSTED_PROXIES=127.0.0.1/32,10.0.0.0/8\nOCMS_API_ALLOWED_CIDRS=203.0.113.0/24\n\n# Hardened embed proxy baseline (when embed module/provider is enabled)\n# Origin matching is exact (scheme + host). Include all real hostnames.\nOCMS_EMBED_ALLOWED_ORIGINS=https://example.com,https://www.example.com\nOCMS_EMBED_ALLOWED_UPSTREAM_HOSTS=api.dify.ai\nOCMS_REQUIRE_EMBED_ALLOWED_ORIGINS=true\nOCMS_REQUIRE_EMBED_ALLOWED_UPSTREAM_HOSTS=true\nOCMS_EMBED_PROXY_TOKEN=replace-with-embed-proxy-token\nOCMS_WEBHOOK_ALLOWED_HOSTS=hooks.example.com,events.example.com\n\n# Required when OCMS_REQUIRE_FORM_CAPTCHA is enabled (enabled by default in production)\nOCMS_HCAPTCHA_SITE_KEY=your-site-key\nOCMS_HCAPTCHA_SECRET_KEY=your-secret-key\n\n# Optional: Redis caching\nOCMS_REDIS_URL=redis://redis:6379/0\n```\n\nThen start with:\n```bash\ndocker compose --profile redis up -d\n```\n\n## Project Structure\n\n```\nocms-go/\n├── cmd/ocms/             # Application entry point\n├── docs/                 # Documentation\n│   ├── multi-language.md # Multi-language guide\n│   ├── webhooks.md       # Webhooks configuration\n│   ├── import-export.md  # Import/export guide\n│   └── reverse-proxy.md  # Nginx/Apache/NPM setup\n├── internal/\n│   ├── auth/             # Password hashing utilities\n│   ├── cache/            # Caching layer (memory + Redis)\n│   ├── config/           # Configuration loading\n│   ├── handler/          # HTTP handlers\n│   │   └── api/          # REST API handlers\n│   ├── i18n/             # Internationalization (admin UI)\n│   ├── imaging/          # Image processing (thumbnails, variants)\n│   ├── middleware/       # HTTP middleware (auth, API, rate limiting)\n│   ├── model/            # Domain models\n│   ├── module/           # Module system (registry, hooks)\n│   ├── render/           # Template rendering\n│   ├── scheduler/        # Cron-based task scheduler\n│   ├── seo/              # SEO utilities (sitemap, robots.txt, meta)\n│   ├── service/          # Business logic (media, menus, forms)\n│   ├── session/          # Session management\n│   ├── store/            # Database layer (sqlc generated)\n│   │   ├── migrations/   # Goose SQL migrations\n│   │   └── queries/      # sqlc query definitions\n│   ├── theme/            # Theme loading and management\n│   ├── themes/           # Embedded core themes (default, developer)\n│   ├── transfer/         # Import/export functionality\n│   ├── util/             # Utility functions (slug generation)\n│   └── webhook/          # Webhook system\n├── modules/              # Custom modules directory\n│   └── example/          # Example module implementation\n├── custom/               # User content directory (gitignored)\n│   ├── themes/           # Custom themes (override or extend core)\n│   └── modules/          # Custom modules (future use)\n├── web/\n│   ├── static/           # Static assets (CSS, JS)\n│   │   └── scss/         # SCSS source files\n│   └── templates/        # HTML templates\n│       ├── admin/        # Admin panel templates\n│       ├── api/          # API documentation templates\n│       ├── auth/         # Login/logout templates\n│       ├── errors/       # Error pages (404, 403, 500)\n│       ├── layouts/      # Base layouts\n│       └── partials/     # Reusable components\n├── uploads/              # Media uploads directory\n├── scripts/              # Build scripts\n├── Makefile              # Development commands\n├── package.json          # npm dependencies (htmx, alpine.js)\n└── sqlc.yaml             # sqlc configuration\n```\n\n## REST API\n\nThe CMS provides a RESTful API for programmatic access to content.\n\n### Authentication\n\nAPI requests require a Bearer token in the Authorization header:\n\n```bash\ncurl -H \"Authorization: Bearer your-api-key\" http://localhost:8080/api/v1/pages\n```\n\nCreate API keys in the admin panel under **Settings \u003e API Keys**.\n\n### Endpoints\n\n| Method | Endpoint | Description | Auth |\n|--------|----------|-------------|------|\n| GET | `/api/v1/pages` | List published pages | Optional |\n| GET | `/api/v1/pages/{id}` | Get page by ID | Optional |\n| GET | `/api/v1/pages/slug/{slug}` | Get page by slug | Optional |\n| POST | `/api/v1/pages` | Create page | Required |\n| PUT | `/api/v1/pages/{id}` | Update page | Required |\n| DELETE | `/api/v1/pages/{id}` | Delete page | Required |\n| GET | `/api/v1/media` | List media | Optional |\n| POST | `/api/v1/media` | Upload media | Required |\n| GET | `/api/v1/tags` | List tags | Public |\n| GET | `/api/v1/categories` | List categories (tree) | Public |\n| GET | `/api/v1/docs` | API documentation | Public |\n| GET | `/health` | Health check | Public |\n\n### Response Format\n\n```json\n{\n  \"data\": { ... },\n  \"meta\": {\n    \"total\": 100,\n    \"page\": 1,\n    \"per_page\": 20\n  }\n}\n```\n\n### Error Format\n\n```json\n{\n  \"error\": {\n    \"code\": \"validation_error\",\n    \"message\": \"Validation failed\",\n    \"details\": { \"title\": \"Title is required\" }\n  }\n}\n```\n\n## Theme Development\n\nCore themes (`default`, `developer`) are embedded in the binary. To create a custom theme, add a directory in `custom/themes/`:\n\n```\ncustom/themes/my-theme/\n├── theme.json          # Theme configuration\n├── templates/\n│   ├── layouts/\n│   │   └── base.html   # Base layout\n│   ├── pages/\n│   │   ├── home.html   # Homepage template\n│   │   ├── page.html   # Single page template\n│   │   └── 404.html    # Not found page\n│   └── partials/\n│       ├── header.html\n│       └── footer.html\n└── static/\n    ├── css/\n    └── js/\n```\n\nTo override an embedded theme, create a custom theme with the same name:\n```\ncustom/themes/default/    # Overrides the embedded 'default' theme\n```\n\nCustom themes with the same name as core themes take priority.\n\n### theme.json\n\n```json\n{\n  \"name\": \"My Theme\",\n  \"version\": \"1.0.0\",\n  \"author\": \"Your Name\",\n  \"description\": \"A custom theme\",\n  \"settings\": [\n    {\n      \"key\": \"primary_color\",\n      \"label\": \"Primary Color\",\n      \"type\": \"color\",\n      \"default\": \"#3b82f6\"\n    }\n  ]\n}\n```\n\n## Module Development\n\nCreate custom modules to extend functionality:\n\n```go\npackage mymodule\n\nimport (\n    \"database/sql\"\n    \"embed\"\n\n    \"github.com/olegiv/ocms-go/internal/module\"\n    \"github.com/go-chi/chi/v5\"\n)\n\n//go:embed locales\nvar localesFS embed.FS\n\ntype MyModule struct {\n    module.BaseModule\n}\n\nfunc New() *MyModule {\n    return \u0026MyModule{\n        BaseModule: module.NewBaseModule(\"mymodule\", \"1.0.0\", \"My custom module\"),\n    }\n}\n\nfunc (m *MyModule) RegisterRoutes(r chi.Router) {\n    r.Get(\"/my-endpoint\", m.handleEndpoint)\n}\n\nfunc (m *MyModule) Migrations() []module.Migration {\n    return []module.Migration{\n        {\n            Version:     1,\n            Description: \"Create my_table\",\n            Up: func(db *sql.DB) error {\n                _, err := db.Exec(\"CREATE TABLE my_table (...)\")\n                return err\n            },\n        },\n    }\n}\n\n// TranslationsFS returns embedded locale files for i18n support\nfunc (m *MyModule) TranslationsFS() embed.FS {\n    return localesFS\n}\n```\n\nAdd self-registration in `register.go`:\n\n```go\npackage mymodule\n\nimport \"github.com/olegiv/ocms-go/internal/module\"\n\nfunc init() {\n    module.RegisterCustomModule(New())\n}\n```\n\nThen add a blank import to `custom/modules/imports.go`:\n\n```go\n_ \"github.com/olegiv/ocms-go/custom/modules/mymodule\"\n```\n\nSee `docs/custom-modules.md` for the full guide and `custom/modules/bookmarks/` for a working example.\n\n### Module Translations\n\nModules can embed their own translation files:\n\n```\nmymodule/\n├── module.go\n├── handlers.go\n└── locales/\n    ├── en/\n    │   └── messages.json\n    └── ru/\n        └── messages.json\n```\n\nTranslation keys should be prefixed with the module name (e.g., `mymodule.title`).\n\n### Module Active Status\n\nModules can be enabled/disabled from the admin UI at **Admin \u003e Modules**. When a module is disabled:\n- Public routes return 404\n- Admin routes redirect to the modules list\n- Template functions are not registered\n- The module remains initialized but inactive\n\n## Testing\n\nRun all tests:\n```bash\nOCMS_SESSION_SECRET=test-secret-key-32-bytes-long!! go test ./...\n```\n\nRun tests with verbose output:\n```bash\nOCMS_SESSION_SECRET=test-secret-key-32-bytes-long!! go test -v ./...\n```\n\nRun tests for a specific package:\n```bash\nOCMS_SESSION_SECRET=test-secret-key-32-bytes-long!! go test -v ./internal/store/...\n```\n\nCheck for vulnerabilities:\n```bash\ngovulncheck ./...\n```\n\n## Claude Code Support Tools\n\nThis project uses a shared submodule at `.claude/shared` containing reusable Claude Code extensions:\n\n- **Agents**: security-auditor, project-architect, code-quality-auditor\n- **Commands**: commit-prepare, commit-do, security-audit, setup-project-tools\n- **Global config**: CLAUDE.md rules and settings.json templates\n\n### Updating the Submodule\n\nTo update the shared Claude Code tools to the latest version:\n\n```bash\n# Using the slash command (recommended)\n/update-submodule\n\n# Or manually\ngit submodule update --remote --merge\n```\n\nIf you are using Codex, Claude slash commands are not registered in the\nCodex UI and may show `No commands`. Use shell/make commands directly,\nor ask Codex in chat to run them.\n\nYou can run slash-command equivalents through Claude CLI:\n\n```bash\nclaude -p \"/commit-prepare\" --dangerously-skip-permissions\nclaude -p \"/commit-do\" --dangerously-skip-permissions\nclaude -p \"/code-quality\" --dangerously-skip-permissions\nclaude -p \"/security-audit\" --dangerously-skip-permissions\n```\n\nCodex wrapper commands (Claude proxy by default):\n\n```bash\n./scripts/codex-commands code-quality\n./scripts/codex-commands security-audit\n./scripts/codex-commands commit-prepare\n./scripts/codex-commands commit-do\n\n# Explicit local fallback scripts\n./scripts/codex-commands code-quality-local\n./scripts/codex-commands security-audit-local\n./scripts/codex-commands commit-prepare-local\n./scripts/codex-commands commit-do-local\n```\n\nAfter updating, stage and commit the submodule change if you want to keep it:\n```bash\ngit add .claude/shared\ngit commit -m \"Update Claude Code shared submodule\"\n```\n\n## Technology Stack\n\n- **Backend**: Go 1.26+\n- **Database**: SQLite with [goose](https://github.com/pressly/goose) migrations\n- **SQL**: Type-safe queries with [sqlc](https://sqlc.dev/)\n- **Templates**: [templ](https://templ.guide/) for type-safe HTML\n- **Frontend**: HTMX + Alpine.js\n- **Rich Text Editor**: [TinyMCE](https://www.tiny.cloud/) for content editing\n- **Styling**: Custom SCSS framework\n- **Authentication**: Secure sessions with argon2id password hashing\n- **Containerization**: Docker with multi-stage builds\n\n## License\n\nCopyright (C) 2025-2026 Oleg Ivanchenko\n\nGNU General Public License v3.0 - see [LICENSE](LICENSE) file for details.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Folegiv%2Focms-go","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Folegiv%2Focms-go","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Folegiv%2Focms-go/lists"}