Skip to content

Changelog ​

What changed in each release of Trek, newest first, with upgrade notes when an app needs to act. Unreleased lists what's merged but not yet published.

[Unreleased] ​

Added ​

  • rails g trek:public_accounts Member --invite-only, for a private site where only the accounts the app created sign in: a code is emailed only to an email that has an account, with the same answer for every email, and signing in never creates an account. Which accounts may sign in is Member.for_sign_in(email), to narrow in the app (e.g. active.find_by(email:)). Codes only. See Public accounts.
  • Generated apps check their npm packages for known vulnerabilities, as bundler-audit does their gems: make scan and a new npm-audit CI job run yarn npm audit, failing on any advisory or deprecation in the packages the app bundles (dependencies, not devDependencies). See Vulnerable dependencies for how to resolve one. Upgrade note: move the PostCSS packages to devDependencies first (see below), then add the npm-audit job next to bundler-audit in .gitlab-ci.yml, and yarn npm audit --recursive --environment production to make scan (see lib/generators/trek/templates/.gitlab-ci.yml).
  • The Controllers reference documents Trek::Localization: where a request's locale comes from, and how to add a source by overriding locale_for_request.
  • bin/rails trek:security:check checks a deployed app's configuration against Trek's recommended settings: HTTPS and HSTS, the Content Security Policy, the cache store, the proxies and ADMIN_ALLOWED_IPS, the audit log and the scheduled purges, the admin users (password only, dormant, password expiry) and Active Record Encryption's keys. It prints each check with ✓, •, ⚠ or ✗ and a link to the docs, in a dated report to keep as evidence, and exits with status 1 when a check fails. See Self-check.
  • A Settings › Security page in the admin, for admins only, shows the same checks, dated, and checks the request that opened it too: that it reached the app over HTTPS, and that the IP address Rails sees is the one a CDN forwards. See In the admin. Upgrade note: run rails g trek:install:admin:security_check, and add the page to the Settings section of app/views/layouts/admin.html.slim, after the background jobs: s.with_item(text: t("admin.security_checks.show.title"), href: %i[admin security_check], allowed_to: Current.user.admin?). In apps with Trek's host specs, copy spec/requests/admin/security_checks_spec.rb from Trek's templates (lib/generators/trek/templates/spec/requests/admin/).
  • A Personal data page lists the personal data Trek's generators create, table by table: why it's kept, for how long, and what deletes it; what deleting a user or an account leaves; and the third parties a default app sends some of it to. For an app's record of processing activities, and security questionnaires.
  • The Deploying to Scalingo guide covers what a first deploy still needed: keeping Solid Cache in the app's single database, the environment variables the generated code reads, domains and their certificates, deploying from GitLab CI, and > /dev/null on env-set so secrets aren't printed.
  • Trek::Auditable's audit_read_actions lists actions that only read although they're not a GET, such as the chips hotwire_combobox requests with a POST, to keep them out of the audit log. See Customizing. Upgrade note: an app overriding read_only_request? for this can set self.audit_read_actions = %i[chips] instead.
  • Trek::PurgeExpiredSignInCodesJob deletes a public account model's expired sign-in codes, which each hold an email: only a new code deleted them, so the last ones requested were kept for good. rails g trek:public_accounts schedules it every night in GoodJob's cron, with codes (3:45am). See Public accounts. Upgrade note: in apps signing in with codes, add to config.good_job.cron, for each public account model (here Member): purge_expired_member_sign_in_codes: { cron: "45 3 * * *", class: "Trek::PurgeExpiredSignInCodesJob", args: ["Member::SignInCode"] }.
  • bin/rails trek:attachments:strip_image_metadata strips the metadata of the images stored before ImageUploader stripped it on upload (DRY_RUN=1 only lists them). Each image is re-encoded and replaced in place, so every record, page version and content editor block pointing to it keeps working. See Image metadata. Upgrade note: once the app's ImageUploader strips metadata (see below), back up the bucket, or enable its versioning, then run the task once in production.

