Built with Chinaski

Changelog

Every tagged release, newest first. Notes are published per release under their own content; see the Full-changelog links for the underlying diffs.

v1.11.0

2026-09-25

Minor release: Chinaski can now run entirely on an Android phone, publishing to Cloudflare R2, plus a new schedule block for recurring hours and dated exceptions.

Added

  • #255 (#7365): new Termux-based install path (install.sh --termux) runs the full admin (build, database, everything) on an Android phone with no root and no systemd, plus an idempotent boot/widget launcher (termux-start.sh) so it survives being closed and restarts itself after a reboot. See Publishing From a Phone.
  • #255: new deploy_auto_enabled master switch in Settings → Deploy gates both rsync deploy and S3 deploy behind one "Deploy automatically after a successful build" toggle, so either or both targets can be wired up without silently changing existing behavior. See SSH / rsync Deploy.
  • #255: new schedule block type. A weekly recurring schedule (service times, opening hours) plus a list of upcoming dated exceptions (closures, one-off events, cancellations), with automatic schema.org Event structured data. See Schedule.
  • #255: the admin's build/deploy status banner now walks through “Building your update…” to “Publishing your update…” to “Your update is live,” staying accurate for as long as a sync is genuinely still running, including a slow upload over a phone connection.

Upgrade notes

No manual steps required. If SSH/rsync or S3 deploy was already configured and working, the new deploy_auto_enabled master switch is backfilled on automatically during upgrade, so existing deploys keep firing with no action needed.

v1.10.4

2026-09-23

Patch release: a mobile-responsive audit of the default theme across all 32 block types, plus several longstanding bugs surfaced along the way.

Fixed

  • #253: full mobile-responsive pass on the default theme. The text and grid block types had no CSS at all; grid used an inline grid-template-columns that couldn’t be overridden by a mobile media query and now uses CSS custom properties instead. Fixed a template/CSS class-name mismatch that left the columns and grid block headings unstyled. code_block’s <pre> had no overflow handling, so a long line pushed the whole page wider on mobile instead of scrolling within the block. Added missing styling for two-button heroes and the secondary button-group variant. Fixed a real header overflow around 1024px viewport width (between the mobile breakpoint and the assumed-wide desktop nav row) that a realistic navigation triggers. The testimonial block now stacks under 480px instead of squeezing the avatar and quote into a fixed-width row. Button padding was bumped site-wide so tap targets clear the ~44px accessibility guideline.
  • #254: build.pl’s signed build manifest (private/build-manifest.json) was resolved from the script’s own install directory rather than following CHINASKI_OUTPUT, so redirecting build output elsewhere (e.g. a sandboxed or test build) silently wrote the manifest into the live instance’s private/ directory instead. Now follows the output directory’s location, a no-op for every normal deployment.
  • #254: btn_submissions and 30 other admin-panel strings were referenced by templates but never defined in any language, so admins saw raw key text (e.g. "hdr_status") instead of real labels. Found via a full sweep of every t('...') call against the actual defined keys. Added to all 4 languages.
  • #254: the block-form__field--w-half / newsletter-form__field--w-half width variant, offered in the form-field editor, had no CSS at all: "half width" did nothing. Fields now sit two-per-row on desktop, stacking under 640px.
  • #254: builder/site_templates/_landing_block_styles.tt (used by the standalone landing.tt template) only styled 15 of the 32 block types. The other 17 rendered completely unstyled on landing pages; ported over from the theme fix above.

Upgrade notes

No database migrations required for this release.

v1.10.3

2026-09-21

Patch release: breadcrumb and page_list heading i18n fix.

Fixed

  • #7290: the breadcrumb root crumb was hardcoded to the literal English string "Home" regardless of the page’s actual language. Now uses the real per-language title of the site’s home page, falling back to "Home" only when none exists for that language.
  • #7291: the page_list block’s auto-generated "Also in X" heading was hardcoded to English. Now uses a per-language prefix table (en/es/pt/fr).

Both were already fixed and verified live on this site before this release formalized the fix into the codebase with test coverage.

Upgrade notes

No database migrations or config changes required for this release.

v1.10.2

2026-09-21

Patch release: shortcode-escaping and broken-link-check fixes, plus two new admin-exposed settings.

Fixed

  • #7212: shortcode expanders ([form:slug​], {{block:name​}}, {{stack:name​}}, {{document:slug​}}) no longer expand inside <pre>/<code>. A docs page showing the syntax as a literal example no longer triggers a render error and blocks promotion.
  • #7215: _detect_broken_links no longer double-prepends the language directory onto hrefs that already carry an active-language prefix under lang_url_mode=path, which had been false-positiving every correctly-authored internal link on a path-mode multilingual site.

