Skip to content

Repository files navigation

Nous

Content management system and blogging platform. Single binary, multiple database backends, static site generation with live admin.

Features

  • Dual database: PostgreSQL and SQLite, auto-detected from DSN
  • Page types: Generic Page, Blog Index, Gallery
  • Posts: Separate content type with categories and tags
  • Per-page sidebar: Each page chooses sidebar mode — None, Categories, Blog, or Attachments
  • Gallery: Dedicated gallery page type with sortable media picker, captions, and multiple layouts (grid, masonry, slideshow)
  • Static publishing: Generates HTML output per page, served by the built-in file server
  • Admin panel: SPA-style JSON API with vanilla JS frontend, drag-drop media uploads
  • Media pipeline: Image upload, dimension extraction, automatic thumbnail generation; MIME type whitelist, configurable upload size and per-user quota
  • Theme system: File-based (no database), Core and Neon stock themes on embedded FS, on-disk themes override embedded
  • Taxonomies: Hierarchical categories and free-form tags
  • Role-based permissions: Administrator, Editor, Author, Contributor — per-site role enforcement on every endpoint
  • Attribution profiles: Personal profiles auto-created per user, group profiles for collaborative authorship; toggle between personal byline and generic attribution per site
  • Page attachments: File download lists with configurable sidebar widget and [attachments] shortcode for inline content
  • Comments: Optional karu-backed threaded comments for pages and posts; collapsed widgets load comments only when expanded
  • Plugin system: Tengo-based scripting (embedded, no external runtime). Editor buttons, content filters, pre/post-publish triggers, admin tool pages, custom page types, sidebar widgets
  • API key management: Create and revoke API keys with secret-once display
  • Import/Export: Full JSON export/import of site content, plus WordPress WXR import (pages, posts, categories, tags)
  • Multi-site: Domain-based site resolution, site switcher in admin panel, admin CRUD for sites
  • Navigation: Customisable navbars and navbar items per site
  • Hotlink protection: Configurable per-site, restricts media embedding to allowed domains
  • Security: Rate limiting (token bucket), IP blocking (DB-backed with expiry), API key authentication, CSRF protection, bcrypt passwords
  • Session management: Cookie-based sessions with sliding expiry and IP validation
  • Notifications: Persistent error toasts with manual dismiss, auto-hiding success toasts
  • Migrations: Versioned per-dialect .sql files, run automatically on startup

Quick Start

# Create an admin user (exits after creation)
nous --create-user admin@example.com:mypassword:admin --db /path/to/data.db

# Start the server (migrations run automatically)
 nous --db /path/to/data.db --secure-cookies=false

# Open http://127.0.0.1:62208/_admin/ and log in

For a running container, use docker exec or podman exec:

docker exec <container> nous --create-user email:password:admin
podman exec <container> nous --create-user email:password:admin

If the database uses a non-default DSN, pass the same --db the container was started with.

In Kubernetes:

kubectl exec <pod> -- nous --create-user email:password:admin

Or as a one-shot Job (needs the same DSN env vars as the deployment):

kubectl create job --image=<image> nous-bootstrap -- nous --create-user email:password:admin

Configuration

All settings can be passed as CLI flags, environment variables, or both.

Flag Env Variable Default Description
--db NOUS_DSN ~/.nous/nous.db Database DSN
--port, -p NOUS_PORT 62208 Listen port
--host NOUS_HOST 127.0.0.1 Listen address
--data NOUS_DATA ~/.nous Data directory
--admin-path NOUS_ADMIN_PATH /_admin/ Admin panel path
--session-ttl NOUS_SESSION_TTL 1440 Session TTL in minutes
--site-name NOUS_SITE_NAME My Site Default site name
--secure-cookies NOUS_SECURE_COOKIES true Set Secure flag on session and CSRF cookies; disable for local plain-HTTP development
--single-site NOUS_SINGLE_SITE false Disable multisite features

Database DSNs starting with postgres:// or postgresql:// use PostgreSQL. Anything else (including empty) uses SQLite.

Example with PostgreSQL

export NOUS_DSN="postgres://user:password@localhost:5432/nous?sslmode=disable"
nous

Global Flags

  • --dev — Development mode (live template reloading, verbose logging)
  • --verbose, -v — Verbose output
  • --version — Print version and exit
  • --create-user, -C — Create user and exit. Format: email:password[:role[:display_name]] Role defaults to contributor. Display name defaults to the email address.

Permissions

Four role levels, enforced per-endpoint:

Role Capabilities
Administrator Full access: users, sites, themes, settings, all content, publishing
Editor Content management: pages, posts, categories, tags, navbars, galleries, API keys, attribution profiles, attachments, publishing
Author Own posts (create, edit, publish own), media upload to own folder
Contributor Own draft posts (cannot publish), media upload to personal folder (image/pdf/markdown/office types only, 10 MB max, 500 MB quota)

Authors and contributors can only edit their own posts. The --create-user flag defaults to contributor role.

Attribution Profiles

Blog posts can show a personal byline or use generic attribution.

  • Personal profiles: Auto-created per user per site when the user first interacts with the system. Profile name is synced with the user's display name.
  • Group profiles: Created by editors in the Admin → Profiles tab. Multiple users can be members, useful for team-authored content.
  • Site setting: blog_attribution can be personal (show profile name) or generic (no byline). Toggle in Admin → Settings → General.
  • Post editor: Shows available profiles based on the user's role. Editors see all profiles; authors/contributors see their own.

Page Attachments

Pages and posts can have file attachments displayed as download links.

  • Sidebar mode: Select "Attachments" in the page editor sidebar settings to show the attachment list as a sidebar widget. Title is configurable per page (defaults to "Downloads").
  • Shortcode: Place [attachments] in the page content to embed the download list inline. Works for all page types including posts.
  • Management: Add/remove attachments in the page editor. Each attachment has a media ID and display title. Order is preserved.

Comments

Comments are disabled by default. Enable them per page or post in the editor; plugin-provided page types can also opt in, while gallery comments are currently controlled at gallery level and per-image comments remain deferred.

The published comment widget is placed above the footer and starts collapsed. It makes its first request only when a visitor expands it, so comments do not affect the initial page load. The widget uses the public /api/comments endpoints and supports threaded replies through karu.

For initial testing, visitors provide a public name and email address. Nous associates the resulting anonymous comment identity with the visitor's IP and uses the identity internally when passing comments to karu. Email addresses are reference data and are not displayed as comment author information.

Plugin System

Nous includes an embedded Tengo (Go-based scripting language) runtime for plugins. Plugins run server-side during content editing and publishing — they are not executed on page hits.

Architecture

Each plugin is a .tengo file placed in {data}/plugins/. The engine compiles plugins to bytecode on startup, caches the compiled result, and clones it for concurrent invocations.

Communication follows a simple globals protocol:

Go sets:   action  (string) — "register" or "invoke"
           args    (map)    — per-invocation context (capability, method, ...)
Plugin sets: result  (any)  — output (html, content, data, ...)

During registration (action = "register"), the plugin sets metadata globals (plugin_name, plugin_version, plugin_description) and capability globals (editor_buttons, filters, triggers, admin_tools, page_types, sidebar_widgets).

All subsequent calls use action = "invoke" with the capability dispatched via args.capability.

Example Plugin

The project ships an example in plugins/tip-box.tengo:

plugin_name       := "Tip Box"
plugin_version    := "1.0.0"
plugin_description := "Inserts a styled tip box into the editor content"
editor_buttons    := [{id: "insert-tip", label: "Tip"}]

if action == "invoke" && args.capability == "editor_buttons" {
    if args.button_id == "insert-tip" {
        body := "<em>Tip</em>"
        if args.selection != "" {
            body = "<strong>Tip:</strong> " + args.selection
        }
        result = {html: '<div class="tip-box">' + body + '</div>'}
    }
}

Place this in {data}/plugins/tip-box.tengo and restart the server. A "Tip" button will appear in the content editor toolbar.

Capabilities

Capability args fields expected result
editor_buttons button_id, selection { html: "..." }
filters name, content, user_role { content: "..." }
triggers name, event, page, user { allow: true/false, error: "..." }
admin_tools method (page|rpc), tool_id { html, js } or { data }
page_types type_id, config, page { content: "..." }
sidebar_widgets widget_id, site_id { html: "..." }

Available Go Modules

Plugins can import these modules using the import() builtin:

Module Functions
db query(sql, params) → [{col: val}]
schema create_table(name, [{name, type}]), drop_table(name), exec(sql)
log info(msg), warn(msg), error(msg)
text replace(s, old, new [, n]), contains(s, sub), has_prefix, has_suffix, trim, lower, upper, split(s, sep), join(items..., sep)
util slugify(s), truncate(s, max), json_encode(v), json_decode(s)