Changed ​

  • A paragraph holding only line breaks (Shift+Enter) counts as empty, as a paragraph with no content does: it renders nothing (Trek::Content::Paragraph.empty?) instead of a blank space, a list item holding only such a paragraph is skipped, and content is cleaned of them before it is saved (Trek::Contentable). A line break between words stays. Upgrade note: existing content is cleaned the next time it is saved; apps that pruned these paragraphs before saving can drop that code.
  • The trek_known_users cookie, which recognizes a browser that already signed in as a user so that only a new one emails them, is encrypted and kept 13 months after the last sign-in, instead of signed and kept 20 years: someone using the browser can no longer read who else signed in from it. Browsers recognized by the former signed cookie still are. Upgrade note: apps have their own copy of app/controllers/concerns/trek/authentication.rb: replace its signed_in_from_this_browser? and remember_sign_in_from_this_browser, and add known_user_ids, from Trek's (lib/generators/trek/templates/controllers/concerns/trek/authentication.rb). In apps with Trek's host specs, copy spec/requests/admin/sign_in_notices_spec.rb from Trek's templates.
  • Security: the ImageUploader trek:install:uploaders copies strips an uploaded image's metadata (EXIF, with the GPS location and the camera's serial number, XMP, IPTC), which was published with the file. It re-encodes the image in its own format after applying its orientation (a JPEG or a WebP at quality 90, every frame of an animated WebP), and keeps its color profile with libvips 8.15 or later. It also accepts WebP images, besides JPEG and PNG. Thumbnails and other sizes are on by default: the derivation endpoint is enabled and mounted at /derivations/image, and makes every size a WebP at quality 80, without metadata. An image's sizes are deleted with it. See Images: types and sizes. Upgrade note: apps have their own copy of app/uploaders/image_uploader.rb: re-run rails g trek:install:uploaders, which offers to replace it with Trek's, then bring back the app's own changes, and mount the endpoint in config/routes.rb if it's commented out: mount ImageUploader.derivation_endpoint => "derivations/image". Images uploaded before keep their metadata until they're uploaded again, but their sizes don't.
  • The generators schedule Trek's purges every night in GoodJob's cron, which they left to each app, and most never did: expired sessions and old audit log rows were kept for good. rails g trek:install schedules Trek::PurgeExpiredSessionsJob (the admin's sessions, 3am), trek:audit_log adds Trek::AuditLog::PurgeJob (4am), and trek:public_accounts the purge of its accounts' sessions (3:30am). Without GoodJob's initializer, the generators say what to schedule instead. See Scheduled jobs. Upgrade note: in config/initializers/good_job.rb, add to config.good_job.cron the entries the app is missing: purge_expired_sessions: { cron: "0 3 * * *", class: "Trek::PurgeExpiredSessionsJob" }, purge_audit_logs: { cron: "0 4 * * *", class: "Trek::AuditLog::PurgeJob" } with the audit log, and purge_expired_member_sessions: { cron: "30 3 * * *", class: "Trek::PurgeExpiredSessionsJob", args: ["Member::Session"] } for each public account model (here Member). An app on another Active Job backend schedules the same jobs with it.
  • The admin's image and sound fields upload with Uppy 6, up from Uppy 3, which brought nanoid 4 and its four advisories (three high) that yarn npm audit reported in every app (no nanoid 4 fixes them). The fields no longer bundle Uppy's stylesheet: it only styles Uppy's own interface, which Trek doesn't use.
  • rails g trek:install:postcss adds the PostCSS packages to devDependencies, as the other build and lint tools, rather than to dependencies, and no longer adds postcss-inline-svg, which Trek's stylesheets don't use any more: it brought a css-what with a high advisory. Upgrade note: in package.json, move postcss, postcss-flexbugs-fixes, postcss-import, postcss-modules and postcss-preset-env to devDependencies, and run yarn install. Unless the app's own CSS uses @svg-load or svg-load(), remove postcss-inline-svg from package.json and its require("postcss-inline-svg")(…) from postcss.config.js.
  • rails g trek:scalingo and the Deploying to Scalingo guide pin trek-buildpack 0.1.4, up from 0.1.0. Since 0.1.0, the buildpack installs the Bundler version of Gemfile.lock (apps whose lockfile asked for a newer one died on start with Could not find 'bundler') and forces its install past the default gem, and it now preloads jemalloc: the libjemalloc2 of the Aptfile was installed but never used, as rv's Rubies aren't built with it. Upgrade note: on each Scalingo app, run scalingo --app <app> env-set BUILDPACK_URL=https://github.com/etaminstudio/trek-buildpack/archive/refs/tags/v0.1.4.tar.gz; it applies from the next deploy.
  • Trek no longer requires json < 3, which made bundle add trek on a fresh Rails 8.1.4 app silently install Trek 2.2.0. json 3 needs Rails 8.1.4 or later: with an older Rails, the app now fails at boot saying so. Upgrade note: apps on Rails 8.0 or 8.1.0 to 8.1.3.1 keep the json 2 in their Gemfile.lock; add gem "json", "< 3" to the Gemfile so a bundle update doesn't bring json 3.
  • rails g trek:scalingo comments out the kamal gem rails new adds, since a Scalingo app doesn't deploy with it, and runs bundle install: Kamal's SSH dependencies (net-ssh, net-scp, net-sftp, sshkit, bcrypt_pbkdf, ed25519) leave the bundle, and with them the advisories that kept failing bundler-audit. config/deploy.yml, .kamal/secrets and bin/kamal stay in place. Upgrade note: apps already on Scalingo can re-run rails g trek:scalingo, or comment out gem "kamal" by hand and run bundle install.
  • bin/rails g trek:scaffold gives the new model a to_s returning its first string attribute (title.presence || super), which the admin shows records by: they were listed as #<Book:0x…> until its commented-out to_s was filled in. Without a string attribute, it stays a TODO. The Getting started guide and the Scaffold page now say to run the migration, and list the model's TODOs.
  • The docs, the generators' messages and the security checks write commands as bin/rails …, the app's own Rails, instead of a mix of rails g … and bin/rails ….

Removed ​

  • DeepL: the scaffold generator no longer translates the new locale files with DEEPL_AUTH_KEY, Trek no longer depends on deepl-rb, and rails g trek:install:translations no longer adds it, nor a DeepL translation: section to config/i18n-tasks.yml. Upgrade note: apps can remove gem "deepl-rb" from their Gemfile, the translation: section from config/i18n-tasks.yml, and DEEPL_AUTH_KEY from their environment; an app that still translates with i18n-tasks translate-missing --backend=deepl keeps the gem and the section.

Fixed ​

  • Password managers such as Bitwarden didn't offer to save the password after signing in to the admin, until the page was reloaded: Turbo replaced the page's body, and the prompt they had added to it. The forms that take a password (the admin's sign-in and password reset, and trek:public_accounts' sign-in, sign-up, password reset and password change) now submit without Turbo, and load the next page in full. Upgrade note: add data: { turbo: false } to the form_with of app/views/admin/user_sessions/new.html.slim and app/views/admin/user_password_resets/edit.html.slim, and, in apps that ran trek:public_accounts, of its sessions/new (the password form), registrations/new, password_resets/edit and passwords/edit views.
  • A locale param that isn't one of the app's locales (?locale=xx) made every controller including Trek::Localization answer with a 500 (I18n::InvalidLocale), the admin's included. It's now ignored, and the locale comes from the user or the browser as without it.
  • The password strength meter no longer makes the form grow when its label appears, as a password is typed.
  • An app that resolved Prism 2, as a fresh one has since rubocop-ast 1.51.0, failed at boot wherever it eager loads, in production and on CI (uninitialized constant Prism::MissingNode): refract, which prosemirror brings through Phlex, uses a node Prism 2 removed. Trek now requires Prism < 2. Upgrade note: if the app's Gemfile.lock has Prism 2 (bundle info prism), bundle update trek brings it back to 1.x.
  • The arrows around a larger image in the content editor didn't show in host apps: their build looked for the icons in the app's own app/assets/icons, and left an invalid rule. The icons are now inlined in the stylesheet.
  • The generated .gitlab-ci.yml ran on the ci-ruby/node-24 image whatever Node the app pins: rails g trek:install:ci now picks the image of the major in .node-version (20, 22, 24 or 26), and falls back to Node 24, with a warning, without the file or for a major ci-ruby has no image for. Upgrade note: apps pinning another Node than 24 can set node-<major> in the image: of .gitlab-ci.yml.
  • rails g trek:public_accounts added the account's attributes to Current without indenting them. Upgrade note: apps that ran it on Trek 2.6.0 can indent the three lines it added to app/models/current.rb.
  • The sign-in concern rails g trek:public_accounts generates for a website (app/controllers/concerns/<accounts>/authentication.rb) failed the generated CI's Brakeman job (HTTP verb confusion): it now remembers the page to return to for HEAD requests too, as for GET. Upgrade note: apps that ran it on Trek 2.6.0 can replace if request.get? with if request.get? || request.head? in require_<account>.
  • The Getting started guide and the README installed Yarn with corepack enable, which Node 25 and later no longer ship (corepack: command not found). They now install corepack first, with npm install --global corepack, or Yarn with mise.
  • The admin's edit form of a page made of sections (sectioned) but not versioned failed with a NameError (vf). Upgrade note: in app/views/admin/pages/_form.html.slim, replace vf.object.sections_instances with @object.sections_instances in the branch for pages without versions (the second one).
  • Docs that no longer matched the code: the Fragmentable configuration, the scaffold's form fields and route, icon keys (trek/search), Trek::Search, Sectionable, Orderable's ordered, the SEO fallbacks, the cryptography table and self-check levels in Security, CSP reports in Personal data, S3_BASE_URL, the admin's per-IP sign-in limit, the installer's linters, mailers and specs, media-src, FEATURE_STORAGE, TREK_LOCALES, and the json pin Rails before 8.1.4 needs.
  • Deleting a public account left the sign-in codes waiting for its email, which hold that email: they're keyed by email, not linked to the account, so only their expiry and the nightly purge deleted them. rails g trek:public_accounts (with codes) now deletes them with the account. Upgrade note: in apps signing in with codes, add to the account model (here Member), after has_many :sign_in_codes: after_destroy { Member::SignInCode.where(email:, purpose: "sign_in").delete_all }.
  • Small inaccuracies in the reference docs: GateComponent and the toast component's name, what f.group renders, the formatter's narrow no-break spaces, User#last_activity_at, Current's attributes, page.versions.current, and the generators list, which now has trek:scalingo and trek:upgrade:v2.
  • bin/rails g trek:scaffold left the new model's entry in Admin::Dashboard::MODELS_IN_MENU unindented.

[2.6.0] - 2026-10-28 ​

Added ​

  • The content editor's link, image and prompt panels give hints under their fields: what to write in an image's alternative text, credit and caption, that a link goes to a page or a URL, and what a prompt's title is for. Upgrade note: you can now drop your own helpers.hint.trek/content_editor/* translations.
  • The content editor's link panel asks for the link's title, shown when hovering it, as its prompt panel does: Tiptap 3 keeps it on the link, where Tiptap 2 dropped it, and the site renders it.
  • CSP violations reported to AppSignal (Trek::CspReportsController) tell what was blocked: they're tagged with the browser's user agent, where it happened (the source file and line) and, for an inline script, its first 40 characters, which the generated policy now asks for with 'report-sample'. The log line gives the source and sample too. Tags are cut to 255 characters, as AppSignal silently drops longer ones. Upgrade note: add :report_sample to policy.script_src in config/initializers/content_security_policy.rb to get the samples.
  • rails g trek:install:good_job, a new installer step (also runnable alone) making GoodJob the Active Job backend, as every Trek app was doing by hand: it swaps solid_queue for good_job in the Gemfile (Solid Cache stays), removes the Solid Queue setup rails new leaves behind (config/queue.yml, config/recurring.yml, db/queue_schema.rb, bin/jobs, the Puma plugin, the queue: database and Kamal's SOLID_QUEUE_IN_PUMA/JOB_CONCURRENCY variables), and configures the adapter (Active Job' test adapter in config/environments/test.rb, so specs can check what is enqueued), :async execution in production, the queues, cron and error reporting in config/initializers/good_job.rb. The dashboard is mounted at /admin/good_job with a menu entry in the admin, guarded by the Trek session: anyone who is not an administrator gets a 404 rather than a redirect. Two specs come with it (dashboard access per role, and the mail delivery job). Because GoodJob discards a failed job, every deliver_later email now goes through an installed MailDeliveryJob that retries five times with a growing delay, and config/database.yml reserves connections for the job threads on top of the web threads (GOOD_JOB_MAX_THREADS, which GoodJob defaults to RAILS_MAX_THREADS). See Background jobs. Upgrade note: apps installed before this version can run rails g trek:install:good_job — it is idempotent and skips what is already in place — then add the dashboard entry to app/views/layouts/admin.html.slim (an item with text: t("admin.jobs"), href: "/admin/good_job", allowed_to: Current.user.admin? in the settings section) and run the migration it generates. Apps already on GoodJob only need the Gemfile left alone and their config/initializers/good_job.rb compared with the generated one; apps still on Solid Queue must drop the queue database from their deploy target as well.
  • Dashboard tiles (Trek::Dashboard::TileComponent) opening in a new tab (target: "_blank") show an external link icon after their text, and a tile's color is the accent of what it holds (data-accent-color), which can use var(--accent-*).
  • Public accounts: rails g trek:public_accounts Customer gives the site's or the mobile app's users (customers, members…) a model of their own, apart from the admin's User, with its own table, sessions and sign-in, so nothing public can reach a staff account. They sign in either with a code emailed to them, which creates the account the first time, or with --sign-in=password with a password, the email confirmed and the password reset by a link; a website gets pages and a cookie, --api JSON endpoints and a JWT naming a session, so it can be revoked, and --model-only neither, for an app whose sign-in is its own. See Public accounts.
  • Trek::Users::SignInLockout sends its email through sign_in_lockout_mailer ("AdminMailer" by default), and Trek::PurgeExpiredSessionsJob takes the session model to purge (User::Session by default), for public accounts.

Changed ​

  • Trek requires the prosemirror gem 1.0 or later (was 0.7.1), and works with 1.1: rendering content (formatted_content) raised ArgumentError with it. Trek::Content::Document#to_html takes 1.1's heading_ids: true.
  • rails g trek:install ends by listing the generators it doesn't run, to run as the app needs them: trek:scaffold, trek:taxonomies, trek:audit_log, trek:public_accounts and trek:scalingo.
  • trek:install:audit_log and trek:install:scalingo are now trek:audit_log and trek:scalingo, as trek:install doesn't run them: trek:install:* is left to its own steps. The former names still work, with a deprecation warning, until Trek 3. Upgrade note: update the generator names in your docs and scripts.
  • trek:install:ruby now pins Ruby 4.0.9 (was 4.0.7), and the CI images (repo + generated .gitlab-ci.yml template) use the matching 4.0.9 tag. Upgrade note: existing apps can set 4.0.9 in .ruby-version and bump the ci-ruby image tag in .gitlab-ci.yml.
  • Asking every user for a new password goes through its own controller, Admin::PasswordRenewalsController#create (POST /admin/password_renewal), instead of a require_password_renewal action of Admin::UsersController. Upgrade note: copy app/controllers/admin/password_renewals_controller.rb from Trek's templates (lib/generators/trek/templates/controllers/admin/), add resource :password_renewal, only: :create to the admin namespace of config/routes.rb, remove post :require_password_renewal, on: :collection from resources :users and the require_password_renewal action from Admin::UsersController, and point the button of app/views/admin/users/index.html.slim to [:admin, :password_renewal].
  • The new sign-in email (AdminMailer#new_sign_in) gives the IP address's host name next to it, as the active sessions do, looked up when the email is sent. Upgrade note: to get it, copy new_sign_in from Trek's lib/generators/trek/templates/mailers/admin_mailer.rb, and replace @ip with @address in app/views/admin_mailer/new_sign_in.*.
  • Content is cleaned before it's saved (Trek::Contentable): empty paragraphs, empty list items and lists left empty are removed, as rendering already skipped them, so the stored JSON, content_text and searches no longer carry them. The document stays one the content editor opens, and node types or attributes the schema doesn't know are kept. The editor's JSON string is now stored as a document, which Fragment.search can search. Upgrade note: existing content is cleaned the next time it's saved; empty paragraphs authors used for spacing are removed for good. Apps that pruned empty paragraphs before saving can drop that code.
  • The content editor's panels label their fields more explicitly: « Largeur d'affichage », « Légende de l'image », « Lien vers », « Ouvrir ce lien dans… », « Texte du lien », « Texte du bouton », and « Dépasse sur les côtés » for the wider image size (in English: "Display width", "Image caption", "Link to", "Open this link in…", "Link text", "Button text", "Extends past the sides").
  • Trek is now developed and tested against prosemirror 1.0, its first stable release, which imports HTML and Markdown about a quarter faster. Trek already accepted it (>= 0.7.1, < 2), and doesn't use what 1.0 renames. Upgrade note: an app calling Schema#parse_html, #parse_markdown or #parse_text, or Prosemirror::TextAlign, renames them #from_html, #from_markdown, #from_text and Prosemirror::Extensions::TextAlign when it runs bundle update prosemirror (see its changelog).
  • Failed form submissions answer with :unprocessable_content (422), Rack's name for it since 3.1, instead of the deprecated :unprocessable_entity, in Trek::ResourceController, the sign-in controller and the generated specs, which printed a deprecation warning in every run. Upgrade note: replace :unprocessable_entity with :unprocessable_content in the app's own controllers and specs to silence the warning.
  • Security: the content editor runs on Tiptap 3 (was 2), which fixes mergeAttributes() turning an own __proto__ key into DOM attributes (GHSA-cp6q-959q-f8rh). Its floating menu uses Floating UI instead of Tippy. The editor behaves as before: no underline, and no empty paragraph added after a final block. Upgrade note: run yarn up trek, and update the app's own Tiptap extensions, if any, to Tiptap 3 (see Upgrade from Tiptap v2).
  • Trek::Form::ImageFieldComponent (f.image_field) shows two icons over its thumbnail, on hover and focus, opening the original image in a new tab or downloading it, instead of a « See original » button under « Replace » and « Remove », which wrapped onto two lines in French.

Fixed ​

  • Trek::TypographyFormatter broke content saved from the content editor: given the editor's JSON string, it formatted it as plain text, and the non-breaking spaces it added before colons made the JSON invalid. It now parses the document, and formats its text nodes. Upgrade note: an app that added :content to format_attributes may hold records whose content no longer parses (formatted_content raises JSON::ParserError), which the editor can't open either: restore them from a backup.
  • Trek::TypographyFormatter no longer adds a space before a colon, semicolon, !, ?, %, € or $ inside a word: times (10:30), URLs (https://…?a=1) and ratios (3:1) are left alone, and one space goes before a group of signs (Quoi ?!). Apostrophes and the space after « now work next to accented letters (l’été, « Été »). Spaces earlier versions put inside a word (10 :30, https ://) are removed. Upgrade note: formatted titles and content are repaired the next time they're saved.
  • content_text ran the content's paragraphs and lines together (Line oneLine two), which shows in the excerpts of the admin's fragments index: it now separates blocks with a blank line, and hard breaks with a newline (Prosemirror::Document#to_text).
  • Reordering a list in the admin (Trek::Reorderable) could leave records out of place: it saved them one at a time while acts_as_list, and closure_tree for the trees ordered with numeric_order, renumbered the other records at each save. A list numbered from 1, acts_as_list's default, lost a record on most moves, as the list posts positions from 0. The posted order is now applied as a whole: in an acts_as_list_no_update block, from the list's top_of_list, or with closure_tree's prepend_sibling and append_sibling for a tree. Records missing from the list (deleted meanwhile, or left out by reorderable_objects) are skipped. Upgrade note: you can now drop your own reorder actions written to work around it.
  • Adding a call to action from the content editor failed with NoMethodError: undefined method 'color': the prompt panel's response read a color that Trek::ContentEditor::Prompt never had, and the panel has no field for. Prompts no longer have a color: the editor's block, the panel and Trek::Content::PromptsBlock drop it, and the site's link loses its data-color. A color already stored in content stays in the JSON, unrendered. Upgrade note: an app styling prompts with [data-color], or reading @color in its own PromptsBlock, can drop that code.
  • Security: the content editor showed image and call-to-action blocks by writing their attributes into HTML: an alternative text, caption or button text containing " or < broke the block, or injected markup into the admin. They are now built as DOM nodes, their texts as text.
  • The content editor's toolbar buttons show the styles and the block where the cursor is right after a click: the bold, italic and heading buttons marked their icon instead of the button until the cursor moved.

[2.5.0] - 2026-10-25 ​

Changed ​

  • Security: rich text is rendered with the prosemirror gem instead of prosemirror_to_html, which had two XSS advisories: HTML is escaped as it is built, attributes are typed, and links and images only allow http, https, mailto, tel and relative URLs. The content editor's image and prompt blocks render out of the box (Trek::Content::ImageBlock, Trek::Content::PromptsBlock), empty list items and lists are skipped like empty paragraphs, invalid content is reported to Rails.error, with the record holding it in the context (trek_content: "Page#12", a tag in AppSignal), and the records that a page's links point to load at once, with one query per model. See Rich text rendering. Upgrade note: the Prosemirror model and the ProsemirrorToHtml formatters are removed: call formatted_content, or Trek::Content.schema.parse(content).to_html, instead of Prosemirror.new.format_content, and rewrite the app's ProsemirrorToHtml nodes as node types registered in Trek::Content.schema (see Custom blocks).
  • Trek requires Rails 8.0 or later (was 6.0) and Ruby 3.3 (was 3.1: Rails 8 needs 3.2, and the prosemirror gem 3.3). Its authentication already relied on Rails 8: has_secure_password's :password_reset token and password_reset_token, without which a password reset fails on Rails 7.2 (KeyError: key not found: :password_reset) and so does a password renewal (NoMethodError). Upgrade note: an app on Rails 7 upgrades Rails before Trek (see the Rails 8.0 upgrade guide); meanwhile, it can declare the token in app/models/user.rb, after has_secure_password: generates_token_for(:password_reset, expires_in: 15.minutes) { password_salt&.last(10) } and def password_reset_token = generate_token_for(:password_reset).
  • Trek is now developed and tested against css_parser 3.3 (was 3.0), which Lookbook, a Trek dependency, brings in: 3.2 and 3.3 fix a ReDoS on an unclosed CSS function (GHSA-84w7-hpvm-rxx2) and a local file read through @import (GHSA-w3mx-4vv9-hjpg). Upgrade note: run bundle update css_parser if the app's Gemfile.lock has an older one.
  • Trek is now developed and tested against crass 1.0.8 (was 1.0.7), which Loofah and rails-html-sanitizer bring in to sanitize CSS: it fixes a CSS sanitizer bypass through bad URLs (GHSA-pq4g-8rxf-4g29). Upgrade note: run bundle update crass if the app's Gemfile.lock has an older one.
  • The generators no longer print RuboCop's report when they autocorrect the files they inject code into, only an autocorrect line per file; an offense RuboCop can't correct is still shown, and stops the install.
  • Trek's own CI is faster: its linters and security checks run in one job (make -k lint security i18n-check, with a make target per tool), the tests no longer wait for them, and the generated app's install keeps its gems and Rails in a cache, one per Ruby version, and is skipped for changes to the docs or the repository's tooling; it no longer installs Trek's own gems and packages first. Nothing changes for host apps.
  • The admin's fragments index shows each fragment's title, its key (namespace.key) and the start of its content, with Trek::EntriesComponent, instead of only its key, and has a search box matching the key, the title or the content's text (Fragment.search). Upgrade note: to get them, copy from Trek's templates (lib/generators/trek/templates/) views/admin/fragments/index.html.slim, the search scope of models/fragment.rb, and controllers/admin/fragments_controller.rb.

Fixed ​

  • Since 0.1.18, rendering rich text that links to a page raised NoMethodError: undefined method 'page_url': page links are turned into URLs with url_for, which needs Trek::PagePathHelper, no longer included. An app's own direct :page route still takes precedence. Upgrade note: you can now drop initializers that include Trek::PagePathHelper in ProsemirrorToHtml::GlobalIdToLinksFormatter.
  • Since 2.4.0, the site's links to pageable records (Trek::Pageable) used their id instead of their page's slug: to_param took the admin IP allowlist middleware, in the stack of every request, for an admin caller.
  • The content editor's link, image and prompt panels opened empty ("Content missing"): the installer routed them to Admin::Panels controllers it didn't generate. It now generates them, subclasses of Trek::Panels::* that serve Trek's panels under the app's routes. The panels' titles, field labels and image sizes are translated, and the link panel no longer needs OpenStruct, which Ruby doesn't load by default since 3.5. Upgrade note: run rails g trek:install:admin and keep only its new app/controllers/admin/panels/*_controller.rb, or copy them from Trek's templates (lib/generators/trek/templates/controllers/admin/panels); you can then drop your own translations of trek.panels.*.form.title, of activemodel.attributes.trek/content_editor/* and of the image sizes, and the ostruct gem if nothing else needs it. Re-run rails g trek:install:model_specs for the new spec/requests/admin/panels_spec.rb.
  • Trek::Reorderable gives the views reorder_path, which the scaffolded index's commented-out reorder form uses, and reorders without Prosopite taking acts_as_list's repeated queries for an N+1. Upgrade note: you can now drop your own helper_method :reorder_path and Prosopite pause around reorder.
  • The admin's lists, the pages' tree and the content editor's image panel raised NoMethodError: undefined method 'derivation_url' on an image when the image uploader doesn't load Shrine's derivation endpoint, as the generated ImageUploader doesn't: they show the image itself instead, as the image field already did (Trek::ThumbnailHelper.thumbnail_url). Upgrade note: you can now drop the derivation endpoint if you enabled it only for these thumbnails.
  • The specs installed by trek:install:model_specs pass in an app whose default locale isn't English: the three examples reading browser descriptions ("Firefox on Windows") use an English-speaking user. Upgrade note: re-run rails g trek:install:model_specs, or drop your own locale workarounds in spec/mailers/admin_mailer_spec.rb, spec/requests/admin/sign_in_notices_spec.rb and spec/requests/admin/active_sessions_spec.rb.
  • trek:scaffold passes the attributes to the model generator as given: it dropped their indexes (slug:string:uniq, isbn:string:index), references' options (author:references{polymorphic}) and decimals' precision (price:decimal{10,2}).
  • The generated Makefile lints every Slim template: unquoted, make's shell expanded app/**/*.slim one directory deep only, so make lint skipped what CI caught. Upgrade note: quote the pattern in your Makefile's lint target: bundle exec slim-lint "app/**/*.slim".
  • trek:install:specs includes the matchers of rspec-html-matchers (have_tag, with_tag), which it adds to the Gemfile, in spec/rails_helper.rb. Upgrade note: add config.include RSpecHtmlMatchers to the RSpec.configure block of spec/rails_helper.rb to use them.
  • Trek::SearchComponent labels its field in the admin's language by default (it was always « Rechercher »), and its label is tied to the field, which has an id of its own: clicking the label focuses the field, and screen readers announce the field by its label.

[2.4.0] - 2026-10-16 ​

Added ​

  • Trek::Form::ImageFieldComponent (f.image_field) shows a « See original » button opening the uploaded image in a new tab.
  • An audit log: rails g trek:install:audit_log installs an AuditLog model recording each mutation in the admin (author, request, target record, status), sign-ins included, and an admin list for the admin role; Trek::AuditLog::PurgeJob deletes rows past 6 months. See Audit log. Upgrade note: an app with its own audit log can drop its copy for Trek's, keeping its audit_logs table (see the docs' migration steps).
  • Trek::Filters exposes filter_params to views, as Trek::Scopes does scope_params.
  • Users can't reuse one of their last 5 passwords (Trek::Users::PasswordHistory, configurable with password_history_size). Upgrade note: copy app/models/user/previous_password.rb (User::PreviousPassword) and the user_previous_passwords migration from Trek's templates (lib/generators/trek/templates/{models,migrations}), run it, add include Trek::Users::PasswordHistory after has_secure_password in app/models/user.rb, and re-run rails g trek:install:model_specs for the new specs.
  • Password expiry and renewal (Trek::Users::PasswordRenewal): an admin can ask every user for a new password after a suspected compromise, and password_expires_after (none by default) makes passwords expire. A user concerned is signed out and sent to the new password form, which states the rules and rates the password as it is typed (Trek::Form::PasswordStrengthComponent). Upgrade note: copy the user_password_renewal migration from Trek's templates (lib/generators/trek/templates/migrations) and run it, add include Trek::Users::PasswordRenewal after has_secure_password in app/models/user.rb, and take the changes to require_authentication in app/controllers/concerns/trek/authentication.rb, the require_password_renewal action of Admin::UsersController, its route (post :require_password_renewal, on: :collection in resources :users), and the admin/users/index and admin/user_password_resets/edit views from Trek's templates.
  • Sessions expire after 48 hours without a request and 30 days after signing in (Trek::Users::SessionLifetime, configurable on User::Session), and users see their active sessions in a tab of their profile, to sign out any other or all of them; an IP address shows its host name on hover. Session activity is written at most every 5 minutes instead of on every request. Upgrade note: copy the user_session_tracking migration from Trek's templates (lib/generators/trek/templates/migrations) and run it; add include Trek::Users::SessionLifetime to app/models/user/session.rb (or app/models/user_session.rb); take Trek's changes to app/controllers/concerns/trek/authentication.rb (find_session_by_cookie, start_new_session_for, track_last_activity), to Admin::UsersController#show and to app/views/admin/users/show.html.slim (with its new _properties and _sessions partials); copy app/controllers/admin/active_sessions_controller.rb from Trek's templates and add its route (resources :active_sessions, only: :destroy do delete :others, action: :destroy_others, on: :collection end in the admin namespace); schedule Trek::PurgeExpiredSessionsJob daily.
  • Users see their previous sign-in (date, browser, IP address) on their profile's Active sessions tab, and a sign-in from a browser that never signed in as them emails them (Trek::Users::SignIns, new_sign_in_email). Upgrade note: copy the user_sign_ins migration and the admin_mailer/new_sign_in views from Trek's templates and run the migration; add include Trek::Users::SignIns to app/models/user.rb; take Trek's changes to app/controllers/concerns/trek/authentication.rb (record_sign_in_of and the methods after it), app/controllers/admin/user_sessions_controller.rb (create, sign_in_notice), app/mailers/admin_mailer.rb (new_sign_in) and app/views/admin/users/_sessions.html.slim.
  • Trek::SectionComponent: a titled section of a page (an h2 labelling it), with an optional hint under the heading.
  • NotDisposableValidator refuses email addresses from disposable providers, on any model: validates :email, not_disposable: true. The domains come from the valid_email2 gem, which keeps them up to date. See Validators.
  • Failed sign-ins are throttled per account and per IP address over a longer window: after 5 failures in a row, an account's sign-in locks for 1 minute, doubled on each further failure up to 1 hour, and its user is emailed (Trek::Users::SignInLockout); an IP address gets 20 failures an hour, whatever the account. A locked account gets the same answer as a wrong password, and a failed or throttled sign-in now renders the form (422, 429) instead of redirecting, so the audit log counts it as a failure, on the account tried. See Failed sign-ins, including why Rails.cache must be shared between servers. Upgrade note: copy the user_sign_in_lockout migration and the admin_mailer/sign_in_locked views from Trek's templates and run the migration; add include Trek::Users::SignInLockout to app/models/user.rb; take Trek's app/controllers/admin/user_sessions_controller.rb and the sign_in_locked method of app/mailers/admin_mailer.rb.
  • Passkeys: users add and delete passkeys on a new tab of their profile, which names each after its password manager, and sign in with one instead of their password, from a button on the sign-in form or the email field's autofill (Trek::Users::Passkeys, User::Passkey, through the webauthn gem). The password keeps working. See Passkeys. Upgrade note: copy app/models/user/passkey.rb and the user_passkeys migration from Trek's templates and run it; add include Trek::Users::Passkeys to app/models/user.rb; copy app/controllers/admin/passkeys_controller.rb and app/controllers/admin/passkey_sessions_controller.rb, and add their routes in the admin namespace (resources :passkeys, only: %i[create destroy] do get :options, on: :collection end and resource :passkey_session, only: :create do get :options end); take Trek's changes to app/views/admin/user_sessions/new.html.slim, app/views/admin/users/show.html.slim (with the new _passkeys partial) and Admin::UsersController#show; with the audit log, add Admin::PasskeySessionsController to config/initializers/audit_log.rb.
  • A Security and compliance docs page maps Trek's security measures to ISO 27001's controls and NIS2's measures, and lists what stays with the app, its hosting and the organization.
  • The Security and compliance page lists the cryptography a generated app uses, and how each algorithm stands against a quantum computer.
  • The generated GitLab CI has an sbom job, commented out, making a Software Bill of Materials (CycloneDX, with cdxgen) of the app's gems and npm packages, for a client who asks for one. See Software Bill of Materials. Upgrade note: when a client asks for an SBOM, copy the sbom job from Trek's lib/generators/trek/templates/.gitlab-ci.yml into the app's .gitlab-ci.yml, uncommented.
  • ADMIN_ALLOWED_IPS restricts the admin to IP addresses and ranges (e.g. 203.0.113.0/24, 2001:db8::/32): from anywhere else, /admin and everything below it answer like an unknown URL, a 404. Off when unset. Behind a CDN, list its ranges in trusted_proxies first. See Restricting the admin to IP addresses.
  • Security headers: rails g trek:install:security_headers, which trek:install runs, sets up a Content Security Policy, report-only at first, allowing no inline script, and a Permissions-Policy header turning off the browser features the app doesn't use (camera, microphone, geolocation…). Violations are reported to /csp-reports (Trek::CspReportsController), which sends them to AppSignal, one incident per directive and blocked origin, and to the log, throttled against floods. It replaces Rails' commented-out example, but leaves an app's own policy. HSTS, from force_ssl, already covers subdomains; its preload option is documented. See Security headers. Upgrade note: run rails g trek:install:security_headers, browse the app with the console open and watch AppSignal's csp namespace, add the front office's third-party hosts, then set content_security_policy_report_only to false.

Changed ​

  • Trek pins Node 24.22.0 (was 24.18.0) in .node-version. Upgrade note: existing apps can set 24.22.0 in their .node-version.
  • trek:install:yarn installs Yarn 4.18.1 (was 4.18.0), and Trek's own development moves to it from Yarn 3, with its defaults: no install scripts, and no package published less than a day ago. Upgrade note: existing apps can upgrade with yarn set version 4.18.1 followed by yarn install.
  • Passwords follow the ANSSI's recommendations for a privileged access (Trek::Users::PasswordPolicy, included by Trek::Users::Validations): at least 12 characters, with a digit and a symbol below 20, and neither a common password, a keyboard or number sequence, nor the user's name or email; a Have I Been Pwned check is opt-in. Existing passwords keep working: the rules apply when one is set. Upgrade note: the rules are configurable in app/models/user.rb (password_min_length, passphrase_length, password_check_pwned); update seeds and specs whose passwords no longer pass.
  • The generated signed-in browser model is User::Session (user.sessions), in the same user_sessions table. Apps generated before keep working with UserSession. Upgrade note: to follow, move app/models/user_session.rb to app/models/user/session.rb as class User::Session, replace has_many :user_sessions with has_many :sessions in app/models/user.rb, and update UserSession.find_by and user.user_sessions in app/controllers/concerns/trek/authentication.rb and app/controllers/admin/user_password_resets_controller.rb; no migration is needed.
  • The admin's Inter font comes with Trek's npm package (@fontsource/inter), bundled into admin.css, instead of Google Fonts: visitors' browsers no longer contact Google, and the Content Security Policy needs no Google host. Upgrade note: remove admin_webfonts_link_tag from app/helpers/application_helper.rb and app/views/layouts/admin.html.slim, and the Google Fonts link tags from both admin layouts (admin.html.slim, admin/auth.html.slim); esbuild.config.js must have the .woff and .woff2 file loaders, as Trek's has.

Deprecated ​

  • Trek::UserSession, the sign-in form object Trek 2 no longer uses, warns and will be removed in Trek 3. Upgrade note: in a sign-in controller still building one, pass the params to User.authenticate_by(email:, password:) instead, as Trek's generated Admin::UserSessionsController does.

Changed ​

  • The install generators no longer add esbuild-plugin-import-glob, postcss-cli, postcss-nesting and postcss-url, which no generated config uses. Upgrade note: you can drop them from the app's package.json (yarn remove …), except those its own postcss.config.js, esbuild.config.js or package.json scripts still use (e.g. postcss-url to rebase the URLs of @import-ed CSS).
  • rails g trek:install runs Trek's own generators in its process instead of starting rails generate for each, which saves about two minutes.

Fixed ​

  • The generated .gitlab-ci.yml's commented-out deploy jobs point at example.com instead of a real site.
  • A tag's key is unique within its category, as Tag validates it and the docs said: the generated tags migration's unique index on key alone refused the same key in two categories with ActiveRecord::RecordNotUnique. A tag category with tags can no longer be deleted, which left them without a category, and the admin now says why a deletion is refused instead of reporting it done. Upgrade note: in app/models/tag_category.rb, replace dependent: :nullify with dependent: :restrict_with_error. Check that no category has two tags with the same key, nor two tags without a category, then generate a migration (bin/rails g migration TagKeyUniquePerCategory) with remove_index :tags, name: "index_tags_on_key", if_exists: true (apps generated with Trek 0.1.9 or later have it), add_index :tags, %i[category_id key], unique: true and add_index :tags, :key, unique: true, where: "category_id IS NULL", name: "index_tags_on_key_without_category". Copy spec/models/tag_spec.rb from Trek's templates for the specs.
  • The notice after signing in is in the user's language, rather than the app's default one.
  • The password reset request no longer reveals whether an email has an account: it answers every email the same way, and queues Admin::PasswordResetJob for each, which emails privileged users only, so the response takes as long either way. Upgrade note: copy app/jobs/admin/password_reset_job.rb from Trek's templates (lib/generators/trek/templates/jobs/admin), and in Admin::UserPasswordResetsController#create, replace the user lookup and deliver_later with Admin::PasswordResetJob.perform_later(params[:email].to_s).
  • Icons drawn in black (stroke="#000000", such as the content editor's toolbar icons) stayed black in the dark theme, and in any colored context: since the sprite was sanitized in two passes, black was turned into currentColor before the icon's paths were inserted, so it never reached them. The substitution now runs once the paths are in.
  • trek:install:esbuild installed esbuild 0.15, too old for the esbuild.config.js it copies: yarn build --watch (and bin/dev) crashed with esbuild.context is not a function. It now installs esbuild 0.28, as Trek does. Upgrade note: in an app generated by an earlier Trek 2, run yarn add --dev esbuild@^0.28.0.
  • The generated invitation and password reset emails formatted the link's expiry (« 7 days ») in the app's default locale instead of the recipient's. rails g trek:install:model_specs now also installs mailer specs covering it. Upgrade note: in app/mailers/admin_mailer.rb and app/mailers/admin/passwords_mailer.rb, move the @expiration_delay = … line inside the I18n.with_locale block, before mail; re-run rails g trek:install:model_specs for the new specs.

[2.3.2] - 2026-10-02 ​

Fixed ​

  • Security: Trek::Scopes#apply_scopes called any name found in params[:scopes] that the relation answered to, not only the controller's available_scopes, which let a request run unintended methods on the listed records. Only the names in available_scopes are applied now; others are ignored. Upgrade note: update Trek as soon as possible. A controller passing apply_scopes a scope name it does not list in available_scopes must add it there.
  • The generated admin sign-in, password reset request and new password pages rendered their actions with the default aspect of Trek::Form::ActionsComponent, whose sticky action bar (top margin, padding, background) is meant for edit forms: inside those small cards, the button and the « forgotten password » link sat in a padded bar. They now use the seamless aspect. Upgrade note: in apps generated before this version, pass aspect: :seamless to Trek::Form::ActionsComponent.new in app/views/admin/user_sessions/new.html.slim and app/views/admin/user_password_resets/{new,edit}.html.slim.

[2.3.1] - 2026-10-02 ​

Changed ​

  • Trek::Filters only filters on the columns a controller lists in its new available_filters (empty by default), read through strong params (filter_params): other keys of params[:filters], and values that aren't a single value, are ignored. Until now any key became a where condition, so a request could filter an index on columns it was never meant to expose. Upgrade note: in each controller including Trek::Filters, define available_filters returning the filtered columns (e.g. def available_filters = %i[country_code]), or its filters stop applying.
  • Trek::Users::Validations stores emails stripped and in lower case (sign-in and password resets now ignore case), checks them with email_validator's strict mode, and refuses a spaces-only password; the password reset form refuses an empty password instead of pretending to change it. Upgrade note: resolve users whose emails differ only by case (User.group("lower(email)").having("count(*) > 1").count), then downcase the rest (User.find_each { |user| user.update_column(:email, user.email.strip.downcase) }); in Admin::UserPasswordResetsController#update, redirect back with an error when params[:password] is blank (see Trek's template); re-run rails g trek:install:model_specs for the new specs.

Fixed ​

  • The header's theme switcher now hovers like the links next to it, and the whole pill is clickable, not just the icon. Trek::DropdownComponent's trigger inherits its context's color, and a new trigger_classnames: option styles the trigger button. Upgrade note: if you reuse Trek::DropdownComponent, move trigger styling (padding, hover) from classnames: to trigger_classnames:.

[2.3.0] - 2026-10-01 ​

Added ​

  • More admin request specs from rails g trek:install:model_specs, run by Trek's own suite as well: fragments_spec.rb (index, new, create, duplicate key, edit, update, destroy), users_spec.rb (an admin lists privileged users only, creates a user and sends the invitation, changes a role, deletes a user; an editor is sent back from the users list and other profiles, gets a 403 in JSON, edits their own profile but not their role, switches the admin theme) and user_password_resets_spec.rb (the reset request answers known and unknown emails alike and only mails privileged users, reset and invitation links open the form, an invalid link goes back to the request form, a new password signs the user out everywhere, a password under 8 characters is refused). A db/seeds/setup/fragments.rb makes fragments available to seeds and specs. Upgrade note: re-run rails g trek:install:model_specs to add them.
  • Trek::Dashboard::TileComponent accepts method: and data:. With method: :post (or :delete…) the tile issues that request through Turbo, as a data-turbo-method link like Trek::ButtonComponent's to: links, so a dashboard shortcut can create a record in one click (e.g. a pre-filled draft) rather than only open a form; data: adds attributes to the link, such as turbo_confirm:. Tiles without them still render a plain GET link. Upgrade note: you can now turn a tile pointing to a form into a direct action, and drop buttons kept elsewhere only because a tile couldn't POST.
  • Policy specs for host apps, run by Trek's own suite as well: rails g trek:install:model_specs installs spec/policies/admin/ specs for the page, fragment, user, preview and external link policies (what each role may do, which attributes it may change, which records it sees), and rails g trek:taxonomies installs the tag and tag category ones (a tag or category in use can't be deleted). Upgrade note: re-run rails g trek:install:model_specs (and rails g trek:taxonomies if you use tags) to add them.
  • Sound uploads from the admin: Trek::Form::SoundFieldComponent (f.sound_field) posted to /admin/upload/audio, which no generated app routed or handled. trek:install:uploaders now installs an AudioUploader (MP3, AAC/M4A, Ogg, WAV, WebM, FLAC, up to the field's 200 MB), the generated Admin::UploadsController gains an audio action, and trek:install:admin:routes adds post "upload/audio", to: "uploads#audio" to the admin namespace. Attach it with include AudioUploader::Attachment(:audio) on a record with an audio_data jsonb column. Upgrade note: copy app/uploaders/audio_uploader.rb from Trek (or re-run rails g trek:install:uploaders), add the audio action to app/controllers/admin/uploads_controller.rb (set_rack_response AudioUploader.upload_response(:cache, request.env)), and add post "upload/audio", to: "uploads#audio" inside namespace :admin in config/routes.rb; you can then drop a hand-written audio uploader, action and route that did the same.
  • rails g trek:install:model_specs also installs request specs for the admin, run by Trek's own suite as well: spec/requests/admin/user_sessions_spec.rb (sign-in form, signing in and out, refusing a wrong password or a user who isn't privileged), pages_spec.rb (index, new, create, invalid create, edit, update, destroy as an editor), uploads_spec.rb (the image upload endpoint) and the existing n_plus_one_spec.rb, with an image fixture in spec/fixtures/files. The Oaken users setup gains a users.password helper to sign in with. Upgrade note: re-run rails g trek:install:model_specs to add them, then bin/rails spec.
  • rails g trek:install:model_specs, a new installer step (also runnable alone) giving host apps specs for the models Trek installs: spec/models/page_spec.rb (slugs, full paths through the page tree, path history when a page moves) and spec/models/user_spec.rb (validations, roles, privileged, to_s). Their test data uses Oaken seed scripts rather than factories: db/seeds/setup/users.rb sets defaults for users.create/users.new (a unique email, a valid password), and db/seeds/test/cases/page_tree.rb lays out a named page tree the Page spec loads with seed "cases/page_tree". The step adds the oaken gem (in every group, since db/seeds.rb now calls Oaken.seed, which also makes these seeds available to bin/rails db:seed) and require "oaken/rspec_setup" in spec/rails_helper.rb, which seeds the database before the suite. The specs check Trek's behaviour inside the app, so a customization that breaks it fails the app's own suite; edit or remove the ones covering behaviour you deliberately changed. Trek's own suite runs the very same files against its dummy app, so they stay green with each Trek release. Oaken needs Ruby 3.2+. Upgrade note: run rails g trek:install:model_specs to add them (it skips the gem, the db/seeds.rb line and the rails_helper line when already there, and asks before overwriting an existing file), then bundle exec rspec spec/models. oaken/rspec_setup truncates every table and runs db/seeds.rb before the suite, so make sure your db/seeds.rb is fine to run in the test environment.
  • The header's theme button is now a working theme switcher. It used to toggle light-theme/dark-theme on the page without saving the choice (a TODO logged it to the console), so the theme reset on the next page load. It now opens a menu offering Auto, Light and Dark, applies the choice at once and saves it to the user's theme through PATCH /admin/users/theme. The action answers with a turbo stream that refreshes the header icon. Logged-out pages (login, password reset) now render with auto-theme instead of light-theme. Auto follows the system setting (prefers-color-scheme) without JavaScript: a small PostCSS plugin in postcss.config.js copies Radix's .dark-theme variables into a @media (prefers-color-scheme: dark) block scoped to .auto-theme. color-scheme is now set for each theme too, so native controls and scrollbars follow it. The menu is a new Trek::DropdownComponent (a trigger slot plus with_item entries, with keyboard navigation), which host apps can reuse. Upgrade note: in app/controllers/admin/users_controller.rb, replace head :no_content in the theme action with respond_to { |format| format.turbo_stream; format.html { head :no_content } }, and add app/views/admin/users/theme.turbo_stream.slim from Trek's generator templates (or re-run rails g trek:install:admin:users). Without them the theme is still saved, but the header icon only updates on the next page load. For Auto to follow the system setting, copy the autoDarkTheme plugin from Trek's postcss.config.js into yours and add it to plugins right after postcss-import; without it, Auto renders the light theme.
  • phonable_attributes accepts a strictness: option — :possible (the default, unchanged) or :valid. :possible only asks Phonelib whether the digits could form a number for the country; :valid asks whether they fall in a range the regulator has actually assigned. The gap bites on input errors: with a French default country, "0012345678" is possible, so it is accepted and normalized to "+12345678" — the stray leading zero read as an international prefix, the number silently rehomed to country code +1, and the original input gone since normalization happens on save. Apps that intend to call or text the numbers they collect want phonable_attributes :phone, strictness: :valid.
  • Plain file inputs are now styled: inside a Trek::Form::GroupComponent (f.group), a standard f.file_field gets its native ::file-selector-button styled like a Trek::ButtonComponent with size: :narrow, using the accent variables so data-accent-color applies as on buttons. The browser's built-in "file chosen" feedback is kept and no JS is involved — useful for one-off uploads (e.g. an .xlsx import) next to Trek's styled fields. The Shrine-backed image_field/sound_field are unaffected (their file inputs are visually hidden). Upgrade note: you can now drop custom ::file-selector-button styles that mirrored Trek's buttons for plain file inputs.
  • The confirm dialog (Trek::DialogComponent wired to Turbo.config.forms.confirm) now takes its title, confirm button label and button color per action from the confirming element's data attributes — data-turbo-confirm-title, data-turbo-confirm-button and data-turbo-confirm-color alongside data-turbo-confirm — falling back to the current delete wording ("Are you sure?" / "Delete" / tomato) when absent. A non-destructive confirm (a "Validate" flow, a scheduled job trigger) no longer shows a Delete button, and one page can host a delete confirm and an action confirm side by side. Works on form submit buttons and on data-turbo-method links (the clicked element is remembered, since Turbo only forwards data-turbo-confirm to the form it builds for a link). Upgrade note: you can now drop content_for(:dialog)-style overrides that swapped the whole dialog just to rename the button — pass the data attributes on the confirming button instead.
  • Trek::MenuComponent items accept a target: option forwarded to the link — e.g. s.with_item(text: …, href: …, target: "_blank") for external tools mounted in the admin, like a GoodJob dashboard. A _blank item automatically gets rel="noopener" and the trek/external new-tab icon after its text, as the toast link already did. Trek::Dashboard::TileComponent (which already accepted target:/rel:) now also defaults rel to noopener when target is _blank. Upgrade note: you can now drop initializer patches that reopened ItemComponent to add a target option.
  • Trek::ButtonComponent accepts a name: option, rendered on the plain-button branch (paired with value:) so a type: :submit button can carry a param — needed when a form has two submit actions with different outcomes (e.g. a "save" button next to a "save then preview" one submitting then_preview). Upgrade note: you can now drop initializer patches that reopened the component to add a name option.
  • rails g trek:install:appsignal, a new installer step (also runnable alone) that sets up AppSignal error tracking and performance monitoring, until now wired by hand in every app: it adds the appsignal gem, injects APPSIGNAL_PUSH_API_KEY into the Rails credentials (as the Postmark generator does with its API token — bundle exec appsignal install would write the key in plain text and needs a terminal), and writes a config/appsignal.rb that reads the key from the credentials, activates the agent in production as soon as a key exists, and ignores the /healthcheck action. It also wraps the boot in config.ru so a broken initializer or a missing credential is reported, and installs an AppsignalContext controller concern that sets the admin/web namespace and tags transactions with the acting user — included in ApplicationController and, through an initializer, in Trek::BaseController. APPSIGNAL_APP_ENV is added to .env.example; the rescued Postmark delivery in ApplicationMailer now reports through Rails.error rather than only logging, which keeps app code independent of the monitoring vendor. Check an install with bundle exec appsignal diagnose. Upgrade note: existing apps can run rails g trek:install:appsignal — it skips the gem and the credentials when AppSignal is already set up, but review the generated config/appsignal.rb against your own (it takes the push API key from the credentials, so drop a plain-text key or an APPSIGNAL_PUSH_API_KEY environment variable you no longer need), and remove your hand-written AppsignalContext equivalent if you have one.
  • bin/rails trek:attachments:promote_cached, a rake task finding the records whose Shrine attachments still point to the temporary cache storage — for every model and attachment, whatever the uploader — and promoting those whose cached file still exists (backed by Trek::CachedAttachments). It prints one line per attachment with its status: promoted, missing when the cached file is gone (it has to be uploaded again), or changed when the record changed meanwhile; DRY_RUN=1 only lists them, as promotable. Documented in the uploaders reference.

Changed ​

  • Trek's dummy app now generates and serves the whole admin (authentication, authorization, the admin generator's controllers and layout, routes, the pages, fragments, users, sessions and password-reset panels, mailers), so request and component specs run against the real admin. Line coverage goes from 49% to 73%; the minimum is raised to 72%.
  • Every Trek generator is now run by Trek's own suite (spec/trek/generators_spec.rb): on a fresh rails new app set up as in the Getting started guide, then a second time to check that re-running it changes nothing (host apps re-run generators when upgrading). Shell-outs (bundle, yarn, migrations, rubocop) are recorded rather than run; the few other gems' generators whose output a later step edits (rails g model, action_policy:install, mobility:install, rspec:install) run for real, in-process. trek:admin:user has its own spec against the dummy app's database. Line coverage goes from 35% to 49%; the minimum is raised to 49%.
  • trek:install:ruby now pins Ruby 4.0.7 (was 4.0.6), and the CI images (repo + generated .gitlab-ci.yml template) use the matching 4.0.7 tag. Upgrade note: existing apps can set 4.0.7 in .ruby-version and bump the ci-ruby image tag in .gitlab-ci.yml.
  • Trek's own specs now run in random order (--seed replays an order, --bisect finds the spec leaking state), and no longer depend on it: page_path_helper_spec switched the default locale to French inside I18n.with_locale, which left I18n.locale set to :fr for the specs that ran after it, so localization_spec and i18n_helper_spec failed under some orders.
  • Trek's own suite can now test code that needs Rails and a database. Specs that require "rails_helper" boot spec/dummy, a minimal host app on PostgreSQL. Its models, migrations and Shrine initializer are not copied by hand: they are produced on each boot by running Trek's own generator steps (trek:install:page, :user, :authentication, :fragment…), so they cannot drift from what rails g trek:install gives a real app, and a spec fails when a model or migration template is added without the suite generating it. First User and Page model specs come with it. Line coverage goes from 9% to 35%, most of it from the app's code now being loaded; the minimum is raised to 34%.
  • Trek's own spec suite runs in CI again, in a tests job that had been commented out — so nothing flagged that spec/helpers/trek/page_path_helper_spec.rb required a rails_helper the repository does not have, which stopped the whole suite from loading. That spec is rewritten in the suite's isolated style (no Rails app, stubbed routes). COVERAGE=1 turns on SimpleCov, which reports line and branch coverage over every Ruby file in app/ and lib/ (generator templates excluded), writes a Cobertura report for GitLab's merge-request diff coverage, and fails the run below a minimum line coverage, currently 9%, to be raised as coverage grows.
  • Trek is now developed and tested against Shrine 3.10 (was 3.6). The gemspec constraint stays ~> 3.0, so this changes nothing for host apps by itself — they already resolve 3.10 on Ruby ≥ 3.2 — but Trek's own suite and the generated app in CI no longer run against a version nobody installs. Worth picking up: 3.8 fixes a path traversal outside the storage directory in the file_system storage (the development and test storage), 3.7 stops column from failing on an empty string, 3.9 fixes remove_invalid leaving the attribute set after an invalid file is deassigned and stops the activerecord plugin from deleting the original attachment of a duplicated record. Shrine 3.7 dropped Ruby < 3.2, so an app still on Ruby 3.1 keeps resolving 3.6. Upgrade note: run bundle update shrine (nothing else to change).

Fixed ​

  • A signed-in user opening what their role doesn't allow (an editor on the users panel, or on another user's profile) got a 500 error page: ActionPolicy::Unauthorized was raised and handled nowhere. Trek::Authorization now rescues it: back to the previous page, or to the dashboard, with a translated alert (notices.unauthorized); Turbo Stream and JSON requests get a 403.
  • The new fragment form (and any fragment saved without content) raised undefined method '[]' for nil: the generated fragments migration created content as a jsonb column without the {} default its translated title has, and Mobility's jsonb backend reads a translation as column[locale]. The migration now defaults content to {}. Upgrade note: in apps generated before this version, add a migration with change_column_default :fragments, :content, from: nil, to: {} and up_only { execute "UPDATE fragments SET content = '{}' WHERE content IS NULL" }.
  • The generated Admin::ExternalLinkPolicy scoped external links for non-privileged users with relation.where(user:), a leftover from an example: external_links has no user column, so applying the scope raised a SQL error. Non-privileged users now get none, as in Trek's other policies. Upgrade note: in app/policies/admin/external_link_policy.rb, replace relation.where(user:) with relation.none.
  • With rails g trek:taxonomies, asking whether a tag could be deleted (Admin::TagPolicy#destroy?) raised HasManyThroughAssociationPolymorphicSourceError: it called Tag#taggables, a has_many :taggables, through: :taggings that Rails can't build over the polymorphic Tagging#taggable. The policy now checks taggings, and Tag#taggables is a method returning the tagged records. Upgrade note: in app/models/tag.rb, replace has_many :taggables, through: :taggings with def taggables = taggings.includes(:taggable).map(&:taggable), and in app/policies/admin/tag_policy.rb, replace record.taggables.empty? with record.taggings.empty?.
  • The docs and README still described Sorcery as Trek's authentication, and the User reference listed its crypted_password column. Trek authenticates with Rails' has_secure_password and database-backed UserSession records; the Getting started guide, the introduction, the home page, the customization guide, the installer and User references now say so, and the User columns table lists password_digest and last_activity_at. The README's ClosureTree link, which pointed to Sorcery, now points to ClosureTree.
  • Deleting a page (or a page version) raised NameError: Missing model class Section for the Page#sections association: Trek::Sectionable, included in the generated Page and PageVersion, declared has_many :sections, dependent: :destroy, but no generator creates a Section model. The association is now declared only when the app defines one, so apps with their own sections keep them.
  • Editing a page raised undefined method 'page_path' for an instance of Trek::HeaderComponent: the admin header's "See on website" link called page_path, which components don't include. The header now includes Trek::PagePathHelper, and leaves the link out when the app has no front-office route for the page (a freshly generated app has none), instead of raising.
  • Image uploads from the admin returned 404: trek:install:admin:routes wrote post "upload", to: "uploads#image", namespace: :admin at the top of the routes (the namespace: ended up inside the route string), while image fields post to /admin/upload/image. It now writes post "upload/image", to: "uploads#image" inside namespace :admin. Upgrade note: in config/routes.rb, replace post "upload", to: "uploads#image", namespace: :admin with post "upload/image", to: "uploads#image" inside your namespace :admin do … end block.
  • Trek's components use Turbo::FramesHelper, but the gemspec didn't declare turbo-rails: it only worked because rails new adds it by default. Trek now depends on turbo-rails (>= 1.0) and requires it.
  • Trek::Users::Validations only enforced the 8-character password minimum when a user was created: the check for a later change looked at crypted_password, a column from Trek's former authentication library that has_secure_password users don't have, so an existing user could set a 1-character password (e.g. through the password reset form). It now also applies whenever password_digest changes, and still skips saves that leave the password alone. The generated spec/models/user_spec.rb covers both cases. Upgrade note: nothing to change; to get the new examples in an existing app, re-run rails g trek:install:model_specs and accept overwriting spec/models/user_spec.rb (or copy the two examples).
  • rails g trek:upgrade:v2 left two blank lines in .yarnrc.yml where it removed the git.etamin.studio registry, right before the npmPreapprovedPackages block it adds, so running it again changed the file. It now leaves a single one.
  • rails g trek:scaffold wrote an invalid config/locales/models/<model>.en.yml: the attribute labels ran together on one line (name: Name description: Description), so Rails failed to load the translations (Psych::SyntaxError: mapping values are not allowed in this context) and every page rendering a translation raised. Each attribute is on its own line again. Trek's CI now generates its test app the way the Getting started guide does (rails new -j esbuild) and runs that app's own suite (bin/rails spec) after trek:install and trek:scaffold, which is how this surfaced. Upgrade note: in models scaffolded before this version, put each attribute of config/locales/models/*.en.yml on its own line.
  • Freshly generated apps got a broken db/schema.rb: they resolve json 3, whose JSON.parse takes its options as keywords only, while ActiveSupport (up to 8.1.3.1, the latest release) passes them positionally. The schema dumper then fails on every jsonb column with a default and writes # Could not dump table "pages" because of following ArgumentError in place of pages, page_paths, fragments and menu_nodes, so the test database, loaded from schema.rb, lacks those tables and the app's specs cannot run. Trek now requires json < 3 until Rails supports json 3, and its suite dumps the generated schema to catch a regression. Upgrade note: run bundle update trek (it moves json back to 2.x if you had 3), then bin/rails db:schema:dump and check that db/schema.rb has no "Could not dump" comment left.
  • Three generated migrations tripped RuboCop in a freshly generated app: trek_tag_categories and trek_taggings (from rails g trek:taxonomies) indented create_table by one space, and trek_menu_nodes ended with a trailing blank line. The templates are fixed, and Trek's suite now runs RuboCop on every generated migration. Upgrade note: apps generated before this version can run bundle exec rubocop --autocorrect db/migrate to fix them.
  • Trek::PagePathHelper#page_path (and page_url) raised NoMethodError: undefined method 'shop/product_path' for a page attached to a namespaced pageable: the route name was built from pageable.class.name.underscore, which keeps the / of the namespace. It now uses pageable.model_name.singular_route_key, the key Rails' polymorphic routes use — shop_product_path for Shop::Product, and post_path for an isolated engine's Blog::Post. Non-namespaced pageables are unaffected.
  • Trek::PagePathHelper#page_url returned a path, not a URL, for a page with a named route (a keyed page such as about): that branch always called #{key}_path and ignored the url suffix the other branches honour. It now calls about_url, so mailers, sitemaps and canonical links built with page_url get an absolute URL. Upgrade note: you can now drop workarounds that made page_url absolute for keyed pages (e.g. prefixing request.base_url).
  • trek:install:scalingo pinned the buildpack with BUILDPACK_URL=https://github.com/etaminstudio/trek-buildpack#v0.1.0, and Scalingo resolves a #ref suffix as a branch: the tag was looked up as archive/refs/heads/v0.1.0, which does not exist, and every build failed with fail to download and extract the custom buildpack: Invalid status code: 404 Not Found. The generator and the Deploying to Scalingo guide now point at the release archive, https://github.com/etaminstudio/trek-buildpack/archive/refs/tags/v0.1.0.tar.gz, which Scalingo downloads as is and which stays pinned to the release. Upgrade note: on each Scalingo app, run scalingo --app <app> env-set BUILDPACK_URL=https://github.com/etaminstudio/trek-buildpack/archive/refs/tags/v0.1.0.tar.gz — an app still on the #v0.1.0 form cannot deploy, and nothing surfaces it until the next deploy fails.
  • The English locale gave the content editor's prompt panel its target labels in French ("un nouvel onglet" / "le même onglet"): admin.content_editor.prompt.targets had been copied from trek.fr.yml rather than translated. An English-locale admin inserting a Turbo-frame prompt now reads "a new tab" / "the same tab", as the link panel right above already did.
  • The generated trek_users migration tripped RuboCop's Rails/NotNullColumn on add_column :users, :password_digest, :string, null: false, so make lint (and the lint CI job) failed in a freshly generated app — the installer's own closing make fix already exited non-zero on it. The column is fine as it stands: the preceding trek_create_users migration creates the table empty, so there is nothing to backfill and no default is wanted on a password digest. The line now carries an inline rubocop:disable Rails/NotNullColumn with a comment saying why. Upgrade note: apps generated before this version can add the same disable comment to db/migrate/*_trek_users.rb.
  • The generated .slim-lint.yml listed an AllCops: key (NewCops: disable) among its linters. slim-lint validates linter names since 0.34 ("Add check for invalid keys in config files"), so make lint in a freshly generated app died with No such linter: AllCops. Available: CommentControlStatement, … before checking a single template; Trek's own lockfile pins 0.33, where the unknown key was ignored, which is why it went unnoticed. The key is gone — it configured nothing (the embedded RuboCop is configured under the RuboCop linter's ignored_cops). Upgrade note: remove the two AllCops: / NewCops: disable lines from your .slim-lint.yml if it still has them.
  • The generated config/appsignal.rb now sets config.log_path = "/tmp" in production, so the AppSignal agent stops announcing appsignal: Unable to log to '/app/log'. Logging to '/tmp' instead. on every boot and every one-off container. Hosts with a read-only filesystem (Scalingo, Heroku and the like) cannot let the agent write its diagnostic log next to the app, and it falls back to /tmp by itself — nothing was lost, but the message reads like a misconfiguration. Upgrade note: add config.log_path = "/tmp" if Rails.env.production? to a config/appsignal.rb written before this version, or regenerate it with rails g trek:install:appsignal.
  • Trek::Phonable#normalize_phone normalized every declared attribute from the value of phone: a model declaring phonable_attributes :phone, :fax — the concern's own documented example — overwrote the fax with the phone number on every save, and wiped it when the phone was blank. Each attribute is now parsed from its own value. Models declaring a single attribute are unaffected.
  • Trek::ButtonComponent's disabled option is now actually rendered — it was declared but ignored by every branch, so a "disabled" button stayed clickable with no visual cue. A disabled button renders <button disabled>; a disabled link (href: or to:) renders an inert anchor — no href, no Turbo method — with aria-disabled="true", since <a> has no disabled attribute. Both get a dimmed, non-interactive style (opacity: 0.5, pointer-events: none, cursor: not-allowed). Upgrade note: you can now drop classnames: "is-disabled"-style workarounds and their CSS, and pass disabled: with a real href instead of blanking the href conditionally.
  • The icon of a Trek::ButtonComponent no longer shrinks next to a long label: as a flex item it was squeezed (and, with a min-width, distorted) when the text ran out of room. It keeps its size, the text wraps or ellipsises as before. Upgrade note: you can now drop a .c-trek--button-icon { flex-shrink: 0 } override.
  • The login, forgotten-password and password-reset forms labelled their fields with the humanized attribute name ("Email", "Password") whatever the locale. They post to a URL rather than to a record, so Trek::Form::GroupComponent had no model to resolve activerecord.attributes.user.* against and fell back to the attribute name; the translations shipped in Trek's locales were never looked up. The three generated views now pass model_name: User.model_name to f.group, which drives the i18n lookup alone and leaves field names (and therefore the params the controllers read) untouched. Upgrade note: views generated before this version keep the old labels — add model_name: User.model_name to the f.group calls in app/views/admin/user_sessions/new.html.slim and app/views/admin/user_password_resets/{new,edit}.html.slim, or regenerate them.
  • The toast link's new-tab icon rendered as an empty <svg><use></svg>: Trek::Toaster::ToastComponent::LinkComponent referenced an external icon that didn't ship with the gem (it only resolved when the host app provided its own app/assets/icons/external.svg). Trek now bundles trek/external (Lucide's external-link, ISC) and the toast link uses it; the icon is also available to host apps as Trek::IconComponent.new("trek/external"). Upgrade note: you can now drop an app/assets/icons/external.svg that only existed to back the toast link (keep it if your own code renders the bare external key).
  • Trek::MenuComponent items pointing at a namespaced model doubled the namespace in the path used for the active-state check (href: [:admin, :missive, Missive::List] compared against admin/missive/missive/lists), so those items never got is-active. The namespace segments already covered by the model's collection are now deduplicated, and the item highlights on its own controller. Upgrade note: you can now drop active: overrides that only worked around this (e.g. active: "admin/#{model.model_name.collection}").
  • make release now chains its steps with &&, so a failing step halts the run. Previously the steps were separated by ;: when rake release failed (e.g. RubyGems asking for an MFA code), npm publish still ran and its output buried the error, leaving the gem unpublished while the tag and npm package were out.
  • make release prompts for the RubyGems and npm one-time passwords right before each publish step (passed as GEM_HOST_OTP_CODE and npm publish --otp), so accounts with MFA no longer fail the push; leave a prompt empty when MFA is off for that registry.
  • A file attached to a record on a numerically ordered closure tree — the generated Page and its image — stayed in Shrine's temporary cache storage forever when attached on create or together with a parent change. closure_tree reloads the record in its after_save callback to reorder siblings, and Shrine's activerecord plugin resets the attacher on reload, so the after_commit promotion found nothing to promote: the record kept "storage": "cache" data, and its derivation URLs broke with "Source file not found" as soon as the cached file was deleted. Updating an existing page's image was unaffected. Trek::SavedAttachmentsReload, a Shrine plugin loaded by the generated RecordImageUploader, now keeps across a reload the attachers whose file was saved with the record (an unsaved attachment is still discarded), so the file is promoted and the replaced one deleted after commit. Upgrade note: add plugin Trek::SavedAttachmentsReload under plugin :activerecord in app/uploaders/record_image_uploader.rb (it covers every model using that uploader, not only Trek's). Records saved before that may still point to cached files — run bin/rails trek:attachments:promote_cached to promote those whose file still exists, and have the ones it reports as missing uploaded again.

[2.2.0] - 2026-08-28 ​

Added ​

  • Trek::Dashboard::ChartComponent, a dashboard card wrapping Chartkick — pass title, data, a type (:line_chart, :bar_chart, …), optional info/options, and a color (default :slate); it renders public_send(type, data, **options) inside a coloured card matching the tiles. Chartkick is an optional dependency: the component only includes Chartkick::Helper when it is loaded, so Trek keeps working without it. Trek::Dashboard::AnalyticsComponent gained a renders_many :charts slot (alongside the existing section) so charts can be dropped straight into the dashboard with a.with_chart(...). Upgrade note: to use ChartComponent, add gem "chartkick" to your app and import "chartkick/chart.js" in your admin JS bundle (Trek does not pull Chartkick in for you).
  • Trek::Dashboard::TileComponent gained a title option (a large heading above the text, coloured per the tile's color) and a wide option (default false) that spans two tile columns. Its href is now optional: without one the tile renders as a plain div instead of a link, for read-only stat tiles.
  • The layout gains a stacked panel: a second panel frame (trek--layout-stacked-panel) that opens on top of the primary one. The content editor's link/image/prompt inserters now open there, so an editor hosted inside a panel (e.g. a Sectionable payload edited in a side panel) no longer loses its unsaved content when inserting a link — the primary panel stays put underneath and the inserted element lands back in the live editor. Closing the primary panel also closes the stacked one; Annuler in the stacked panel closes only that one. The stacked panel is 1rem narrower than the primary panel (only while it stacks on an open one). Upgrade note: host apps that override the panel views (app/views/<engine>/panels/{links,images,prompts}/*) must retarget them to the trek--layout-stacked-panel frame and the stackedPanelCloser target, as Trek's own views now do.

Fixed ​

  • Internal links inserted through the content editor (stored as gid:// URLs) rendered as literal gid://… hrefs — or were dropped by the sanitizer — in formatted content: Prosemirror#format_content now resolves GlobalIDs to page paths before rendering, and the editor's Link extension accepts the gid protocol so reopening such a link shows the linked page instead of an empty field.
  • Trek::Form::ActionsComponent: the sticky footer styling (sticky positioning, --accent-1 background, 4rem vertical padding) now only applies to the default aspect. It was set on the root rule, so the seamless aspect — used by Trek::DialogComponent — was also sticky and padded inside dialogs.

[2.1.0] - 2026-08-25 ​

Added ​

  • Docs: a Deploying to Scalingo guide covering the trek-buildpack setup, the environment to configure, and how to migrate an app from the stock buildpacks.
  • trek:install:prosopite generator (wired into trek:install) sets up Prosopite N+1 query detection (#211): adds prosopite and pg_query to the development/test Gemfile group and a config/initializers/prosopite.rb that mounts the Rack middleware — so every request is scanned, Trek's engine controllers included — raising Prosopite::NPlusOneQueriesError in test and logging in development. It also generates spec/requests/admin/n_plus_one_spec.rb, a smoke request spec rendering the dashboard, pages index and fragments index as an admin (extend it with the app's other admin routes). Documented under Controllers › N+1 detection with the Prosopite.pause escape hatch. Upgrade note: existing apps can run rails g trek:install:prosopite; apps that already wired Prosopite by hand can keep their setup (the generator skips the gems when prosopite is already in the Gemfile and won't overwrite an existing initializer).
  • make lint and make fix targets in Trek's own Makefile, mirroring the ones generated for host apps (RuboCop, slim-lint, ESLint, Stylelint, Prettier — the fix variant autocorrects all but slim-lint). A .prettierignore excludes the gitignored esbuild output in app/assets/builds/, which made the Prettier check fail on a local checkout that had run yarn build.
  • Trek::MenuComponent items accept an active: option for menu entries whose pages live under other controllers: a String is matched as a controller_path prefix, a Regexp against the whole path, and a boolean forces (or clears) the state — e.g. s.with_item(text: …, href: [:admin, Shop], active: "admin/shop") keeps Shop highlighted while editing its products. Without it, the item stays active when its href matches the current controller, as before.
  • content_editor accepts an autofocus: option (default false): when true, the editor grabs focus on mount with the cursor at the end of the content — useful when the editor is the primary field of a side panel.

Changed ​

  • The shared .rubocop.yml (Trek's own, and the one trek:install:rubocop copies into host apps) re-enables Layout/IndentationWidth and Layout/IndentationConsistency with EnforcedStyle: normal. rubocop-rails-omakase disables both to allow its house style of indenting methods under private; Trek's style keeps methods at the class-body level, and a plain rubocop --autocorrect now fixes indentation — in generated files too. Upgrade note: copy the two cop blocks from the template (rails g trek:install:rubocop will prompt to overwrite) into your app's .rubocop.yml, then run bundle exec rubocop --autocorrect once; methods indented under private and mis-indented generated files get fixed.
  • trek:install:scalingo now targets the dedicated trek-buildpack (#193) instead of the stock apt + ruby buildpack pair: it no longer generates .buildpacks (and removes an existing one) and prints the env-set commands to run (BUILDPACK_URL pinned to the buildpack's release tag, and the DATABASE_URL=$SCALINGO_POSTGRESQL_URL mapping). The buildpack pins Ruby/Node/Yarn from .ruby-version, the .node-version that rails new already generates, and package.json's packageManager, runs yarn install once, and ships a slimmed image (~40% smaller, measured deploys down from ~3min to ~1min35) — see the buildpack README for details. Upgrade note: on each Scalingo app, set BUILDPACK_URL=https://github.com/etaminstudio/trek-buildpack#v0.1.0, delete .buildpacks, make sure .node-version exists (apps generated with rails new -j esbuild have it), and check that config/database.yml reads DATABASE_URL in production (mapped with scalingo env-set 'DATABASE_URL=$SCALINGO_POSTGRESQL_URL').
  • Trek::Form::FieldsetComponent: the fieldset title (.c-trek--form--fieldset-title) now shares the form group label style (--slate-12, semibold, 1.2rem bottom margin) so a titled fieldset reads like a labelled group; the section modifier only adds its larger font size on top. Hosts that restyled the plain fieldset title themselves can drop that override.
  • trek:install:yarn now installs Yarn 4.18.0 (was 4.17.0). Upgrade note: existing apps can upgrade with yarn set version 4.18.0 followed by yarn install.

Fixed ​

  • trek:scaffold now references the model top-level qualified (::Shop::Product) in the generated controller's model and in the Admin::Dashboard::MODELS_IN_MENU entry: once the scaffold creates the Admin::Shop namespace, a bare Shop under module Admin resolves to that module instead of the model — the scaffolded controller raised NameError, and previously generated code referencing a Shop model silently got the module. The generator also warns when a namespace segment matches an existing model file that rails g model would replace with a namespace module. (#201) Upgrade note: if a scaffold introduced a namespace named like an existing model, qualify that model as ::Shop in the existing Admin::…Controller#model and MODELS_IN_MENU entries.
  • Trek::Entries::ItemComponent: the details list (.c-trek--entries--item-details) now wraps (flex-wrap: wrap) instead of overflowing or squeezing its items when an entry has many details or a narrow container.
  • trek:install:postmark is idempotent again: re-running it on an app where it had already run appended a second config.action_mailer.delivery_method = :postmark / postmark_settings block to config/environments/production.rb (the gem, credentials and mailer steps were already guarded). The config injection is now skipped when the file already sets delivery_method = :postmark. (#210)
  • Namespaced models (e.g. Shop::Product, routed under namespace :shop) now work out of the box with Trek::ResourceController (#212): object_params requires model_name.param_key (shop_product) instead of the demodulized element, so create/update no longer 400; model_element_route no longer prefixes the namespace when the route key already carries it (shop_product), so the delete button and list item links build admin_shop_product_path instead of the double-namespaced admin_shop_shop_product_path — models from isolated engines with relative naming (e.g. an engine's Blog::Post, route key post) keep the [:blog, :post] form; the controller defines implicit_authorization_target (@object || model) so authorized(params) and Trek::Reorderable#reorder's bare authorize! find the policy; and i18n_model_gender reads activerecord.models.<i18n_key>.gender (shop/product), so i18n_resource_label picks the declared gender instead of the neutral fallback. Upgrade note: hosts that worked around this with a controller concern overriding implicit_authorization_target/object_params/model_element_route, or a prepend on Trek::I18nHelper#i18n_model_gender, can drop them.
  • Trek::Form::GroupComponent now styles input[type="time"] like input[type="date"] (field frame, 16rem max width, lighter picker colour); time_fields used to render as bare browser inputs.
  • Removed the dangling sourceMappingURL comment from the vendored tom_select/css/tom-select.custom.css: the referenced .map file is not shipped, so every host-app esbuild build (including assets:precompile on deploy) logged a missing-source-map debug warning. (#209)
  • trek:scaffold now re-indents the generated controller and policy: for a namespaced model the nested module wrappers were emitted without indenting the class body, tripping the host app's linters. The generator runs a targeted rubocop --autocorrect --only Layout/IndentationWidth,Layout/IndentationConsistency on both files (forced with --only since rubocop-rails-omakase leaves those cops disabled). (#199)
  • The config/initializers/host.rb generated by trek:install:config no longer references ActionMailer::Base during the initializer phase: doing so fires Action Mailer's load hook before the app's autoloader can resolve app classes, so an app configuring a custom config.action_mailer.delivery_job (e.g. "MailDeliveryJob") crashed with a NameError on every production boot — breaking assets:precompile on deploy. The assignment is now deferred with ActiveSupport.on_load(:action_mailer). (#208) Upgrade note: apply the same change to your app's config/initializers/host.rb — replace the ActionMailer::Base.default_url_options = … if Rails.env.production? line with the deferred block (see the generator template).
  • trek:scaffold now generates the right controller class for a namespaced model (e.g. rails g trek:scaffold Shop::Product): the template used the still-namespaced controller_class_name inside the per-namespace module wrappers, defining Admin::Shop::Shop::ProductsController while the generated route expected Admin::Shop::ProductsController — every request 404ed with an uninitialized constant. The class name is now demodulized, like the policy template already did. (#200)
  • The show.html.slim generated by trek:scaffold no longer ends with a trailing blank line: the attribute loop emitted its separator after every item instead of between items, so slim-lint failed in host apps with TrailingBlankLines: Multiple empty lines in the end of file. (#205)
  • trek:upgrade:v2 now upgrades Yarn to 4.18.0 first (applying the 0.1.26 upgrade note: yarn set version + dropping the interactive-tools plugin) when the app still runs Yarn 3. Previously the generator wrote npmPreapprovedPackages — a Yarn 4-only setting — unconditionally, which under Yarn 3 is a hard error that breaks every yarn command, including the generator's own yarn add step.
  • Clarified the 0.1.26 changelog upgrade note about the Yarn minimal-age-gate exemption: it only applied to the pre-2.0.0 private GitLab distribution — since 2.0.0 the (Yarn 4-only) npmPreapprovedPackages entry, set up automatically by trek:install and trek:upgrade:v2, replaces it.
  • Fixed whitespace in model.en.yml.tt which caused prettier to fail.

[2.0.1] - 2026-08-18 ​

Fixed ​

  • The npm tarball ships vendor/ again: the 2.0.0 slimming dropped it, but the three select form components (collection_select, grouped_collection_select, link_select) import vendor/tom_select/css/tom-select.custom.css from their JS sidecars, so host-app esbuild builds failed with "Could not resolve" errors.
  • AGENTS.md now describes the public distribution (RubyGems + npmjs.com, npm package renamed to trek) instead of the retired private GitLab packages.

[2.0.0] - 2026-08-18 ​

Changed ​

  • Trek's packages are now public-only (#197): the gem is consumed from RubyGems and the npm package — renamed from @etaminstudio/trek to the unscoped trek — is published to npmjs.com. Nothing is published to the private GitLab registries any more, and no deploy token is needed. Because the previous owner of the npm trek name had already published versions up to 1.0.0 (npm never frees version numbers), Trek jumps straight to 2.0.0, keeping gem and npm versions in lockstep. Upgrade note: existing apps upgrade by moving to 0.1.27 first, then running rails g trek:upgrade:v2 (see 0.1.27's changelog entry) — the generator now also adds the npmPreapprovedPackages exemption below. Afterwards, remove the now-unneeded CI variables — NPM_AUTH_TOKEN and BUNDLE_GIT__ETAMIN__STUDIO (spelled BUNDLE_GIT__ETAMINSTUD__IO on apps created before the domain migration) — along with any local TREK_DEPLOY_TOKEN export, and revoke the project's Trek deploy token on GitLab.
  • trek:install now wires new apps to the public packages: the generated package.json depends on trek, the JS entrypoint imports "trek", and the generated .yarnrc.yml no longer configures the GitLab registry/scope — instead it lists trek under npmPreapprovedPackages, exempting it from Yarn's package gates (minimal age, …) so fresh releases install without the quarantine delay.
  • The published npm tarball now only contains what host apps consume — app/javascript, app/assets/{icons,images,stylesheets} and the components' JS/CSS sidecars — instead of the whole repository (Ruby engine code, specs, docs, local config…).
  • Package metadata refresh for the public listings: description "A modern content management & back-office system for Ruby on Rails" and homepage https://trek.etamin.studio on both the gemspec and package.json, plus an explicit MIT license on both.
  • make release now publishes the npm package with npm publish (requires npm login as a trek maintainer) instead of yarn npm publish to the GitLab registry.

[0.1.27] - 2026-08-18 ​

Added ​

  • trek:upgrade:v2 generator: upgrades a host app to Trek 2.0 by switching it from the private GitLab packages to the public ones — the trek gem on RubyGems and the unscoped trek npm package. It rewrites the Gemfile (git: source → gem "trek", "~> 2.0"), drops the git.etamin.studio Bundler credentials, swaps the @etaminstudio/trek npm dependency and imports for trek, cleans the GitLab registry/scope out of .yarnrc.yml, and lists the CI credentials to remove manually. (#198) Upgrade note: this ships in 0.1.27 — the last version published to the private GitLab sources — so upgrade to it, then run rails g trek:upgrade:v2 to move to Trek 2.0 and stop depending on the private GitLab sources and their deploy token.

Changed ​

  • trek:install:ruby now pins Ruby 4.0.6 (was 4.0.5), and the CI images (repo + generated .gitlab-ci.yml template) use the matching 4.0.6 tag. Upgrade note: existing apps can set 4.0.6 in .ruby-version and bump the ci-ruby image tag in .gitlab-ci.yml.

[0.1.26] - 2026-07-02 ​

Changed ​

  • Trek::Localization now resolves the request locale through an overridable locale_for_request method (still params → current_user → headers by default). Apps that need another source can override it and fall back to the default chain with super instead of forking the whole concern — e.g. def locale_for_request = Current.event&.locale.presence || super. No behavior change for existing consumers.
  • trek:install:authentication no longer injects generates_token_for declarations into the generated User: :password_reset (15 min) is provided automatically by Rails' has_secure_password, and :invite (1 week) now lives in the Trek::Users::Invitable concern (which owns the rest of the invite behaviour). Upgrade note: existing apps can delete both generates_token_for blocks from their User — Rails and the concern provide them; customise the reset window via has_secure_password reset_token: { expires_in: … } if needed.
  • trek:install:ruby now pins Ruby 4.0.5 (was 4.0.0), and the CI images (repo + generated .gitlab-ci.yml template) use the matching 4.0.5 tag. Upgrade note: existing apps can set 4.0.5 in .ruby-version and bump the ci-ruby image tag in .gitlab-ci.yml.
  • trek:install:yarn now installs Yarn 4.17.0 (was 3.8.7) and no longer imports the interactive-tools plugin, whose upgrade-interactive command is built into Yarn 4. Yarn 4 is also what supports the minimal-age-gate exception below. Upgrade note: existing apps can upgrade with yarn set version 4.17.0 followed by yarn install, and drop the now-redundant interactive-tools plugin.
  • Bumped Node to 24.18.0 (was 22.21.1) in .node-version and the CI images (repo + generated .gitlab-ci.yml template, now ci-ruby/node-24). Upgrade note: existing apps can set 24.18.0 in .node-version and bump the ci-ruby/node-* image in .gitlab-ci.yml.
  • CI now runs i18n-tasks missing -t diff (config in config/i18n-tasks.yml) so a base-locale key with no translation in another locale fails the build. (Full health isn't used: as an engine, Trek's keys are consumed by host apps and many components/templates use relative keys that only resolve there.)

Fixed ​

  • Added the missing French translations that fell back to English: admin.dialog_component.{title,text} (the confirmation dialog shown on every destroy action) and admin.{pages,fragments}.{show.intro,reorder.success,search.alert}. Also added the English admin.section/admin.sections flash messages that previously existed only in French. (#194)
  • trek:install:yarn now exempts the @etaminstudio scope from Yarn's minimal-age gate (npmMinimalAgeGate: 0 under npmScopes.etaminstudio in the generated .yarnrc.yml), so a freshly published @etaminstudio/trek release installs without being quarantined; the gate still applies to public packages. Upgrade note: existing apps can add npmMinimalAgeGate: 0 under npmScopes.etaminstudio in their .yarnrc.yml. (Only relevant to the private GitLab distribution used before 2.0.0: the @etaminstudio scope is retired since then, so this scope exemption does nothing for the public unscoped trek package. From 2.0.0 the equivalent is trek listed under npmPreapprovedPackages — a Yarn 4-only setting, hence the Yarn upgrade above — which trek:install generates and trek:upgrade:v2 adds during migration, so no manual step is needed.)
  • trek:install:locales no longer generates an invalid config/initializers/locales.rb (I18n.available_locales = [:] / I18n.default_locale = :) when passed no or blank locales. A blank option/env now falls back to the default (en, fr), blank entries are dropped, and the list can never end up empty. (#169)

[0.1.25] - 2026-07-01 ​

Added ​

  • Invitation and password-reset emails now tell the recipient when the link expires. Invitations get their own :invite token purpose (1 week) — distinct from the 15-minute :password_reset token — and the generated Admin::UserPasswordResetsController accepts either purpose, so the shared "set your password" form works from both emails. Each mailer reads its window from the model's token definition (User.token_definitions[:purpose].expires_in) rather than a hardcoded constant. Upgrade note: add generates_token_for :invite, expires_in: 1.week do password_salt&.last(10) end to your User; have AdminMailer#invite issue an :invite token (and include ActionView::Helpers::DateHelper); and update Admin::UserPasswordResetsController#set_user_by_token to try both purposes, e.g. User.privileged.find_by_token_for(:invite, @token) || User.privileged.find_by_token_for!(:password_reset, @token).
  • trek:install now generates an AGENTS.md (onboarding guidance for AI coding agents: commands, conventions, and a "read the changelog on every upgrade" checklist) plus a CLAUDE.md that imports it via See @AGENTS.md, so Claude Code and other agents share one source of truth. Upgrade note: existing apps can add both files by running rails g trek:install:agents.
  • Trek::Filters controller concern: filter the index by params[:filters] (attribute equality via where). Call apply_filters! in your index.
  • Pedagogical NotAppliedError guard on Trek::Filters and Trek::Scopes: raises (outside production) when the concern is included but index never calls apply_filters! / apply_scopes!.
  • trek:install:scalingo now adds libjemalloc2 to the generated Aptfile, enabling the jemalloc allocator to reduce Ruby memory usage on Scalingo.

Fixed ​

  • The generated dashboard's "search a user" tile now links to the users index (href: %i[admin users]) instead of a dead #, and gates on allowed_to: allowed_to?(:index?, User) so roles that can't reach user management no longer see it (matching the sidebar's Users entry). Upgrade note: existing apps can set href: %i[admin users] and add allowed_to: allowed_to?(:index?, User) on that tile in app/views/admin/dashboards/index.html.slim.
  • trek:install now points Action Mailer at the same host as the routes' default_url_options (driven by DEFAULT_HOSTNAME) in the generated config/initializers/host.rb, in production only (development and test keep the host from their environment files). Previously the mailer kept Rails' example.com default, so invitation and password-reset email links pointed to https://example.com/... in production. Upgrade note: existing apps can add ActionMailer::Base.default_url_options = Rails.application.routes.default_url_options if Rails.env.production? to their config/initializers/host.rb (and drop any hardcoded config.action_mailer.default_url_options host).
  • The generated Admin::UsersController (trek:install) seeds a random password in create_hook, so users created via the admin form — which never collects one — satisfy has_secure_password and the non-null password_digest column. Invitees set their real password via the invite email; others via password reset. (Password seeding lives in the controller rather than Trek::Users::Invitable so it also covers users created without an invite.) Upgrade note: ensure your Admin::UsersController#create_hook seeds a password when blank: @object.password = SecureRandom.alphanumeric(24) if @object.password.blank?.
  • trek:install:ci now sets POSTGRES_DB in .gitlab-ci.yml from the application name (e.g. myapp_test) instead of the hardcoded project_test.
  • trek:install:yarn now rewrites bin/setup's yarn install --check-files to yarn install, since --check-files is not a valid flag under Yarn berry and was breaking bin/setup (and therefore CI).
  • trek:install now adds a !/.env.example exception to .gitignore so the generated .env.example template stays tracked (Rails ignores all /.env* files by default).
  • trek:install:postmark now matches the production.rb SMTP comment with or without the bin/ prefix, so the Postmark config is injected on Rails 8.0 as well as 8.1 (previously the injection silently did nothing when the anchor did not match).

[0.1.8] - 2025-09-03 ​

Fixed ​

  • (documented retroactively, 2026-07-27) Removed the dead option :items from Trek::ListComponent, which collided with its renders_many :items slot. This turned out to fix a serious incompatibility: since view_component 3.15.0, slot readers are defined in an included module, so the dry-initializer option reader (defaulting to []) shadows the slot reader — items always returns [] and every ListComponent list renders its empty state, i.e. all admin index pages look empty regardless of data or permissions (tree-based indexes like pages are unaffected). With view_component ≤ 3.14 the slot reader happened to overwrite the option reader, masking the collision. Upgrade note: apps pinned to Trek < 0.1.8 that bump view_component to ≥ 3.15 (e.g. for its security fixes) hit this immediately; until they can upgrade Trek, they can restore the slot reader with an initializer: Rails.application.config.to_prepare { Trek::ListComponent.class_eval { def items = get_slot(:items) } }.

[0.0.1] - 2023-01-24 ​

  • Initial release

Released under the MIT License.