POST /_journal/api/v1/images/alt writes the alt of a markdown post into
its source: the inline image  outside code
(classes.SetMarkdownImageAlt) or a raw img outside code
(classes.SetImageAltOutsideCode), so the next content save keeps it. An
image the markdown does not hold that way is refused with 422
changes.N.src and nothing is written.
The command opens the database with lookupDB, as journal:image-variants
does: without a published database it opens database.dsn, and without
a DSN it names SUMMER_DATABASE__DSN.
ConvertContentH1 rewrites content only when it is HTML; a markdown
source, its code samples included, is kept byte for byte and only its
rendered content_html is converted, on API saves, imports,
journal:fix-headings and translations. ConvertH1 and SetImageAlt keep
the bytes of an unfinished tag at the end of the input.
A width whose variant would be taller than the thumbnailer's 4096 px
edge limit is skipped with a warning and counted, like a width wider
than the original, so the job and journal:image-variants go on with the
next widths and images.
- outline: facts from the editor and the Source tab, skipped, empty and
link-only headings, the H1 flag only in the Source tab while level 1 is
not offered, generic texts, the 300 ms debounce without edits
- link form: rel follows the URL as typed, only checked tokens written,
none for internal links, the generic warning never blocks
- image form: the slug rule examples, decorative clears and locks alt
- ConvertContentH1 and ConvertTranslationH1 for post bodies and category
pillar text
- SetImageAlt with several matches, repeated alt attributes and markup
that only looks like an img
- the content facts outline, images, decorative images and embeds
- the related-posts mode and count
- the generated CLI publishes no database or uploads bucket for plugin
commands, so the command stopped with database is not configured
- without a published database it opens database.dsn and publishes it, and
without a published bucket it opens storage.uploads.bucket_url, closing
what it opened
- golem15_journal_image_variants (migration 202610280002): a thumbnail
per width of golem15.journal.image_widths (480, 800, 1200 by default,
never wider than the original) and its WebP copy, with the original's
size
- conga job golem15.journal.image_variants (three attempts), queued after
a featured image upload and after an admin save of a post whose images
lack a width; the upload request never encodes
- journal:image-variants [--all] backfills
- featured_images[].variants {width, height, url, webp_url, source_width,
source_height} on the post answers (additive key)
- golem15.journal.related_mode (category | scored, default category) and
golem15.journal.related_count (3 to 6, default 3, clamped) in
config/config.yaml; classes.ReadRelatedSettings reads them
- the default mode answers GET /posts/{slug} exactly as before
- scored: related posts ranked by 2 points per shared tag plus 1 per
shared category, ties by newest, in one scoring query
(classes.ScoredRelatedPosts), published only for visitors; the answer
adds previous_in_category and next_in_category, the neighbours by
publish time within the post's first category by name
(classes.CategoryNeighbour), in the direction of previous_post and
next_post
- TestRelatedDefaultUnchanged, TestRelatedScored (a draft and a future
post sharing every tag never shown) and TestRelatedScoredCountClamped
- the H1 setting, its precedence, the write paths, journal:fix-headings,
the settings and image alt routes and the API flag
- the editor's heading levels, outline, link rel and image naming, the
rel rule of the HTML policy and MediaFileName
- journal-author, the content facts, author_pages and the four new MCP
tools
- the blogeditor and mlblogeditor types serve headingLevels and
genericAnchors through RequestOptions, per schema request
- J1: H2 to H4 with a pasted or loaded H1 parsed as H2 while the H1
setting is on, H1 to H4 while it is off; H5 and H6 still parse
- J2: the outline bar and panel (skipped, empty, link-only, H1 in Source,
generic link texts), 300 ms after a change, collapsed, read-only
- J3: nofollow/sponsored/ugc checkboxes for absolute http(s) links only,
and a non-blocking warning for a generic link text
- J4: one form per picked file with the slugged file name, alt required
unless decorative (alt=""); Insert uploads the renamed file and the
image is inserted on the upload result
- assets/dist rebuilt
- the HTML policy keeps rel on a when it holds only nofollow, sponsored
and ugc tokens (D-83); any other rel value is removed
- classes.MediaFileName slugs uploaded image names (lowercase, Polish
letters transliterated, hyphens, at most 80 characters, image when
empty) for the editor upload and POST /media/upload (D-85)
- the editor corpus gains an external link with rel and a decorative
image, pinned by TestEditorOutputPassesPolicy
- journal-author (/blog/author/:author, :author the author slug else the
login): references, Resolve, Records of the authors with a published
post, Describe as a profile per locale, exact Match on the pattern
- golem15.journal.author_pages false resolves nothing, describes empty
URLs and never matches
- posts describe PublishedAt, their categories by name as Parents, the
author (a journal-author reference while author pages are on), the
featured and body images, YouTube embeds, the heading outline and
TopHeading (2 while the H1 setting is on, else 0)
- categories describe their parent, the pillar text's outline and
TopHeading; kinds article/collection and the journal_ fixing tools
- golem15.journal.author_pages (AuthorPagesKey, default true)
- POST /_journal/api/v1/images/alt {dry_run, changes} sets alt on the
matching img of default or translated content through the tokenizer,
all or nothing (422), with a RecordSaved per changed post
- MCP journal_get_settings, journal_update_settings, journal_fix_headings
(manage_settings) and journal_set_image_alt (access_posts, destructive)
- journal:fix-headings help names its output and the refusal
- classes.FixStoredHeadings converts stored H1 in post content (every
locale) and category pillar text, in one transaction with a RecordSaved
and a site cache purge per changed record
- journal:fix-headings, a dry run unless --apply, refuses with exit code 1
while the H1 setting is off
- GET/PUT /_journal/api/v1/settings and POST /fix-headings {dry_run} for
golem15.journal.manage_settings; 409 with the refusal while the setting
is off
- the /_journal/api/v1 post writes convert default and translated content
in the write transaction and answer an additive notices key
- journal:import-posts converts and prints the converted count
(ImportFromDirConfig, ImportStats.H1Converted)
- a category's pillar text saved in the admin answers
editor.h1_converted_category
- every GET /categories row reports content_h1_to_h2 (additive key)
- content_h1_to_h2 and generic_anchors on the settings row (migration
202610280001), Headings and links tab, config key
golem15.journal.content_h1_to_h2 used when no settings row exists
- classes.ConvertH1 on the x/net/html tokenizer; the Blog Editor types
convert while the setting is on and answer editor.h1_converted
- GET /posts/{slug} reports content_h1_to_h2 (additive key)
- Engine search asks Typesense for the first 250 candidates, re-gates them
in SQL, counts the gated rows and pages over them in rank order; pages
after the first are no longer empty. An explicit sort overrides the rank.
- The settings row gates engine search, so it works straight after boot.
- ?fields=suggest serves id, title, slug, a plain-text excerpt,
featured_image_url and published_at.
- Content is indexed as plain text; Post declares SearchIndexSchema.
- journal:reindex [--fresh] syncs every post into the index.
- plugin.yaml generated by summer plugin:manifest (name override: Journal)
- TestPluginManifest guards it with kiosk.CheckT
- require SummerCMS v0.6.0, the framework minimum in plugin.yaml
journal-tag is a content type, but tags created on the fly from names in a
post write fired no event, so SEO and sitemap subscribers only heard of them
from a later bulk job. Each created tag now fires a RecordSaved with Created
inside the post write's transaction.
syncTranslations adds content_html to a translation that sends content, after
snapshotPost read the old values, so the event saw content_html as newly
submitted and reported Old: nil for a translation that existed.
submittedTranslations now lists content_html whenever a non-blank content is
sent, so the snapshot and the event cover the same fields.
POST and PUT /_journal/api/v1/posts saved the row, then the categories, tags
and translations, and fired RecordSaved only when all succeeded. A failure
part-way left a committed row (possibly a new slug) with no event, so
subscribers such as an SEO plugin never created the redirect. The write now
runs in one lagoon transaction with the event emitted inside it.
The descriptor matrix for posts, categories and tags: Describe per locale
with and without Polish unprefixed, Match exact and inexact per locale,
Records paging and cursors, host patterns, unknown and malformed ids;
journal-tag with tag_pages by default, on and off; translated slugs in
menu items and matching, including a changed Polish slug.
GET /_journal/api/v1/posts/{slug} now looks the slug up in translate's
slug index for the request locale after the default-locale slug and
before the numeric id. TestTranslatedSlug covers the translated slug,
the default slug, the id and an unknown slug.
- journal-post, journal-category and journal-tag carry pact.ContentDescriptor
(controller, morph name, Describe, Records, Match) through signpost
- Match reads the locale prefix and the pattern with signpost.MatchPattern,
finds the record by its slug in that locale, the default slug, another
locale's slug or :id, and is exact only on the record's own URL (D-20c)
- journal-tag (/blog/tag/:slug, P-11) with references and Resolve; host key
golem15.journal.tag_pages false empties tag URLs and stops tag matches
- TestContentDescriptors, TestJournalTagType; README documents both
- POST/PUT/DELETE /_journal/api/v1/posts fire RecordSaved/RecordDeleted
through cabana.EmitRecordSaved/EmitRecordDeleted, so a slug changed over
MCP reaches subscribers like an admin save
- PUT reports the changed columns and each submitted translation that
changed, read before the write (a translated slug per locale)
- TestAPIRecordEvents covers POST, PUT slug change and DELETE
- journal-post registers a pact.ContentDescriptor (posts controller, Post
morph name, Describe with per-locale URL, title, excerpt, image, words)
- descriptors read through cabana.TxFromContext inside a save
- TestRecordSavedDescribesNewURL: an admin slug change reaches a subscriber
after commit and Describe answers the new URL; a failed save fires nothing
pier now gives a backend subject only to a grant whose consented
administrator (pact.MCPGrant.BackendUserID) is the one linked; the fake
verifier's grants name the linked backend user.
draft_post declares Writes, and through a served pier endpoint a read-only
grant's prompts/list omits journal_draft_post while a read and write grant
lists it (C-IN-01).
- the seven tools compile in pier's catalog against the plugin's routes
- every input field is a path segment, a list query key or a create/update
body key the routes read
- the draft prompt renders with and without its optional tone
- journal.Plugin.MCP: list_posts, show_post, list_categories, list_tags,
create_post, update_post, delete_post and the draft_post prompt, guard
backend, listed with golem15.journal.access_posts; update fields are
optional so only sent keys reach the route
- optionalBackendPrincipal honours the MCP dispatch marker (no bearer
short-circuit; anonymous reads unchanged)
- require summercms v0.4.4
- TestJournalMCPSmoke, TestOptionalBackendPrincipalDispatch
Every command gets long help and examples, descriptions in imperative
sentence case, and a warn-only TestCommandsDocumented over bonfire.CheckDocs.
The plugin logs through a diary component logger instead of the process
default logger. Requires SummerCMS v0.4.0; command names and flags unchanged.
TestJournalAuthorProfile covers a backend user with only a subtitle and
metadata that is empty, null, not JSON or not strings: empty strings and a
null avatar, with the key set unchanged.
- GET /_journal/api/v1/authors/{author} adds subtitle and description from
backend_users.metadata and avatar (the admin avatar's public URL or null)
- TestJournalAuthorProfile pins the seven keys and checks the three values
Admin saves purged the site cache; the token API's create, update,
delete and featured-image writes did not, so API-published posts waited
for the site's cache TTL.
- framework release with cooler, site-cache purge and the 19.3 transport surface
- go mod tidy adds go-redis, otter and the AWS SDK modules as indirect requires
- site_cache_keys_test.go: wireSiteCache on a config without a database
- rename the Docker admin test to TestCategoriesAdminFrameworkKeysWin so the
unit test owns TestSiteCacheFrameworkKeysWin (one name per test binary)
- classes.SiteCachePurger, NewSiteCachePurger, PurgeSiteCacheAfterCommit and
the key and limit constants stay as deprecated aliases for one release
- wireSiteCache: framework keys site_cache.* win (Journal keys ignored with
a warning); with only golem15.journal.site_cache.* set it publishes the
purger from them with a deprecation warning, so menu and FAQ saves purge
through it too
- controllers take the purger from cooler.SiteCacheFromApp
- TestSiteCacheFrameworkKeysWin; README documents both key sets
journal-post resolved any post by id, so a menu entry or sitemap row
pointing at a draft or future post published its title and slug on the
anonymous menu API and in sitemap.xml. resolvePost now applies the
published filter; the reference list still offers drafts so editors can
wire a menu before the post goes live.
- TestMenuItemTypesPatterns (Docker-free): the five types, ordered
placeholder fill, date parts only for published posts in UTC,
alternates and the unprefixed locale, translation fallback, reference
parse, category tree, refusals without a booted app
- TestMenuItemTypes (Postgres): host overrides of one record type reach
only its list types, item and list-key overrides, references, nesting
at every depth, order with ties by id, published filters, descendant
categories, alternates in sort_order with ties by id
go test passes -test.short=true, so scanning os.Args for -test.short
never matched and every -short run started a Postgres container. The
three TestMains now parse the flags and ask testing.Short.