Features#
This is the canonical catalog of Lesstruct’s product features. The homepage
and README.md curate subsets from this page; when feature wording changes here,
update those surfaces too. Each feature links to the deeper reference where one
exists (configuration.md, plugin-development.md, api-reference.md, etc.).
Screenshots are captured from a running demo by
make screenshotsand served in both light and dark themes. A referenced-but-missing screenshot fails the docs build on purpose, so the catalog cannot ship a broken image by accident.
Deploy & run#
- One binary, no Docker required. A single static Go binary (
CGO_ENABLED=0) runs the whole CMS. SQLite is built in. No runtime, no container, nonode_modulesin production. Containerizing is fully supported if you prefer it —FROM scratchworks. - Static site generation (SSG). Generate a fully static HTML site (tar.gz) with
AMP variants for content pages, listing pages, author pages, tag pages, a
sitemap, and robots.txt — all rendered from the same data layer as the live site.
Download from the admin panel under Export, via the agent API, or via
lesstruct-cli ssg. - Multi-database. Embedded SQLite is the default; PostgreSQL and MySQL are
first-class via
DB_DRIVER. Schema migrations run automatically on first start, per driver. - Two-layer configuration.
config.tomlholds your content schema (languages, post types, fields, thumbnails);.envholds deployment state (host, port, DB, secrets, SMTP, AI keys). OnlyJWT_SECRETis required; everything else has a sensible default. See configuration.md. - Plugin hot-reload. In
DEV_MODE, a file watcher reloads.wasmplugins without a server restart. - Headless mode.
[headless] enabled = trueinconfig.tomlturns the instance into a pure headless CMS: the server-rendered content site (catch-all and/static/*) is not served, only the admin panel and the REST API. The sitemap and robots.txt follow suit. See configuration.md. - Comments can be switched off entirely.
[comments] enabled = falsehard-disables the comment system end to end — no comment routes, no comment UI,allowCommentsforced off on every item, and self-registration blocked (the only self-registerable role, Commentator, exists solely for comments). See configuration.md.
Content & authoring#

Custom post types, built in. Define post types in
config.toml— no plugin, no library. The admin list, form, storage, and queries all read from that file. Built-in slugs (post,page,media,comment) extend instead of collide.Admin content list with lazy loading. The admin list pages through every item server-side (
limit/offset+ a total count) instead of capping at the first page, so sites with thousands of articles stay fully browsable. Scrolling loads the next page automatically, and a total-count badge on the page header shows how many items the current filter matches.Custom fields, built in. Add typed fields to any post type (and to user profiles) in
config.toml. The admin form renders them, the service validates them, and they are queryable. No code required.TipTap rich-text editor. Tables, math (KaTeX), syntax-highlighted code blocks, emoji, YouTube embeds, links, images, and text alignment — all first-class.
Draft and publish. A two-state workflow (
draft,published) with publish/unpublish actions exposed in the admin and the CLI.Soft-delete and restore. Deleted content is recoverable from the admin trash view.
Per-content SEO. Meta description, OpenGraph title and description, and a live preview — collapsible inside the editor. SEO metadata generation is format-aware: TipTap content uses JSON extraction, HTML content uses tag-stripping — so HTML-format imports (e.g. WordPress Elementor pages) get real extracted text in their meta descriptions, not empty strings.
Immutable URL slugs. The slug is the public URL (
/<slug>). Any authenticated user can set a custom slug when creating content (in the editor or via--slugonlesstruct-cli content create); otherwise the slug is auto-generated from the title. Once saved, the slug is locked for everyone — editing the title never regenerates it, so published URLs stay stable for SEO and inbound links. Uniqueness is enforced per language; a collision returns a clear error rather than silently suffixing.HTML/CSS content authoring. Set
format: htmlon create to author raw HTML directly — stored and served as-is, no TipTap conversion. The admin editor provides a CodeMirror 6 editor with syntax highlighting and a live preview (sandboxed iframe). HTML content is sanitized on write (dangerous elements/attributes stripped via bluemonday; inlinestyleand class attributes preserved) and on read (rendered through the same policy). Ideal for WordPress Elementor imports and hand-authored HTML pages.Markdown as first-class ingest. The CLI and
/api/v1accept Markdown bodies; the server converts them to canonical TipTap JSON. Raw Markdown is never persisted.WordPress importer (async). Upload a WordPress WXR export to migrate posts, pages, custom post types (and their custom fields), media, and authors into Lesstruct. The import runs in a background goroutine and returns
202 Acceptedimmediately with ajobId; pollGET /api/admin/wordpress/import/status/{jobId}to track progress. Authors are auto-created as Contributor users and their posts are assigned to them. Custom post types and field schemas are read fromconfig.toml; items whose post type is not registered are silently skipped. Featured images (_thumbnail_id) are resolved from attachment items, downloaded, transcoded to WebP, and prepended to each post’s content body; inline body images are likewise downloaded and remapped (downloading is concurrent with a bounded worker pool; transient errors are retried with backoff). Failed downloads fall back to hotlinking the original WordPress URL. Elementor-built pages are imported asformat=htmlusing the rendered HTML from the Elementor cache, preserving their original layout.- Skip-media option. When importing, you can optionally skip downloading
media (inline images and featured images). Content imports with original
WordPress URLs hotlinked — useful for fast imports when you already have
the media or want to defer downloading. Available in the admin UI
(checkbox) and the CLI (
--skip-mediaflag). - CLI import. The
lesstruct-cli import wordpress --file <path> [--skip-media]command imports the same WXR file via the/api/v1agent-realm endpoint using an API key. It uploads the file, then polls the status every few seconds, printing live progress until the job completes.
- Skip-media option. When importing, you can optionally skip downloading
media (inline images and featured images). Content imports with original
WordPress URLs hotlinked — useful for fast imports when you already have
the media or want to defer downloading. Available in the admin UI
(checkbox) and the CLI (
Hugo importer. Import a Hugo site to migrate posts (HTML or Markdown with YAML frontmatter) into Lesstruct. The import runs asynchronously as a background job (202 Accepted + job ID) with a status endpoint for progress tracking — identical to the WordPress importer. Hugo
{{< highlight >}}shortcodes are converted to<pre><code>blocks and{{< iframe >}}shortcodes to<iframe>elements. YAML frontmatter fields (title,date,tags,description,url,aliases,draft,language,hasMath, etc.) are mapped to their Lesstruct equivalents — includingdate, which becomes the content’s publish date instead of the import time. Old.htmlURLs are stored as aliases in thecontent_aliasestable so the public content site can issue 301 redirects from the old path to the new clean slug. Bilingual en/id pairs are linked as translations when they share a directory.- Media migration. Images referenced by the content (local
static/files and remotehttps://URLs) are downloaded and re-uploaded as Lesstruct media (WebP transcode + SHA-256 dedup); body<img src>paths are rewritten to the new media URLs and the first frontmatterimages:entry is prepended as a featured image. Use the skip-media option to import text only with images hotlinked — available in the admin UI (checkbox) and the CLI (--skip-mediaflag). - Idempotent re-runs. Items whose slug already exists are skipped as “already imported”, so re-running after a partial failure is safe.
- Admin UI. Upload a
.tar.gzarchive (containing at least acontent/directory) under Import → Hugo; the UI polls the job status and shows a progress bar plus per-item issues. The site’sconfig.toml/hugo.tomlis read forbaseURLanddefaultContentLanguage. - CLI import.
lesstruct-cli import hugo --source <path> [--skip-media]accepts either a Hugo project directory (auto-archived: onlycontent/andstatic/are sent) or an existing.tar.gzarchive, and uploads it via the/api/v1/hugo/importagent-realm endpoint using an API key, then polls the job status until it completes. - Known limitations. Hugo sections are imported as
posttype (custom sections are not yet mapped to post types, and_index.mdsection landing pages are not skipped); directory-based i18n (content/en/+content/id/) is not yet detected — only the.id.htmlfilename suffix andlanguage:frontmatter are recognized; Disqus comments are not migrated. See project-context.md for the architecture and api-reference.md for the endpoint.
- Media migration. Images referenced by the content (local
Content export. Download all content as a Hugo-compatible
tar.gzarchive (lesstruct-export-<timestamp>.tar.gz). Each content item becomes a<postType>/<slug>.<language>.htmlfile with YAML frontmatter (title,date,description,tags,url,language,aliases,draft,lastmod, custom fields). TipTip body content is rendered to HTML before export. Referenced media files are bundled understatic/uploads/media/with the HTMLsrcattributes unchanged (Hugo servesstatic/at site root, matching Lesstruct’s own/uploads/media/path). Available from the admin panel under Export, via the admin API (/api/admin/export), and via CLI (lesstruct-cli export --output-dir <dir>).
Media & images#

- Media library. Browse, search, and date-filter uploads from the admin panel.
- Automatic WebP conversion. Every uploaded image is transcoded to WebP (quality 80) on upload, so images never weigh down your content.
- Configurable thumbnail variants. Defaults ship
_thumb(370px),_medium(800px),_large(1600px); all editable inconfig.toml. The content site emits a responsivesrcsetfrom them. Post cards also expose anImageVariantsmap (keyed by configured suffix) and anOriginalURLfield for the unscaled original, enabling hero backgrounds and high-DPI layouts. - SHA-256 dedup. Identical uploads are detected and rejected (with a force-upload escape hatch).
- AI image generation. Generate images from the media library and the content editor via Google Imagen, Gemini, or GPT-Image. Bring your own key.
Internationalization#
- Multilingual by default. Declare your languages in
config.toml(e.g.languages = ["en", "id"]). Content carries aLanguageand authors link translations into translation groups. - Translation-aware SEO. The sitemap declares
hreflangalternates from translation groups. - Localized date formatting. Post dates follow each content item’s
Languagefield: Indonesian content renders1 Januari 2026, English content rendersJanuary 2, 2006. Custom-field dates localize automatically. - AI translation. Translate content between your configured languages from the editor.
AI#
- Opt-in, bring-your-own-key. Text via any OpenAI-compatible endpoint
(
AI_TEXT_GENERATION_BASE_URL); images via Google or OpenAI. Nothing runs without your keys;/api/healthhonestly reports which features are enabled. - Text enhancement and translation. Refine or translate rich-text (TipTap) post bodies from the editor.
- AI-powered HTML/CSS authoring. Describe what you want in plain language — the AI generates production-ready HTML & CSS with semantic markup, responsive layouts, and accessible design. Output is on-brand by default: the AI reuses your active theme’s design tokens (
var(--color-primary), spacing, radius, fonts) and component classes rather than inventing arbitrary colors and fonts. Includes 9 quick-start presets (hero, pricing, testimonials, features, CTA, FAQ, stats, contact, newsletter) and iterative refinement — the AI can modify existing HTML based on your follow-up instructions. Your media library images are automatically surfaced as context. This replaces the need for a drag-and-drop page builder: describe, generate, refine, ship. - Image generation. Generate images from the media library and the editor.
- Built for agents.
lesstruct-cliis a thin Cobra client over/api/v1designed for AI agents and terminal-first humans. Markdown ingest, cursor pagination, and a standard response envelope make it easy to script. - Agent skills. Lesstruct ships installable skills for theme and plugin
development that work from your installed site (no source tree needed).
Install them with
npx skills add aristorinjuang/lesstruct— works with Claude Code, OpenCode, Cursor, Codex, and 25+ other agents. - Crawlable docs. This site publishes
/llms.txt(page index),/llms-full.txt(every page concatenated), and a per-page Markdown mirror for retrieval pipelines.
Themes & rendering#
- Server-rendered by default. The content site is rendered server-side with
Go
html/template— fast and SEO-friendly. - One default theme. Lesstruct ships a single embedded default theme that is the working starting point; it does not generate a new theme per release cycle.
- Customizable. Point
THEME_DIRat athemes/<name>/directory to override CSS, JS, and HTML templates. The contract (CSS variables, layout blocks, JS DOM ids, CDN assets) is documented so fork-and-modify is safe. See theme-development.md. - Per-post-type templates. Each post type gets its own content template
(e.g.
page.html,event.html), falling back topost.html, then the embedded default. Theme authors can ship apage.htmlwithout blog chrome (related posts, author box, date metadata) while keeping the full layout for blog posts — no config changes needed. - Per-slug template overrides. A theme can ship a template that applies
to one specific content row by naming the file
<postType>-<slug>.html(e.g.page-about.html,article-spotlight.html). Mirrors the WordPresspage-{slug}.phpconvention but generalizes to every post type. Pure additive fallback — no existing theme breaks. See theme-development.md. - Multi-type aware. Every post card and single-page template receives the
item’s
.PostType, so a theme can branch layouts for articles, events, and any custom post type from one template set. - Magazine homepages. Optional
[[homepage_section]]blocks inconfig.tomlrender per-post-type groupings (latest articles, upcoming events, …) alongside the latest-posts list — backward compatible (omitted = flat list). Each section supportsoffsetfor non-overlapping content (e.g. “Featured” items 1–6, “Recommendations” items 7–26). The homepage renders via a dedicatedhomepage.htmltemplate (distinct fromindex.htmlused by listing pages). - Content archive API.
GET /api/v1/public/archivereturns year/month counts for building archive widgets; listing pages accept?year=/?month=for date filtering. - Site identity from config. An optional
[site_config]block inconfig.tomlsets the sitenameand an optionallogo, which drive the browser-tab title suffix,og:site_name, the default logo text, and the footer. This is the one branding surface a theme override cannot reach (the name is baked into handler-sidePageTitlestrings); everything else (social links, analytics, custom<head>, image/multi-logo layouts) stays aTHEME_DIRtheme concern. - Paginated listings. Homepage, author, tag, and post-type listing pages
accept
?page=Nand expose prev/next state to templates. Page size is tuned viaPOSTS_PER_PAGE. - SEO built in.
sitemap.xml,robots.txt, JSON sitemap, andhreflangare generated for you.
Extensibility#
- WebAssembly plugins. Drop a compiled
.wasmintoplugins/and it hooks into the content lifecycle. Any language that compiles to Wasm works. - Familiar hook model. Explicit registration, priority-based execution,
immutable data flow. Invoked hooks:
before_save,after_create,after_publish; reserved for forward compatibility:on_plugin_loaded,before_delete. - Host functions. Plugins call into the host for HTTP (
http_get,http_post), the database (db_query,db_exec), and logging (log_info,log_error). - Sandboxed. Each plugin declares a capability manifest (memory ceiling, allowed HTTP URL patterns, DB permissions) and runs under a per-call timeout.
- See plugin-development.md and plugin-capabilities.md.
API & automation#

- Versioned REST API.
/api/v1covers Content, Media, and Comments. See api-reference.md. - Standard response envelope.
{"data": ..., "error": {...}, "meta": {...}}, with bare-array lists and cursor pagination on list endpoints. - API keys. Personal
lesstruct_<keyID>_<secret>Bearer tokens, scoped to the creating user, with revoke and expiry. Created from the admin profile. lesstruct-cli. A Cobra client for the same API —content,media,comment, andconfigsubcommands;--output text|json; auth via--api-key, env, or config file.- Public content & author APIs. Unauthenticated
/api/v1/public/*endpoints serve rendered content, search, post types, and a published-authors listing (/v1/public/authors) for author directories and “most active contributors” widgets — only safe fields, never email/role/custom-fields. - Public custom-field filter & sort.
/api/v1/public/content_itemsand/api/v1/public/authorsacceptcf_<field>,cf_<field>_min,cf_<field>_max, andsort_by=cf:<field>&order=asc|descso theme authors can build dynamic regions (recent-posts grids, “top N by ranking” sidebars, scoped directories) client-side without server-side queries. Each publicly-queryable field must be opted in via a[[public_field]]block inconfig.toml— the default is fail-closed (400field_not_queryable) so sensitive fields are never accidentally exposed. Fields allowlisted with the"expose"operation are also included in the response body (publicFieldsmap on the authors endpoint), enabling client-side rendering of points, ranks, badges, and other live values. See api-reference.md and configuration.md.
Users, roles & security#
- Three roles. Admin, Contributor, and Commentator — enforced by dedicated middleware on each realm.
- First-run setup. A default
admin/adminaccount is auto-created on first start; the first login forces a password change. Self-registration createspendingCommentators an admin approves. - User management. Admins CRUD users, assign roles, suspend/unsuspend, soft-delete, and moderate the registration queue (approve / reject / mark-as-spam).
- Profiles. Self-service profile (name, email, password, custom profile fields, avatar), self-service data export, and self-service account deletion.
- JWT auth (admin realm). Bearer-JWT sessions for the admin SPA, with Argon2id password hashing and transparent rehash-on-login for legacy bcrypt hashes.
- Failed-login lockout. An account locks for 15 minutes after 3 failed attempts, with an email notification.
- Email verification and password reset. Self-registration verifies via email token; forgot-password / reset-password flows are built in.
- Rate limiting. Separate per-minute limits for auth, API, and public realms; per-key limiting on the agent API.
- CSRF and security headers. CSRF token validation plus CSP,
X-Frame-Options,X-Content-Type-Options, andReferrer-Policy. The CSP is configurable fromconfig.toml— operators append sources per directive, switch to Report-Only for safe rollout, add new directives, or override entirely (or disable when behind a CDN that manages its own CSP). Built-in defaults includeyoutube-nocookie.com(privacy-enhanced YouTube) alongside the existingunsafe-inline/ known CDN hosts.
Engagement#

- Comments with moderation. Per-content comments with a moderation queue
(
pending/approved/rejected/spam) and a per-content allow/deny toggle. - Public search. An on-site search box backed by
/api/v1/public/search. - Dashboard. Published/draft counts, users, pending registrations, media stats, recent content, and a per-post-type content breakdown (each card links to that type’s filtered list) in one view.
Missing something, or a feature reads stronger than it should? Features are kept honest against the source tree — open an issue or PR. For the architecture behind these features, read project-context.md.