diff --git a/README.md b/README.md
index 8beaffa..88c8626 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ WinterCMS theme components such as `journalPost` and `journalPosts` are a later
## 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`.
+- **Categories and tags.** `models.Category` is translatable (name, description, slug). It also stores `content`, an HTML host column that is not translatable, edited in the admin as a textarea. Fillable is name, slug, code, description, content and 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`, `/rss`, `/authors/{author}`, and `/authors/{author}/posts`. Writes under the same prefix require a cabana backend JWT (`aud=backend`).
- **Author slug.** Plugin migration adds `backend_users.golem15_bloghub_author_slug`. The column stores the public author slug that the author routes resolve.
@@ -86,6 +86,8 @@ GET /_journal/api/v1/authors/jane-doe/posts?page=2
`content` is the category's HTML body for a category landing page. It is stored and returned as is: it is not markdown and the API does not render, trim or sanitize it. It is `null` when unset, and it is the same in every locale, because it is a host column and not translatable. A consumer that renders it as HTML must apply its own gate or sanitizer first.
+The admin Categories form refuses content that fails `classes.CheckHTML` with a `422` on `content`, and stores blank content as `null`. Rows written by SQL or by import commands bypass that gate, so consumers keep their own gate.
+
```json
{"data":[{"id":4,"name":"Acme news","slug":"news","description":null,"content":"
Acme news
Pillar copy
","parent_id":null,"nest_depth":0,"post_count":3}]}
```
@@ -169,12 +171,13 @@ func (Post) MorphName() string { return `Golem15\Journal\Models\Post
| `models.Post.CanEdit` | Owner or `golem15.journal.access_other_posts`. |
| `models.Category` | Category row; `TableName` is `golem15_journal_categories`; `Content` holds the HTML body served as `content`. |
| `models.Category.MorphName` | PHP class string `Golem15\Journal\Models\Category`. |
-| `models.Category.Fillable` | `name`, `slug`, `code`, `description`, `parent_id`. |
+| `models.Category.Fillable` | `name`, `slug`, `code`, `description`, `content`, `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. |
+| `classes.CheckHTML` | The `FormatHTML` safety gate for HTML stored as is, such as category content. |
## CLI commands
diff --git a/classes/check_html_test.go b/classes/check_html_test.go
new file mode 100644
index 0000000..cc6f2fd
--- /dev/null
+++ b/classes/check_html_test.go
@@ -0,0 +1,34 @@
+package classes
+
+import "testing"
+
+func TestCheckHTML(t *testing.T) {
+ cases := []struct {
+ name string
+ in string
+ ok bool
+ }{
+ {"empty", "", true},
+ {"heading and link", `