Skip to content

grimdork/nous

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
  • 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

# 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 false Set Secure flag on session and CSRF cookies
--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.

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

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)
  └── 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)
       └── 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

License

Stars

1 star

Watchers

1 watching

Forks

Contributors