Integration Points

  • Editor buttons: Loaded dynamically into the Quill toolbar when editing pages or posts. Click invokes the plugin and inserts returned HTML at cursor.
  • Pre-publish filters: Run on page content before save. Can sanitise or transform content.
  • Pre-publish triggers: Run before publishing. Can block with an error message.
  • Post-publish triggers: Fire-and-forget after publish succeeds. Useful for webhooks or notifications.
  • Admin tools: Listed on the Plugins admin page. Opens in a modal with a JSON-RPC helper (pluginAdminRPC) for tool-specific UI.
  • Page types: Plugin-defined page types appear as selectable type cards in the page editor. The plugin renders the full page content at publish time.

Import / Export

Import and export are available through the admin panel. The admin API supports full JSON site export/import (pages, categories, tags, settings, navbars, sidebars) and WordPress WXR import (pages, posts, categories, tags).

Database Migrations

Migrations run automatically at startup. They live in internal/database/migrations/ as per-dialect .sql files (001_postgres.sql, 001_sqlite.sql).

Version Changes
v1 Initial schema: all core tables
v2 Role remap (adminadministrator, usercontributor), attribution profiles, profile members
v9 Attribution profiles for existing databases (backfills personal profiles)
v10 Page attachments table, pages.attachments_title column
v11 Plugin messages table
v12 Page metadata (pages.meta)
v13 Per-page and per-gallery comment toggles and anonymous comment identities

The schema_migrations table tracks which versions have been applied.

Architecture

nous
  ├── Config (flags + env vars)
  ├── Database (PostgreSQL or SQLite)
  ├── Session store (database-backed cookie sessions)
  ├── Template engine (embedded + filesystem fallback)
  ├── Publisher (static HTML generation, per-page sidebar composition)
  ├── Media processor (thumbnail generation, MIME/size enforcement)
  ├── Plugin engine (Tengo bytecode VM, module system)
  ├── Exchange (export/import)
  ├── Comment service (Karu threads and anonymous identities)
  └── HTTP server
       ├── Admin API (/_admin/)
       │   ├── Pages, Posts, Media, Categories, Tags
       │   ├── Galleries, Navbars, Sites, Themes, Users
       │   ├── Settings, API Keys, Profiles, Attachments
       │   └── Plugins (invoke, admin RPC)
       ├── Public files (/)
       │   ├── Static files (from published output)
        │   ├── /_search (HTMX)
        │   ├── /_posts (HTMX)
        │   ├── /_media/ (file serving)
        │   └── /api/comments (deferred-load comment threads)
       └── Middleware
            ├── Recovery
            ├── Logging
            ├── CORS
            ├── Security headers
            ├── Rate limiter
            ├── IP blocker
            ├── Session validation
            └── CSRF protection

Data Layout

{data}/
  nous.db                  (SQLite database)
  plugins/                 (Tengo plugin files, *.tengo)
    tip-box.tengo
    ...
  themes/                  (on-disk theme overrides)
    neon/
      theme.json
      ...
  sites/
    {id}/
      public/              (published static files, served at /)
        index.html
        {slug}/index.html
      media/               (uploaded files, served at /_media/)
        {id}-{filename}
        thumb/{id}-{filename}

Theme System

Themes live entirely on the filesystem — no database involved. Stock themes (Core, Neon) are embedded in the binary at build time. On-disk themes under {data}/themes/ take precedence; the admin "fork" button copies a stock theme there for editing. A theme is stock if its name matches an embedded directory; otherwise it is user-created.

API

Full API documentation including all endpoints, authentication, CSRF protection, and rate limiting is in API.md.

Building

Method 1: go build

go build -ldflags="-s -w \
  -X main.version=$(git describe --tags --always 2>/dev/null || echo dev) \
  -X main.commit=$(git rev-parse --short HEAD 2>/dev/null || echo '') \
  -X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo '') \
  -X main.dirty=$(git diff-index --quiet HEAD 2>/dev/null || echo '1')"

Method 2: creo (via fiat build file)

The project includes a fiat build file understood by creo:

Command Description
creo Production build (stripped, version injected)
creo -F Force overwrite existing binary
creo -c Clean build artifacts
creo debug Debug build (no stripping)
creo linux Cross-compile for linux/amd64
creo image Build OCI container image
creo vet Run go vet ./...
creo test Run vet then all tests

Development

nous --dev

Development mode enables:

  • Live template reloading from disk (no restart needed)
  • Verbose logging output

Template overrides can be placed in {data}/templates/ and will take precedence over the embedded defaults.

License

MIT

About

Content management system and blogging platform.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages