- TestFaqContentDecode, TestFaqModel and TestFaqSerialize (Docker-free): content as list, JSON text or row-keyed object, required export fields, the model contract, output sanitizing, null description, question text and the slug shape - TestFaqAPI and TestFaqImport (Postgres): active duplicate behind an inactive lower id, default locale per is_default, a disabled default, malformed slugs, keyed content order, malformed rows aborting before any write
golem15.faq
The compiled FAQ plugin for SummerCMS applications: question and answer lists per slug and locale, edited in the admin with sanitized rich text and served to frontends as JSON.
import _ "git.golem15.com/golem15/sm-faq-plugin"
Module git.golem15.com/golem15/sm-faq-plugin, package faq, plugin ID golem15.faq.
Overview
The plugin is a compiled SummerCMS plugin, a port of the WinterCMS Golem15.FAQ plugin. Its init registers it with the framework, so an application loads it by listing it in summer.yaml; summer build then emits the blank import above into the application's plugins.gen.go.
It requires golem15.translate: every FAQ belongs to one translate locale through locale_id, and a site keeps one row per locale under the same slug, as the PHP plugin did. MorphName is the PHP class string Golem15\FAQ\Models\FAQ.
Features
- Table.
golem15_faq_faqskeeps the PHP columns: id, name, slug, locale_id (NOT NULL), is_active (default false), description (text), content (text holding the JSON list of{question, answer}rows) and timestamps, with a non-unique index on (slug, locale_id). One gormigrate step creates it; AutoMigrate is not used. - Model.
models.FAQstorescontentaslagoon.Jsonable(models.Content) and belongs to the translateLocale. Fillable: name, slug, locale_id, is_active, description, content. A name, a slug and a locale are required. - Admin. A top-level FAQ item (icon
circle-question-mark, order 500) with the side item FAQs, behind the permissiongolem15.faq.faqs(developer and publisher roles). The list shows id, name, slug, locale (by name, so rows of the same slug are told apart), active and the timestamps. The form has name, slug (preset from name), locale (relation, by name), is_active, description (rich editor) and content (a repeater, "Add question", each row a required question and a rich-editor answer). Rich text is sanitized on save by the framework's policy (seaglass), and the save answers with a notice naming each field it cleaned, such ascontent.1.answer. - Public API.
GET /_faq/api/v1/faqs/{slug}serves an active FAQ in one locale. See Usage. - Import.
faq:import-wintercopies a WinterCMS site's FAQs from the JSON Linesscripts/winter-export.sqlproduces. See CLI commands. - Phrasebooks.
lang/en/lang.yamlandlang/pl/lang.yamlkeep the PHP key names.
Usage
Public FAQ API
GET /_faq/api/v1/faqs/{slug}?locale=en
Anonymous; the route group never attaches the backend middleware. slug is lowercase letters, digits and dashes. locale is the code of an enabled translate locale; without it the default locale is used.
{
"data": {
"name": "Shipping",
"slug": "shipping",
"locale": "en",
"description": "<p>Everything about shipping.</p>",
"content": [
{ "question": "How long does shipping take?", "answer": "<p>Two working days.</p>" },
{ "question": "Where do you ship?", "answer": "<p>Everywhere in the EU.</p>" }
]
}
}
Rules:
- The row served is the active FAQ with that slug and locale; when several rows share them, the one with the lowest id.
description(a string ornull) and everyanswerare HTML passed throughseaglass.Sanitizeon output, so rows that never went through the admin save (imports, SQL copies) are served clean too.questionis a plain string.contentis always an array.- An unknown slug, an inactive row, a malformed slug and an unknown or disabled locale are
404 {"error":"FAQ not found"}.
Throttling and CORS
The API is throttled by the surf bucket faq-public-api (faq.BucketPublicAPI): 120 requests a minute per client IP, read behind the host's trusted proxies. The 429 body is surf's {"message":"Too Many Attempts."}.
The plugin sends no CORS headers. A host whose frontend runs on another origin adds the path to http.cors.paths in config/http.yaml:
cors:
paths: ["_faq/api/*"]
allowed_origins: ["https://www.example.com"]
allowed_methods: ["GET"]
Winter import
-
On the WinterCMS database, check the FAQ and translate locales table names named in the header of
scripts/winter-export.sql, then run the read-only export:mysql -N -B --raw <database> < scripts/winter-export.sql > faqs.jsonl -
In the application, after every locale code the export names exists in the translate locales:
./bin/acme faq:import-winter --file faqs.jsonl --dry-run ./bin/acme faq:import-winter --file faqs.jsonl
API reference
| Identifier | Description |
|---|---|
faq.Plugin |
The golem15.faq plugin: migrations, models, phrasebooks, admin controller, permission, navigation, the faq:import-winter command, the public route and its throttle bucket. |
faq.BucketPublicAPI |
faq-public-api, the throttle bucket of the public API. |
models.FAQ |
A FAQ on golem15_faq_faqs; Items returns its stored rows. |
models.Content |
The stored question and answer list. |
api.Show |
The handler of GET /_faq/api/v1/faqs/{slug}. |
api.FAQ, api.Question |
The data object of the public API and one of its rows. |
console.ImportWinter |
The faq:import-winter command. |
console.ImportWinterFile |
The import itself, for tests and other callers (console.WinterOptions, console.WinterReport). |
controllers.FAQsControllerID |
The admin controller id golem15.faq.faqs. |
controllers.PermissionFAQs |
The permission golem15.faq.faqs. |
CLI commands
faq:import-winter --file FILE [--force] [--dry-run]
- Reads one JSON object per line (
id,name,slug,locale_code,is_active,description,content,created_at,updated_at);contentmay be the PHP JSON string or an array. locale_idis the id of the translate locale withlocale_code; an unknown code aborts the import before anything is written, naming the code.- Ids are kept and the id sequence is moved past them with
setval. One transaction. - A table that already holds rows is refused unless
--force, which replaces them in the same transaction.--dry-runprints the report and writes nothing. An empty file writes nothing and reports zero. - HTML is copied as stored; the API sanitizes it on output.
Winter adaptations
- Form: the PHP
span: noneandcssClass: col-sm-4layout becomes left and right spans in one flat form; the locale relation'sselect: nameisnameFrom: name; a question is required in each content row. - List: a locale column is added, since several rows share a slug.
- Saves: a FAQ without a locale is refused with 422
errors.locale_required(the locale relation is written after the model rules run, so the check follows the write and rolls it back). - API: the public JSON API replaces the PHP component; rich text is sanitized on save and again on output.
- Timestamps are
TIMESTAMPTZ.
Dependencies
- SummerCMS modules: backpack, bonfire, cabana, lagoon, pact, party, seaglass, surf, wire.
- Compiled plugin:
golem15.translate(git.golem15.com/golem15/sm-translate-plugin). - Third-party:
gorm.io/gorm,github.com/go-gormigrate/gormigrate/v2. Test-only:github.com/testcontainers/testcontainers-gowith its Postgres module,github.com/jackc/pgx/v5.
Testing
go vet ./...
go test -short ./... # no Docker
go test ./... # TestFaqAdminSmoke, TestFaqImportSmoke, TestFaqAPISmoke on a Postgres container