Added

  • #7214: org_email field added to Settings > Organization, alongside org_phone. The builder-side JSON-LD plumbing already existed but had no admin UI field to set it.
  • #6018: build_jobs (parallel build workers) exposed in Settings > General, previously only settable via direct SQL or the CHINASKI_JOBS environment variable. Blank preserves the existing auto-detect-CPU-count behavior.

Upgrade notes

No database migrations or config changes required for this release.

v1.10.1

2026-09-17

Patch release: deploy/ systemd unit fixes, no app or database changes.

Fixed

  • #247: StartLimitIntervalSec/StartLimitBurst were placed under [Service] in the per-instance systemd templates (chinaski@.service, chinaski-mcp@.service), where systemd silently ignores them. Moved to [Unit], where they’re actually honoured. Without this fix, a persistently-failing instance (e.g. one whose working directory got deleted) restarts forever instead of giving up after 5 failures in 60s and landing in failed.
  • #248: chinaski-backup@.timer had a trailing inline comment on Persistent=true (Persistent=true # catch up...), which systemd’s unit parser can’t handle; it failed to parse the value and silently dropped it, so a missed daily backup while the host was off never caught up on boot. Comment moved to its own line.

Upgrade notes

Existing installs: re-copy the three files in deploy/ (chinaski@.service, chinaski-mcp@.service, chinaski-backup@.timer) to /etc/systemd/system/ and run systemctl daemon-reload. No database migrations in this release.

v1.10.0

2026-09-16

Minor release. Four bug fixes since v1.9.0.

Fixes

  • Forms: date-type fields were never validated server-side: a direct POST bypassing the browser’s date picker let malformed input, or even markup/spam links, through unfiltered into a date field. Now validated as YYYY-MM-DD.
  • Settings: favicon regeneration silently failed to update favicon.svg on re-upload if the file had the wrong ownership (e.g. from a deploy that didn’t re-chown), while the PNG/ICO variants updated fine and the UI reported success. favicon.svg is now unlinked before rewrite, matching the existing PNG handling.
  • Builder: the sitemap never emitted the bare site root when home_redirect pointed at a regular page instead of the blog; and relative og:image/twitter:image URLs (invalid per the OG spec) are now absolutized across pages, posts, and the blog index.
  • Breadcrumbs: the blog breadcrumb crumb was hardcoded to the literal word “Blog” instead of using the site’s configured blog_index_title; instances with a custom blog label or permalink prefix (e.g. “Notes” at /notes/) got a breadcrumb trail that didn’t match their own nav/URL naming.

Full suite: 122 files, 9,687 tests, all passing.

Full Changelog: https://github.com/bigjohncobb/chinaski/compare/v1.9.0…v1.10.0

v1.9.0

2026-08-31

Minor release: a Markdown file importer lets you create draft posts/pages straight from .md files (with YAML front matter), plus a batch of 8 live-reported bugs fixed across Posts/Pages admin, permalink/link-building, and favicon handling.

✨ Features & UX

