# golem15.journal The compiled journal plugin for SummerCMS applications: translatable posts and categories, tags, a settings singleton, and the tables a host migrate creates for a blog. ```go import _ "git.golem15.com/golem15/sm-journal-plugin" ``` Module `git.golem15.com/golem15/sm-journal-plugin`, package `journal`, plugin ID `golem15.journal`. ## Overview The plugin is a compiled SummerCMS 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`. Post and Category store default-locale values on host columns and other locales in `golem15_translate_attributes`. `MorphName` is the PHP class string (`Golem15\Journal\Models\Post`, `Golem15\Journal\Models\Category`); it is never `reflect.Type.String()`. WinterCMS theme components such as `journalPost` and `journalPosts` are a later successor, not implemented here. ## Features - **Posts.** `models.Post` maps `golem15_journal_posts`. Translatable fields are title, content, content_html, excerpt, metadata, and slug (indexed). Fillable does not include `redactor_id`, `content_html`, or `user_id`. Attachments `featured_images` and `content_images` use `system_files` under the same MorphName. YAML admin uses `mltext`/`mlmarkdown`. `classes.FormatHTML` regenerates `content_html` on admin save. - **Categories and tags.** `models.Category` is translatable (name, description, slug). Fillable is name, slug, code, description, parent_id; nest_* columns exist in DDL and are not fillable. `models.Tag` is not translatable. Fillable is name, slug, description. Pivots are `golem15_journal_posts_categories` and `golem15_journal_posts_tags`. - **Settings.** Singleton row ID=1 on `golem15_journal_settings`, not PHP `system_settings`. `search_use_typesense` defaults to false. A fresh save never contacts Typesense. - **Public HTTP.** Anonymous GET `/_journal/api/v1/posts`, `/posts/{slug}`, `/categories`, `/tags`, and `/rss`. Writes under the same prefix require a cabana backend JWT (`aud=backend`). - **Author slug.** Plugin migration adds `backend_users.golem15_bloghub_author_slug`. - **Import and export.** `journal:export-posts` and `journal:import-posts` write and read one JSON file per post. The Posts list toolbar exposes the same actions to administrators with `golem15.journal.access_import_export`. - **Phrasebooks.** Embedded `lang/en/lang.yaml` and `lang/pl/lang.yaml` hold plugin, journal, post, category, and tag UI strings. They are independent of model translation JSON. - **Schema.** Seven gormigrate steps create the six journal tables and the author-slug column. AutoMigrate is not used. Table names are `golem15_journal_*` only. ## Usage Mount the plugin in an application as a git submodule at `plugins/golem15/journal`: ```sh git submodule add git@git.golem15.com:golem15/sm-journal-plugin.git plugins/golem15/journal ``` Add it to the application's `go.work`: ```text use ( . ./plugins/golem15/journal ) ``` Require it in the application's `go.mod` with a local replace: ```text require git.golem15.com/golem15/sm-journal-plugin v0.0.0 replace git.golem15.com/golem15/sm-journal-plugin => ./plugins/golem15/journal ``` List it in `summer.yaml` after translate and run `summer build`: ```yaml module: git.golem15.com/golem15/acme binary: acme plugins: - id: golem15.translate module: git.golem15.com/golem15/sm-translate-plugin - id: golem15.journal module: git.golem15.com/golem15/sm-journal-plugin ``` The plugin's own `go.mod` replaces the framework with `../summercms.go` for a sibling checkout next to `summercms.go`. A host application that mounts the plugin at `plugins/golem15/journal` should replace the framework from the host `go.work`. `Requires()` is exactly `golem15.translate`. A blog frontend lists published posts and fetches one by slug without a token: ```http GET /_journal/api/v1/posts GET /_journal/api/v1/posts/hello-world GET /_journal/api/v1/categories GET /_journal/api/v1/tags GET /_journal/api/v1/rss ``` Editors create a post with a cabana backend JWT (audience `backend`): ```http POST /_journal/api/v1/posts Authorization: Bearer Content-Type: application/json {"title":"Hello","content":"A blog post"} ``` A blog post model already declares translatable fields: ```go func (Post) Translatable() []string { return []string{"title", "content", "content_html", "excerpt", "metadata", "slug"} } func (Post) TranslatableIndexes() []string { return []string{"slug"} } func (Post) MorphName() string { return `Golem15\Journal\Models\Post` } ``` ## API reference | Identifier | Description | |------------|-------------| | `journal.Plugin` | Compiled plugin; `ID` is `golem15.journal`. | | `journal.Plugin.Requires` | Returns `golem15.translate` only. | | `journal.Plugin.LangFS` | Embedded English and Polish UI phrasebooks. | | `journal.Plugin.Migrations` | gormigrate steps registered by this plugin. | | `journal.Plugin.Models` | Models registered by this plugin. | | `journal.Plugin.Commands` | Registers `journal:export-posts` and `journal:import-posts`. | | `journal.Plugin.Routes` | Public GETs and backend-Bearer writes under `/_journal/api/v1`. | | `journal.Plugin.Buckets` | Limiter buckets `journal-public-api` and `journal-api` (Max 120). | | `models.Post` | Post row; `TableName` is `golem15_journal_posts`. | | `models.Post.MorphName` | PHP class string `Golem15\Journal\Models\Post`. | | `models.Post.Fillable` | Allow-list excluding `redactor_id`, `content_html`, and `user_id`. | | `models.Post.AttachRelations` | `featured_images` and `content_images` attachMany. | | `models.Post.SearchableAs` | Index name `golem15_journal_posts`. | | `models.Post.ShouldBeSearchable` | False when unpublished or `search_use_typesense` is off. | | `models.Post.ToSearchableArray` | Typesense document built after a committed save. | | `models.Post.CanEdit` | Owner or `golem15.journal.access_other_posts`. | | `models.Category` | Category row; `TableName` is `golem15_journal_categories`. | | `models.Category.MorphName` | PHP class string `Golem15\Journal\Models\Category`. | | `models.Category.Fillable` | `name`, `slug`, `code`, `description`, `parent_id`. | | `models.Tag` | Tag row; `TableName` is `golem15_journal_tags`. | | `models.Tag.Fillable` | `name`, `slug`, `description`. | | `models.Settings` | Singleton settings row; `TableName` is `golem15_journal_settings`. | | `models.All` | Models registered by this plugin. | | `updates.All` | gormigrate steps registered by this plugin. | ## CLI commands Both commands write JSON files (one per post slug), matching the PHP `journal:export-posts` / `journal:import-posts` tools. They are not a CSV ImportExport framework. A blog application binary registers them through `Plugin.Commands`. | Command | Arguments and flags | Effect | |---------|---------------------|--------| | `journal:export-posts` | `--path` (default `journal-export`), `--dry-run` | Writes one JSON file per post, including non-default-locale translations. Attachments are omitted. | | `journal:import-posts` | `--path` (default `journal-export`), `--force`, `--dry-run` | Creates posts from those files. Existing slugs are skipped unless `--force` updates them. | ```sh ./bin/acme journal:export-posts --path journal-export ./bin/acme journal:import-posts --path journal-export --dry-run ./bin/acme journal:import-posts --path journal-export --force ``` ## Dependencies - SummerCMS modules: backpack, beachcomber, bonfire, cabana, lagoon/attach, pact, party, surf. - Compiled plugin: `golem15.translate`. - Third-party: `gorm.io/gorm`, `github.com/go-gormigrate/gormigrate/v2`, `github.com/yuin/goldmark`, `gocloud.dev`. Test-only: `github.com/testcontainers/testcontainers-go`. ## Testing ```sh go test ./... ``` Postgres integration smokes start Docker unless `-short`. Missing Docker is a failure, not a skip, unless `-short`.