Markdown file importer for Posts & Pages (PR #242) - New /admin/import-markdown route, linked from both Posts and Pages list pages, accepts a single file or a batch of up to 100 .md files (40 MiB aggregate) and creates draft posts/pages from them, replacing the previous workaround of hand-writing HTML directly into the database. - Optional YAML front matter (title, slug, language, meta_title, meta_description, category, tags, date) fills in fields; missing values fall back to the file’s first H1 heading, then a prettified filename. - Every import lands as a draft, never auto-publishes and never touches scheduled-publish, by explicit design. - Per-file error isolation reports bad files by name without failing the rest of the batch.

🐛 Fixes

Posts admin bug batch: 8 live-reported issues (PR #241) - Restoring a draft (Posts and Pages) silently saved nothing: the request body wasn’t being read correctly, and a form-selector bug also targeted the wrong <form> on the Posts edit page. - Admin “View” links and the internal link-picker built post/page URLs incorrectly for nested pages and non-default permalink/language settings, diverging from what the site builder actually publishes. - Approving a pending post or page (review workflow) never actually made it live: it flipped the review status but not the publish flag. - Editor content accumulated stray whitespace and formatting drift on every save cycle; now normalized server-side. - Publishing a post could trigger a spurious “changes not saved” browser warning that aborted the request. - A live favicon-upload permission bug (root-owned files after deploy) could silently break favicon updates and site rebuilds. - A missing placeholder in the hero block template when no image is set

⬆️ Upgrading

No manual steps. All schema/config changes in this release apply automatically on next boot.

Full changelog: https://github.com/bigjohncobb/chinaski/compare/v1.8.0…v1.9.0

v1.8.0

2026-08-27

Minor release: forms gain conditional fields, read/unread tracking, and a reply link; the modularisation series finishes with Forms and Blocks extracted into their own plugins; webhooks, jobs, nav, redirects, datetime/MIME handling, and email/SMTP all get centralized, audit-driven refactors; plus a batch of real bug fixes including a live favicon/rebuild bug, a revision-restore FK violation, and several admin permission-gate gaps.

✨ Features & UX

Forms: conditional fields, unread tracking, reply link (PR #229) - condition_field is now a dropdown of the form’s existing fields (no more free-typed field names), with server-side validation and datalist value suggestions for select/radio/checkbox sources. - Submissions track read/unread state, unread rows get a “new” badge, the forms list shows “12 (3 new)”, and there’s a one-click “Mark all read”. - A “Reply” button on the submission detail opens a pre-addressed mailto: link when reply_to_field names an email-looking field.

🔧 Refactors

Forms and Blocks extracted into their own plugins, closing the modularisation series (PR #221, / PR #240) - lib/Chinaski/Plugin/Forms.pm and lib/Chinaski/Plugin/Blocks.pm now own the entire forms and blocks/snippets/stacks route surfaces, following the established Plugin::Media/Plugin::Settings pattern; cms.pl shrinks by nearly 2,000 lines from the Forms move alone. - Blocks additionally: create/update now use collected-error validation and shared form rendering (matching pages/posts), the block_types DB table becomes the single source of truth instead of a parallel @BLOCK_TYPES array, and a shared color-picker partial replaces 21 templates’ worth of duplicated pickers.

Redirects, nav, and forms sort-ordering centralized (PR #230; –/ PR #231) - Redirects create/update/CSV-import/chain-walk logic consolidated into shared helpers; CSV import now uses the same RFC-4180 parser as everywhere else instead of a hand-rolled split. - _swap_sort_order and _next_sort_order are now shared between nav, snippets, and form fields instead of three separate implementations; nav create/edit validation centralized (~85 lines removed).

Webhooks, scheduled jobs, and health reporting centralized (PR #235; // PR #236; /// PR #237) - Scheduled publish/unpublish, recurring background jobs, and backup-health summaries deduped across their admin and API consumers. - Webhook retry dispatch, URL validation, and the backup restore background-job wrapper centralized; before_dispatch hooks reorganized into named stages; build-status/disk-usage helpers shared across dashboard, health, and API endpoints.

Datetime and MIME handling unified; email/SMTP and migrations hygiene cleaned up (PR #238; //// PR #239) - One shared local↔︎UTC conversion helper pair replaces repeated per-route loops; a new Chinaski::MIME module is the canonical source for MIME↔︎extension mapping across media, forms, and document serving. - SMTP connection setup (including the header-injection-closing CRLF fix) consolidated into one sender; migrations documentation and existence-check helpers cleaned up; 22 new i18n keys x 4 languages close out remaining hard-coded admin strings.

🐛 Fixes

Favicon delete didn’t trigger a rebuild PR #223); deleting a favicon cleared the setting but left the old icon live in output/ (and thus on the public site) until an unrelated change happened to trigger the next build. Found live on the roiwire instance (stale for ~1h44m).

Favicon upload mangled non-square source images PR #225): a wide/short source image (e.g. a wordmark logo) silently produced a garbled crop instead of an icon; uploads more than 1.25:1 off-square are now rejected with a clear error.

Block revision restore always failed for ordinary blocks (PR #222): restoring wrote '' instead of NULL for form_id/content_json, violating the FK/CHECK constraints on any non-form, non-structural block.

Revision-diff JSON decoding used the wrong decoder for one of two cases (PR #224 audit + PR #232 fix): decompress_value returns raw bytes for compressed rows but an already-decoded string for uncompressed ones; a single blanket JSON decoder was wrong for one case regardless of which was picked. Two call sites (snippets revision preview, generic pages/posts diff) reintroduced this right after the first audit fixed it elsewhere; now both branch on utf8::is_utf8 at decode time.

Turnstile showed a false “could not be loaded” note under a working widget PR #227).

Test suite was leaking .db-wal/.db-shm sidecars and per-run docs directories, filling the CI box’s disk twice PR #234), added proper cleanup plus a cron safety net for hard-killed test processes.

This session’s admin bug-hunt batch (7 fixes, no ticket numbers; found via direct code audit rather than the backlog): - Search page “View” links resolved against the admin subdomain instead of the live site (always 404’d), and didn’t account for nested pages’ parent slugs. - Redirects create/update/delete/bulk had no entity-permission gate at all. Any editor with any other permission could manage redirects regardless of what they were actually granted. - Documents publish didn’t check version status server-side, letting a non-admin force a rejected version live even though the UI already hid that option; duplicate/delete were missing their permission gate entirely. - Snippets create lost the submitted code to a generic error page on a name collision instead of the friendly re-render update already had. - Themes delete didn’t check the theme existed in the DB before removing its on-disk directory. - Users create didn’t enforce the username format CSV import already requires. - Nav update silently zeroed sort_order when the field was omitted from the request instead of preserving the item’s position.

⬆️ Upgrading

No manual steps. Migration 061 (form_submissions.read_at) and all other schema changes in this release apply automatically via the normal migration path on next boot.

Full changelog: https://github.com/bigjohncobb/chinaski/compare/v1.7.1…v1.8.0

v1.7.1

2026-07-22

Patch release: forms gain SEO-friendly JSON-LD schema types, three new field types (date, validation rules, consent checkbox), a denser/more accessible admin UI, and a real production bug fix for page/post editing.

✨ Features & UX

JSON-LD schema types: Article, FAQ, Product, Event, HowTo (#2559, PR #216) - Opt-in schema_type on pages and posts: pick FAQ, Product, Event, or HowTo (posts are limited to Article/FAQ, since posts don’t have the product/event fields to back the others) and the builder emits the matching structured data automatically. - New FAQ question/answer and HowTo step editors on the page form.

Forms: date field type, per-field validation, consent checkbox (PR #218) - New date field type, rendered as a native <input type="date">. - Per-field validation rules (minimum/maximum length, minimum/maximum value, and regex pattern) enforced both client-side (HTML5 attributes) and server-side on submit. - New consent checkbox type for privacy-policy acknowledgment, with a configurable link that opens in a new tab.

Forms admin: denser settings tab + more accessible fields list (PR #217) - The Form Settings tab is now collapsible sections instead of one long scroll, with a sticky Save bar. - The Fields list shows type/required as badges, has larger touch targets, and reorder buttons are properly disabled at the top/bottom of the list with full ARIA labeling.

🔧 Refactors

PWA settings and asset list centralized (PR #215) - The builder and the Settings favicon-delete path now share one source of truth for the generated PWA asset filenames, instead of two lists that could silently drift apart.

🐛 Fixes

Page and post updates could fail with “no such table: main.*_old” (PR #214): - A leftover reference from an old migration meant page_snippets, post_snippets, page_area_blocks, and 3 other junction tables could point at tables that no longer existed, breaking updates to existing pages/posts (creating new ones still worked) on any instance that had been running since roughly v1.6.1 or earlier.

Forms admin polish (–,,,, PRs #212, #213) - Editing a form field no longer jumps you back to the Settings tab. - Add Field validation errors no longer wipe out what you’d typed. - An unsaved-changes warning was added to the Settings tab. - The delete-confirmation dialog on the forms list now correctly says “moved to trash” instead of implying permanent deletion. - SMTP failures now log a specific reason instead of a generic error. - A corrupted theme upload now reports “corrupt archive” instead of the misleading “missing required files”.

⬆️ Upgrading

No manual steps. If you’re running an instance from before this release, the page/post update fix PR #214) applies automatically via the normal migration path on next boot.

Full changelog: https://github.com/bigjohncobb/chinaski/compare/v1.7.0…v1.7.1

v1.7.0

2026-07-22

Minor release: backup/restore extracted into its own plugin, Backups and API Keys promoted to real pages, and the Posts admin screens finish their design-system migration.

✨ Features & UX

Backups and API Keys are now real pages, not settings tabs (PR #209) - /admin/backups and /admin/api-keys are canonical routes with their own URLs: the old /admin/settings?tab=backups / ?tab=api-keys links now 302 to them. - The 1,796-line settings template is split into per-tab partials (templates/admin/settings/_*.html.ep) instead of one monolith; 344 inline style= attributes were extracted into shared CSS classes.

🔧 Refactors

Backup/restore extracted into Chinaski::Plugin::Backups (PR #208) - All 10 /admin/backups/** routes, 3 /api/backups** routes, and ~30 supporting helpers moved out of cms.pl into their own plugin, following the same pattern as the earlier Media/Settings extractions. - Backup-create and restore-result handling are now shared between the admin and API routes instead of each duplicating its own logic.

Posts admin screens finish the po-* → chinaski-* design-system migration (PR #210, follow-up to) - Inline font-family declarations and non-token color literals removed from the Posts form/list, trash, and revision-diff templates.

🐛 Fixes

  • A 120s restore-timeout race where a late-arriving subprocess result could silently apply after the request had already shown the user a “timed out” error.

⬆️ Upgrading

No manual steps: if you have /admin/settings?tab=backups or ?tab=api-keys bookmarked or linked anywhere outside the app, update to /admin/backups / /admin/api-keys (the old links still work via redirect, just with an extra hop).

Full changelog: https://github.com/bigjohncobb/chinaski/compare/v1.6.1…v1.7.0

v1.6.1

2026-07-06

Patch release: fix missing Crypt::PBKDF2 dependency.

  • cms.pl uses Crypt::PBKDF2 directly for password-hash migration but it was never declared in cpanfile, so any 1.6.0 instance that didn’t already have the module installed crash-looped on startup (Can’t locate Crypt/PBKDF2.pm in @INC). Fixed by declaring the dependency in cpanfile.

v1.6.0

2026-07-06

Minor release: everything merged since v1.5.3.

  • Backup hardening: manifest.json with per-file SHA-256 in every archive, PRAGMA integrity_check before archiving, atomic local publish, restore-path validation on all 4 sources, post-backup verification (backup_runs) with Verified badges, configurable retention with pruning, backup metrics on /admin/metrics, dry-run restore preview + confirm, per-row type/schema on the Backups tab, data-loading-text form bridge.
  • Builder: pages expose their published child pages to templates as ‘children’; page_list shares the same precomputed map.
  • Post editor: WYSIWYG restored on the new-post page (autosave-key JS SyntaxError), full two-panel design-system parity with Pages, review buttons via formaction.
  • Shareable page preview renders content_json-driven blocks (faq, pricing_table,…) instead of heading-only.
  • MCP: process manager, health, retry logging, sessions, discovery, bulk ops, resources.
  • Security: SVG media uploads rejected ; post password hashing .
  • Refactors: webhook dispatch unified into Chinaski::Webhook ; media library extracted into Chinaski::Plugin::Media .

All four feature PRs were code-reviewed pre-merge; review fixes are in the squashed PR commits. Full suite green on CI for the final merged tree.

v1.5.3

2026-07-03

Landing pages, forms, and a security hardening pass, plus a data-integrity/build-speed cleanup batch.

✨ Features & UX

Unthemed landing pages - New raw.tt template: admin authors a complete HTML document as the page body, no site header/footer/CSS wrapper, for landing pages that shouldn’t carry the site’s branding. {{block:...}} shortcodes still resolve. - New landing.tt: same unbranded posture, but with a self-contained default stylesheet covering all 15 block types, so hero/CTA/stat blocks etc. render with reasonable styling without hand-written CSS. - Template dropdown now lists theme-provided templates too, and preserves a page’s template across a theme switch instead of silently corrupting it to the first alphabetical option. - Password gate enforcement now checks whether the resolved template actually implements the gate (not just the filename raw.tt), closing a bypass where any gate-less template could ship password-protected content with no gate UI. - Bulk-create gained a template picker and a shared body_template with {{title}}/{{slug}} mail-merge, so N pages of one design can carry distinct headlines without hand-editing each one.

Forms - Marketing attribution capture : UTM params, click IDs (gclid/fbclid/msclkid/ttclid), and the full landing-page URL are parsed from the Referer header and stored on the submission, honouring DNT/GPC. Reimagined submissions list (source/campaign filters, derived source badge) and a new single-submission detail screen. - Turnstile submit-gate : a fast submitter no longer gets a false “Bot verification failed”; the submit is held until the widget issues a token, then auto-resubmitted. - Pre-fill public form fields from URL query params, covering text/email/checkbox/checkboxes/address fields. - Per-item distinct body content in bulk-create via items_input : paste repeating title/body blocks instead of one shared template for every item. - Form revision history : every settings/field save snapshots the previous version; view a revision list, diff against current, and restore. - Normalized submission storage (: submission answers are now written to a queryable submission_values table (in addition to the legacy blob for back-compat), plus a number field type. - Per-form upload quotas and private storage : cap total upload size per form, and optionally store uploaded files outside the public directory with an admin-only download route.

Posts & pages - Area blocks (hero/CTA/stat) now render on posts, not just pages; they were silently being collected but never passed to the post template. - Featured image focal-point picker : visual X/Y picker persisted per page/post, applied via object-position in the responsive image markup.

Build tooling - Benchmark fixture seeder + permanent per-phase timing instrumentation in the builder, establishing the baseline the rest of the build-speed series will be measured against.

🔒 Security

Lockout messaging, headers, and abuse-path throttling - Exact wait-time lockout messages replaced with a generic message on login/forgot-password/suspended-account, closing an information leak. - Permissions-Policy header added, restricting browser features this CMS never uses. - Decoy Disallow entries in robots.txt pointing at monitored honeypot paths. - Tarpit delay on login failures, rate-limited form submissions, and honeypot hits: non-blocking, so legitimate traffic isn’t held up while an attacker burns wall-clock time.

Server fingerprinting, registration abuse, forensic logging - Server: Mojolicious (Perl) response header suppressed. - Per-IP rate limiting on /register (every failed attempt counts, not just username-taken/DB-failure; closes a gap where malformed submissions could flood the endpoint without ever tripping the lockout) plus a constant-time username-exists check. - High-signal security events now also ship to an external append-only log (0600 permissions), so the forensic record survives a compromise of the SQLite database. - Honeypot endpoints (/wp-login.php, /.env, /phpmyadmin, etc.) fingerprint automated scanners. Public-site reachability: the example Caddy config (deploy/chinaski.Caddyfile.example) now proxies these paths to the app on the public vhosts too, not just admin.*; a production deploy using the live Caddy config will need the equivalent proxy block applied manually (see Upgrading).

🐛 Fixes & hardening

  • Data integrity / scale (Phase 40, /–/): blocks/globals.content_json now validated as JSON at write time (UI, API, imports) with a matching SQLite CHECK constraint; build_log capped at 1000 rows; unbounded page ancestor queries bounded with LIMIT; nav_items sort-order renumbering fixed; backup script now checkpoints WAL and vacuums.
  • Quick-win batch : CRLF stripped from field labels before they reach notification email bodies (closes an email-body-spoofing gap); dropped the spoofable X-Country-Code fallback in geo-blocking; fixed prefill-on-validation-failure for address and multi-value checkbox fields; submission reference tokens are now a random CSPRNG value instead of a reversible/enumerable one.
  • SRI/caching: admin CSS URLs are now cache-busted by content hash, and the brittle Turnstile script-tag integrity hash (which Cloudflare rotates without notice) was removed.
  • i18n: admin translation strings extracted from cms.pl into Chinaski::I18n, no behavior change.
  • Posts list: a crash in the client-side filter (thrown by a header/queue row lacking the data attributes the selector assumed) that broke filtering for every row whenever a scheduled post existed, newly reachable via URL-restored filters.
  • CI: fixed an intermittent documents_module test failure caused by a shared /tmp/private archive path across concurrent root/unprivileged test runs on the same CI host.
  • Post-merge review passes on the landing-page and forms work above turned up and fixed several real bugs before/after merge, including: a mail-merge reinterpolation bug and an attribute-breakout escaping gap in bulk-create; an uploaded-file leak and a TOCTOU race in upload quota enforcement; a retention-auto-prune leak that never freed quota; revision snapshots being recorded even when the save they preceded failed; a diff-view false positive after restore; a typo breaking address-field rendering on the submission detail page; a non-transactional/non-idempotent submission-values migration; and a Permissions-Policy regression that broke video-embed fullscreen on preview routes.

⬆️ Upgrading

No manual DB steps: schema changes apply automatically and idempotently at startup; older databases upgrade in place.

One manual step if you’re using the shipped Caddy example as a starting point for your own config: the honeypot-path proxy block added to deploy/chinaski.Caddyfile.example needs to be copied into your live Caddy config and applied (docker restart caddy or equivalent) for scanner probes against the public site to reach the app’s logging/tarpit; this repo’s example file was updated, but a live deployment’s Caddyfile is maintained separately and isn’t touched by this release.

Full changelog: https://github.com/bigjohncobb/chinaski/compare/v1.5.2…v1.5.3

v1.5.2

2026-06-30

Fixes

  • Bulk delete submissions: every_param('ids') returns an arrayref; missing @{} dereference meant the array was never unpacked and zero rows were deleted on every bulk-delete attempt.

v1.5.1

2026-06-30

Bug fixes

  • fix(forms): SMTP LOGIN auth without Authen::SASL: Net::SMTP->auth requires Authen::SASL which is not installed; replaced with a manual AUTH LOGIN exchange in both _send_form_email and _send_admin_email. Outbound form and build-notification emails now work.
  • fix(forms): missing btn_delete_selected translation: the bulk-delete button on the submissions page showed the raw key name instead of “Delete selected” (all 4 languages). Bulk deletion of form submissions now works.

v1.3.0

2026-06-14

Release of Phase 80 (Pages) plus a security fix and an upgrade-safety migration.

✨ Features & UX, Phase 80: Pages

  • Search: server-side q= title/slug filter on the pages & posts lists, composing with sort/dir + pagination.
  • Review workflow: approving a page/post now emails the content creator (mirrors the reject flow) and logs it in the audit trail.
  • Export/import round-trip: pages & posts CSV exports now emit the full column set in the import’s order (OG metadata, canonical URL, scheduling, parent_id, recurring, …); scheduling datetimes convert to the site timezone so re-import round-trips.
  • Accessibility: aria-sort on sortable headers, aria-labels on custom-field inputs and select-all checkboxes, role=status/aria-live on the info flash, non-colour cue on revision-diff changes.
  • Bulk-action flashes now name what they did; revision timestamps render in the site timezone.

🔒 Security

  • Document access tokens now use a CSPRNG. Replaces a token derived from non-cryptographic rand + predictable inputs with a 256-bit /dev/urandom bearer secret, matching session/API-key tokens. Existing tokens remain valid; no migration required.

🐛 Fixes & hardening

  • Edit-form error re-renders (validation and permission failures) now preserve submitted data, including snippet/area panel checkbox state.
  • Posts JSON export no longer used SELECT *: explicit column list excludes password_hash from shareable exports.
  • Upgrade safety: added an idempotent og_image_url startup migration for pages/posts, ordered before the phase41 table rebuild that reads the column, so databases predating it upgrade cleanly.

⬆️ Upgrading

No manual steps. Schema changes are applied automatically and idempotently at startup; older databases upgrade in place. Document tokens issued under the previous scheme stay valid.

Full changelog: https://github.com/bigjohncobb/chinaski/compare/v1.2.3…v1.3.0

v1.2.0

2026-06-12

What’s new

New block types (14)

Text, Button Group, Divider, Spacer, Heading, Pricing Table, Feature Grid, Callout, FAQ, Code Block, Team, Timeline, Logo Bar, Audio: all pure Template Toolkit + CSS, air-gap safe, with admin forms and render tests.

Stacks & composition

  • Dependency tracking, used-on view, and blast-radius panel
  • Revision history with restore
  • Standalone Preview Stack route

Pages & Posts (Phase 47)

  • Custom fields, Authors panel, Translations panel in sidebar
  • Documents module
  • Migrations 026–039 automated on startup

Health & build

  • Structured /health endpoint with per-check status and latency
  • Pre-promotion validation suite (validate_build) with HTML, sitemap, and link checks
  • Promotion guard for always-online failover

Security

  • Block style XSS: CSS value allowlist on all style fields; | html filter on all 33 block templates; FAQ JSON-LD generated via JSON::PP (no hand-rolled escaping)
  • Content URL sanitization: javascript: and other non-http/https URLs stripped from content_json at save time across all block types
  • CSRF: token guard added to POST /register and POST /forgot-password
  • Login timing: dummy bcrypt verify when username not found; response time no longer reveals account existence
  • Session TTL: sessions now expire (8 h default, 30 d remember-me); pre-migration sessions without a TTL are pruned on first startup
  • Pages permission enforcement: can_edit_entity_type('pages') guard on all 17 previously unguarded routes
  • Pages require_approval: bulk-publish and publish-now respect the approval workflow for non-admin editors

Fixes

Backups - SQLite Online Backup API for consistent snapshots (eliminates WAL race) - Shared _backup_paths helper, all four restore paths use identical directory conventions - Themes table metadata exported to themes.json in every archive; restored on all four restore routes - DO Spaces SigV4 query-string signing fixed, pagination beyond 1000 objects now works - 50 MiB upload cap on restore - Backup/DO Spaces credential changes no longer trigger a full site rebuild - S3 credential/network errors surfaced instead of silently showing an empty list

Pages & bulk operations - Phantom revisions fixed: save_revision moved inside the optimistic-lock transaction - Single-page delete and duplicate wrapped in transactions; duplicate now copies page_area_blocks - Bulk publish/unpublish/schedule/unschedule all atomic - Bulk ID list capped at 500; revisions list capped at 500

Admin - Snippet move-up/down rewritten to swap adjacent items in a transaction (was blindly ±1) - Form-field move-up/down transaction wrappers added - CSV import error flash sanitized (full error logged server-side only) - i18n: double-escaping of HTML entities in t helper fixed - N+1 queries, unbounded lists, and build inefficiencies resolved - Startup migrations 035 and 039 added for stack assignment columns (fixes builder crash on fresh deploys)


Upgrade notes

  • Migrations 026-039 run automatically on first startup, no manual SQL needed.
  • user_sessions.expires_at column is added automatically; existing sessions are invalidated on first restart (all admins will need to log in again).
  • The themes.json sidecar is included in backups from this version onward. Old backups restore cleanly: the sidecar is simply absent.

v1.1.2

2026-06-05

What’s New

Fixes

  • Build: salvage subdomain broken-link check; absolute hrefs now resolve against the language root in subdomain mode; harden last_built validation against future/invalid timestamps
  • Build: roll back smart incremental rendering in favor of two corrected safeguards (avoids pages disappearing after incremental builds)
  • Bulk create: stop clipping titles that contain commas; correct quote escaping in the bulk_create title helper
  • i18n: repair broken title helpers and mis-nested lexicon keys; add missing btn_upload lexicon key
  • Auth: require admin auth on the /admin dashboard route

Chores

  • Branding: set SBS avatar as favicon

Full Changelog: https://github.com/bigjohncobb/chinaski/compare/v1.1.0…v1.1.2

v1.1.0

2026-06-05

What’s New

Features

  • Internal link suggestion panel: editor drawer that scans body content and suggests unlinked mentions of published pages/posts, with Link it / Link 1st / Link All / Skip actions and keyboard accessibility
  • Real-time build progress tracking: live progress bar, page/post counts, and current item display in the admin dashboard during builds
  • Build failure prevention system: safety check when 0 pages render, CHINASKI_FORCE_FULL env var override, /api/health monitoring endpoint, last_built future-date validation, force-rebuild script, and cron monitoring script

Fixes

  • i18n: repaired double-encoded UTF-8 / mojibake for uppercase accented characters (É, Ú, À, Ã, etc.) across ES, FR, PT translations
  • Templates: corrected title directives on dashboard and search pages
  • CMS: forward-declare $DOCS_DIR before first use

v1.0.2

2026-06-04

What’s Changed

  • fix(i18n): fix remaining mojibake for uppercase accented chars (É, Ú, À) by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/79

Full Changelog: https://github.com/bigjohncobb/chinaski/compare/v1.0.1…v1.0.2

v1.0.1

2026-06-04

What’s Changed

  • feat(editor): move page action buttons to top of sidebar by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/44
  • feat(blocks): add page_list block type by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/45
  • fix(favicon): generate and deploy favicon.svg on upload by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/46
  • feat(i18n): Group A: translate auth layout, login, and register by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/47
  • feat(i18n): Group B: translate pages and posts templates by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/48
  • fix(builder): render [form:slug​] shortcodes inline instead of iframe by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/49
  • feat(i18n): Group F: translate forms builder and translation workflows by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/50
  • feat(i18n): Group G: settings, themes, and calendar by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/51
  • fix: add design brief upload UI to themes generate panel by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/52
  • fix: use per-language subdomain for View link in pages admin by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/53
  • i18n: Group I: dashboard, admin tools, and maintenance by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/54
  • i18n: Group J: flash messages in cms.pl by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/55
  • feat(documents): make published documents publicly accessible by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/56
  • feat: build performance + configurable API rate limit by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/57
  • fix: six one-liner bug fixes: settings and search by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/58
  • fix: search UX and settings layout bugs by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/59
  • fix(build): increase timeout and lock TTL; add page-list block styles by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/60
  • fix: settings correctness and search result quality by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/61
  • fix: settings tab data, security_contact validation, webhook events by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/62
  • fix: API key banner visibility and audit_log retention by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/63
  • fix: pages list: add page-jump input to pagination by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/64
  • fix: search results, full action set and resolved URL paths by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/65
  • fix: search results: body match context excerpt by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/66
  • fix: move settings audit and snapshots to dedicated History tab by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/67
  • feat: expose API key rate limit in settings UI by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/68
  • fix: trim verbose settings panel hint texts by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/69
  • fix: move DO Spaces credentials from Integrations to Backups tab by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/70
  • fix: cancel dashboard build-poll timer on page unload by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/71
  • fix: History tab routing and DO Spaces credential reset by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/72
  • feat(schema): Organization JSON-LD on every page by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/73
  • fix(builder): skip stale cleaner on incremental builds by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/74
  • fix(cms): forward-declare $DOCS_DIR before first use by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/75
  • fix(templates): use double quotes for title tags containing t by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/76
  • fix(i18n): repair double-encoded UTF-8 in all translation strings by @bigjohncobb in https://github.com/bigjohncobb/chinaski/pull/78

Full Changelog: https://github.com/bigjohncobb/chinaski/compare/v1…v1.0.1

Read this site in