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 isMember.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 scanand a newnpm-auditCI job runyarn npm audit, failing on any advisory or deprecation in the packages the app bundles (dependencies, notdevDependencies). See Vulnerable dependencies for how to resolve one. Upgrade note: move the PostCSS packages todevDependenciesfirst (see below), then add thenpm-auditjob next tobundler-auditin.gitlab-ci.yml, andyarn npm audit --recursive --environment productiontomake scan(seelib/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 overridinglocale_for_request. bin/rails trek:security:checkchecks a deployed app's configuration against Trek's recommended settings: HTTPS and HSTS, the Content Security Policy, the cache store, the proxies andADMIN_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 ofapp/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, copyspec/requests/admin/security_checks_spec.rbfrom 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/nullonenv-setso secrets aren't printed. Trek::Auditable'saudit_read_actionslists 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 overridingread_only_request?for this can setself.audit_read_actions = %i[chips]instead.Trek::PurgeExpiredSignInCodesJobdeletes 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_accountsschedules it every night in GoodJob's cron, with codes (3:45am). See Public accounts. Upgrade note: in apps signing in with codes, add toconfig.good_job.cron, for each public account model (hereMember):purge_expired_member_sign_in_codes: { cron: "45 3 * * *", class: "Trek::PurgeExpiredSignInCodesJob", args: ["Member::SignInCode"] }.bin/rails trek:attachments:strip_image_metadatastrips the metadata of the images stored beforeImageUploaderstripped it on upload (DRY_RUN=1only 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'sImageUploaderstrips 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_userscookie, 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 ofapp/controllers/concerns/trek/authentication.rb: replace itssigned_in_from_this_browser?andremember_sign_in_from_this_browser, and addknown_user_ids, from Trek's (lib/generators/trek/templates/controllers/concerns/trek/authentication.rb). In apps with Trek's host specs, copyspec/requests/admin/sign_in_notices_spec.rbfrom Trek's templates. - Security: the
ImageUploadertrek:install:uploaderscopies 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 ofapp/uploaders/image_uploader.rb: re-runrails g trek:install:uploaders, which offers to replace it with Trek's, then bring back the app's own changes, and mount the endpoint inconfig/routes.rbif 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:installschedulesTrek::PurgeExpiredSessionsJob(the admin's sessions, 3am),trek:audit_logaddsTrek::AuditLog::PurgeJob(4am), andtrek:public_accountsthe purge of its accounts' sessions (3:30am). Without GoodJob's initializer, the generators say what to schedule instead. See Scheduled jobs. Upgrade note: inconfig/initializers/good_job.rb, add toconfig.good_job.cronthe 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, andpurge_expired_member_sessions: { cron: "30 3 * * *", class: "Trek::PurgeExpiredSessionsJob", args: ["Member::Session"] }for each public account model (hereMember). 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 auditreported 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:postcssadds the PostCSS packages todevDependencies, as the other build and lint tools, rather than todependencies, and no longer addspostcss-inline-svg, which Trek's stylesheets don't use any more: it brought a css-what with a high advisory. Upgrade note: inpackage.json, movepostcss,postcss-flexbugs-fixes,postcss-import,postcss-modulesandpostcss-preset-envtodevDependencies, and runyarn install. Unless the app's own CSS uses@svg-loadorsvg-load(), removepostcss-inline-svgfrompackage.jsonand itsrequire("postcss-inline-svg")(…)frompostcss.config.js.rails g trek:scalingoand 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 ofGemfile.lock(apps whose lockfile asked for a newer one died on start withCould not find 'bundler') and forces its install past the default gem, and it now preloads jemalloc: thelibjemalloc2of theAptfilewas installed but never used, as rv's Rubies aren't built with it. Upgrade note: on each Scalingo app, runscalingo --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 madebundle add trekon 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 theirGemfile.lock; addgem "json", "< 3"to the Gemfile so abundle updatedoesn't bring json 3. rails g trek:scalingocomments out thekamalgemrails newadds, since a Scalingo app doesn't deploy with it, and runsbundle 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/secretsandbin/kamalstay in place. Upgrade note: apps already on Scalingo can re-runrails g trek:scalingo, or comment outgem "kamal"by hand and runbundle install.bin/rails g trek:scaffoldgives the new model ato_sreturning its first string attribute (title.presence || super), which the admin shows records by: they were listed as#<Book:0x…>until its commented-outto_swas 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 ofrails g …andbin/rails ….
Removed
- DeepL: the scaffold generator no longer translates the new locale files with
DEEPL_AUTH_KEY, Trek no longer depends ondeepl-rb, andrails g trek:install:translationsno longer adds it, nor a DeepLtranslation:section toconfig/i18n-tasks.yml. Upgrade note: apps can removegem "deepl-rb"from their Gemfile, thetranslation:section fromconfig/i18n-tasks.yml, andDEEPL_AUTH_KEYfrom their environment; an app that still translates withi18n-tasks translate-missing --backend=deeplkeeps 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: adddata: { turbo: false }to theform_withofapp/views/admin/user_sessions/new.html.slimandapp/views/admin/user_password_resets/edit.html.slim, and, in apps that rantrek:public_accounts, of itssessions/new(the password form),registrations/new,password_resets/editandpasswords/editviews. - A
localeparam that isn't one of the app's locales (?locale=xx) made every controller includingTrek::Localizationanswer 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, whichprosemirrorbrings through Phlex, uses a node Prism 2 removed. Trek now requires Prism < 2. Upgrade note: if the app'sGemfile.lockhas Prism 2 (bundle info prism),bundle update trekbrings 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.ymlran on theci-ruby/node-24image whatever Node the app pins:rails g trek:install:cinow 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 majorci-rubyhas no image for. Upgrade note: apps pinning another Node than 24 can setnode-<major>in theimage:of.gitlab-ci.yml. rails g trek:public_accountsadded the account's attributes toCurrentwithout indenting them. Upgrade note: apps that ran it on Trek 2.6.0 can indent the three lines it added toapp/models/current.rb.- The sign-in concern
rails g trek:public_accountsgenerates 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 forHEADrequests too, as forGET. Upgrade note: apps that ran it on Trek 2.6.0 can replaceif request.get?withif request.get? || request.head?inrequire_<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, withnpm install --global corepack, or Yarn with mise. - The admin's edit form of a page made of sections (
sectioned) but not versioned failed with aNameError(vf). Upgrade note: inapp/views/admin/pages/_form.html.slim, replacevf.object.sections_instanceswith@object.sections_instancesin 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'sordered, 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 (hereMember), afterhas_many :sign_in_codes:after_destroy { Member::SignInCode.where(email:, purpose: "sign_in").delete_all }. - Small inaccuracies in the reference docs:
GateComponentand the toast component's name, whatf.grouprenders, the formatter's narrow no-break spaces,User#last_activity_at,Current's attributes,page.versions.current, and the generators list, which now hastrek:scalingoandtrek:upgrade:v2. bin/rails g trek:scaffoldleft the new model's entry inAdmin::Dashboard::MODELS_IN_MENUunindented.
[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_sampletopolicy.script_srcinconfig/initializers/content_security_policy.rbto 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 swapssolid_queueforgood_jobin the Gemfile (Solid Cache stays), removes the Solid Queue setuprails newleaves behind (config/queue.yml,config/recurring.yml,db/queue_schema.rb,bin/jobs, the Puma plugin, thequeue:database and Kamal'sSOLID_QUEUE_IN_PUMA/JOB_CONCURRENCYvariables), and configures the adapter (Active Job' test adapter inconfig/environments/test.rb, so specs can check what is enqueued),:asyncexecution in production, the queues, cron and error reporting inconfig/initializers/good_job.rb. The dashboard is mounted at/admin/good_jobwith 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, everydeliver_lateremail now goes through an installedMailDeliveryJobthat retries five times with a growing delay, andconfig/database.ymlreserves connections for the job threads on top of the web threads (GOOD_JOB_MAX_THREADS, which GoodJob defaults toRAILS_MAX_THREADS). See Background jobs. Upgrade note: apps installed before this version can runrails g trek:install:good_job— it is idempotent and skips what is already in place — then add the dashboard entry toapp/views/layouts/admin.html.slim(an item withtext: 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 theirconfig/initializers/good_job.rbcompared with the generated one; apps still on Solid Queue must drop thequeuedatabase 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 usevar(--accent-*). - Public accounts:
rails g trek:public_accounts Customergives the site's or the mobile app's users (customers, members…) a model of their own, apart from the admin'sUser, 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=passwordwith a password, the email confirmed and the password reset by a link; a website gets pages and a cookie,--apiJSON endpoints and a JWT naming a session, so it can be revoked, and--model-onlyneither, for an app whose sign-in is its own. See Public accounts. Trek::Users::SignInLockoutsends its email throughsign_in_lockout_mailer("AdminMailer"by default), andTrek::PurgeExpiredSessionsJobtakes the session model to purge (User::Sessionby default), for public accounts.
Changed
- Trek requires the
prosemirrorgem 1.0 or later (was 0.7.1), and works with 1.1: rendering content (formatted_content) raisedArgumentErrorwith it.Trek::Content::Document#to_htmltakes 1.1'sheading_ids: true. rails g trek:installends by listing the generators it doesn't run, to run as the app needs them:trek:scaffold,trek:taxonomies,trek:audit_log,trek:public_accountsandtrek:scalingo.trek:install:audit_logandtrek:install:scalingoare nowtrek:audit_logandtrek:scalingo, astrek:installdoesn'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:rubynow pins Ruby 4.0.9 (was 4.0.7), and the CI images (repo + generated.gitlab-ci.ymltemplate) use the matching4.0.9tag. Upgrade note: existing apps can set4.0.9in.ruby-versionand bump theci-rubyimage 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 arequire_password_renewalaction ofAdmin::UsersController. Upgrade note: copyapp/controllers/admin/password_renewals_controller.rbfrom Trek's templates (lib/generators/trek/templates/controllers/admin/), addresource :password_renewal, only: :createto theadminnamespace ofconfig/routes.rb, removepost :require_password_renewal, on: :collectionfromresources :usersand therequire_password_renewalaction fromAdmin::UsersController, and point the button ofapp/views/admin/users/index.html.slimto[: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, copynew_sign_infrom Trek'slib/generators/trek/templates/mailers/admin_mailer.rb, and replace@ipwith@addressinapp/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_textand 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, whichFragment.searchcan 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 callingSchema#parse_html,#parse_markdownor#parse_text, orProsemirror::TextAlign, renames them#from_html,#from_markdown,#from_textandProsemirror::Extensions::TextAlignwhen it runsbundle 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, inTrek::ResourceController, the sign-in controller and the generated specs, which printed a deprecation warning in every run. Upgrade note: replace:unprocessable_entitywith:unprocessable_contentin 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: runyarn 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::TypographyFormatterbroke 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:contenttoformat_attributesmay hold records whose content no longer parses (formatted_contentraisesJSON::ParserError), which the editor can't open either: restore them from a backup.Trek::TypographyFormatterno 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_textran 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 withnumeric_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 anacts_as_list_no_updateblock, from the list'stop_of_list, or with closure_tree'sprepend_siblingandappend_siblingfor a tree. Records missing from the list (deleted meanwhile, or left out byreorderable_objects) are skipped. Upgrade note: you can now drop your ownreorderactions 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 acolorthatTrek::ContentEditor::Promptnever had, and the panel has no field for. Prompts no longer have a color: the editor's block, the panel andTrek::Content::PromptsBlockdrop it, and the site's link loses itsdata-color. A color already stored in content stays in the JSON, unrendered. Upgrade note: an app styling prompts with[data-color], or reading@colorin its ownPromptsBlock, 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
prosemirrorgem instead ofprosemirror_to_html, which had two XSS advisories: HTML is escaped as it is built, attributes are typed, and links and images only allowhttp,https,mailto,teland 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 toRails.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: theProsemirrormodel and theProsemirrorToHtmlformatters are removed: callformatted_content, orTrek::Content.schema.parse(content).to_html, instead ofProsemirror.new.format_content, and rewrite the app'sProsemirrorToHtmlnodes as node types registered inTrek::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
prosemirrorgem 3.3). Its authentication already relied on Rails 8:has_secure_password's:password_resettoken andpassword_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 inapp/models/user.rb, afterhas_secure_password:generates_token_for(:password_reset, expires_in: 15.minutes) { password_salt&.last(10) }anddef 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: runbundle update css_parserif the app'sGemfile.lockhas 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 crassif the app'sGemfile.lockhas an older one. - The generators no longer print RuboCop's report when they autocorrect the files they inject code into, only an
autocorrectline 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 amaketarget 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, withTrek::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, thesearchscope ofmodels/fragment.rb, andcontrollers/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 withurl_for, which needsTrek::PagePathHelper, no longer included. An app's owndirect :pageroute still takes precedence. Upgrade note: you can now drop initializers that includeTrek::PagePathHelperinProsemirrorToHtml::GlobalIdToLinksFormatter. - Since 2.4.0, the site's links to pageable records (
Trek::Pageable) used their id instead of their page's slug:to_paramtook 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::Panelscontrollers it didn't generate. It now generates them, subclasses ofTrek::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 needsOpenStruct, which Ruby doesn't load by default since 3.5. Upgrade note: runrails g trek:install:adminand keep only its newapp/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 oftrek.panels.*.form.title, ofactivemodel.attributes.trek/content_editor/*and of the image sizes, and theostructgem if nothing else needs it. Re-runrails g trek:install:model_specsfor the newspec/requests/admin/panels_spec.rb. Trek::Reorderablegives the viewsreorder_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 ownhelper_method :reorder_pathand Prosopite pause aroundreorder.- 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 generatedImageUploaderdoesn'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_specspass 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-runrails g trek:install:model_specs, or drop your own locale workarounds inspec/mailers/admin_mailer_spec.rb,spec/requests/admin/sign_in_notices_spec.rbandspec/requests/admin/active_sessions_spec.rb. trek:scaffoldpasses 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
Makefilelints every Slim template: unquoted,make's shell expandedapp/**/*.slimone directory deep only, somake lintskipped what CI caught. Upgrade note: quote the pattern in yourMakefile'slinttarget:bundle exec slim-lint "app/**/*.slim". trek:install:specsincludes the matchers of rspec-html-matchers (have_tag,with_tag), which it adds to the Gemfile, inspec/rails_helper.rb. Upgrade note: addconfig.include RSpecHtmlMatchersto theRSpec.configureblock ofspec/rails_helper.rbto use them.Trek::SearchComponentlabels 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_loginstalls anAuditLogmodel recording each mutation in the admin (author, request, target record, status), sign-ins included, and an admin list for theadminrole;Trek::AuditLog::PurgeJobdeletes rows past 6 months. See Audit log. Upgrade note: an app with its own audit log can drop its copy for Trek's, keeping itsaudit_logstable (see the docs' migration steps). Trek::Filtersexposesfilter_paramsto views, asTrek::Scopesdoesscope_params.- Users can't reuse one of their last 5 passwords (
Trek::Users::PasswordHistory, configurable withpassword_history_size). Upgrade note: copyapp/models/user/previous_password.rb(User::PreviousPassword) and theuser_previous_passwordsmigration from Trek's templates (lib/generators/trek/templates/{models,migrations}), run it, addinclude Trek::Users::PasswordHistoryafterhas_secure_passwordinapp/models/user.rb, and re-runrails g trek:install:model_specsfor the new specs. - Password expiry and renewal (
Trek::Users::PasswordRenewal): an admin can ask every user for a new password after a suspected compromise, andpassword_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 theuser_password_renewalmigration from Trek's templates (lib/generators/trek/templates/migrations) and run it, addinclude Trek::Users::PasswordRenewalafterhas_secure_passwordinapp/models/user.rb, and take the changes torequire_authenticationinapp/controllers/concerns/trek/authentication.rb, therequire_password_renewalaction ofAdmin::UsersController, its route (post :require_password_renewal, on: :collectioninresources :users), and theadmin/users/indexandadmin/user_password_resets/editviews from Trek's templates. - Sessions expire after 48 hours without a request and 30 days after signing in (
Trek::Users::SessionLifetime, configurable onUser::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 theuser_session_trackingmigration from Trek's templates (lib/generators/trek/templates/migrations) and run it; addinclude Trek::Users::SessionLifetimetoapp/models/user/session.rb(orapp/models/user_session.rb); take Trek's changes toapp/controllers/concerns/trek/authentication.rb(find_session_by_cookie,start_new_session_for,track_last_activity), toAdmin::UsersController#showand toapp/views/admin/users/show.html.slim(with its new_propertiesand_sessionspartials); copyapp/controllers/admin/active_sessions_controller.rbfrom Trek's templates and add its route (resources :active_sessions, only: :destroy do delete :others, action: :destroy_others, on: :collection endin the admin namespace); scheduleTrek::PurgeExpiredSessionsJobdaily. - 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 theuser_sign_insmigration and theadmin_mailer/new_sign_inviews from Trek's templates and run the migration; addinclude Trek::Users::SignInstoapp/models/user.rb; take Trek's changes toapp/controllers/concerns/trek/authentication.rb(record_sign_in_ofand the methods after it),app/controllers/admin/user_sessions_controller.rb(create,sign_in_notice),app/mailers/admin_mailer.rb(new_sign_in) andapp/views/admin/users/_sessions.html.slim. Trek::SectionComponent: a titled section of a page (anh2labelling it), with an optional hint under the heading.NotDisposableValidatorrefuses email addresses from disposable providers, on any model:validates :email, not_disposable: true. The domains come from thevalid_email2gem, 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 whyRails.cachemust be shared between servers. Upgrade note: copy theuser_sign_in_lockoutmigration and theadmin_mailer/sign_in_lockedviews from Trek's templates and run the migration; addinclude Trek::Users::SignInLockouttoapp/models/user.rb; take Trek'sapp/controllers/admin/user_sessions_controller.rband thesign_in_lockedmethod ofapp/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 thewebauthngem). The password keeps working. See Passkeys. Upgrade note: copyapp/models/user/passkey.rband theuser_passkeysmigration from Trek's templates and run it; addinclude Trek::Users::Passkeystoapp/models/user.rb; copyapp/controllers/admin/passkeys_controller.rbandapp/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 endandresource :passkey_session, only: :create do get :options end); take Trek's changes toapp/views/admin/user_sessions/new.html.slim,app/views/admin/users/show.html.slim(with the new_passkeyspartial) andAdmin::UsersController#show; with the audit log, addAdmin::PasskeySessionsControllertoconfig/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
sbomjob, 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 thesbomjob from Trek'slib/generators/trek/templates/.gitlab-ci.ymlinto the app's.gitlab-ci.yml, uncommented. ADMIN_ALLOWED_IPSrestricts the admin to IP addresses and ranges (e.g.203.0.113.0/24, 2001:db8::/32): from anywhere else,/adminand everything below it answer like an unknown URL, a 404. Off when unset. Behind a CDN, list its ranges intrusted_proxiesfirst. See Restricting the admin to IP addresses.- Security headers:
rails g trek:install:security_headers, whichtrek:installruns, sets up a Content Security Policy, report-only at first, allowing no inline script, and aPermissions-Policyheader 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, fromforce_ssl, already covers subdomains; itspreloadoption is documented. See Security headers. Upgrade note: runrails g trek:install:security_headers, browse the app with the console open and watch AppSignal'scspnamespace, add the front office's third-party hosts, then setcontent_security_policy_report_onlytofalse.
Changed
- Trek pins Node 24.22.0 (was 24.18.0) in
.node-version. Upgrade note: existing apps can set24.22.0in their.node-version. trek:install:yarninstalls 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 withyarn set version 4.18.1followed byyarn install.- Passwords follow the ANSSI's recommendations for a privileged access (
Trek::Users::PasswordPolicy, included byTrek::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 inapp/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 sameuser_sessionstable. Apps generated before keep working withUserSession. Upgrade note: to follow, moveapp/models/user_session.rbtoapp/models/user/session.rbasclass User::Session, replacehas_many :user_sessionswithhas_many :sessionsinapp/models/user.rb, and updateUserSession.find_byanduser.user_sessionsinapp/controllers/concerns/trek/authentication.rbandapp/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 intoadmin.css, instead of Google Fonts: visitors' browsers no longer contact Google, and the Content Security Policy needs no Google host. Upgrade note: removeadmin_webfonts_link_tagfromapp/helpers/application_helper.rbandapp/views/layouts/admin.html.slim, and the Google Fontslinktags from both admin layouts (admin.html.slim,admin/auth.html.slim);esbuild.config.jsmust have the.woffand.woff2file 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 toUser.authenticate_by(email:, password:)instead, as Trek's generatedAdmin::UserSessionsControllerdoes.
Changed
- The install generators no longer add
esbuild-plugin-import-glob,postcss-cli,postcss-nestingandpostcss-url, which no generated config uses. Upgrade note: you can drop them from the app'spackage.json(yarn remove …), except those its ownpostcss.config.js,esbuild.config.jsorpackage.jsonscripts still use (e.g.postcss-urlto rebase the URLs of@import-ed CSS). rails g trek:installruns Trek's own generators in its process instead of startingrails generatefor each, which saves about two minutes.
Fixed
- The generated
.gitlab-ci.yml's commented-out deploy jobs point atexample.cominstead of a real site. - A tag's key is unique within its category, as
Tagvalidates it and the docs said: the generatedtagsmigration's unique index onkeyalone refused the same key in two categories withActiveRecord::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: inapp/models/tag_category.rb, replacedependent: :nullifywithdependent: :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) withremove_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: trueandadd_index :tags, :key, unique: true, where: "category_id IS NULL", name: "index_tags_on_key_without_category". Copyspec/models/tag_spec.rbfrom 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::PasswordResetJobfor each, which emails privileged users only, so the response takes as long either way. Upgrade note: copyapp/jobs/admin/password_reset_job.rbfrom Trek's templates (lib/generators/trek/templates/jobs/admin), and inAdmin::UserPasswordResetsController#create, replace the user lookup anddeliver_laterwithAdmin::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 intocurrentColorbefore the icon's paths were inserted, so it never reached them. The substitution now runs once the paths are in. trek:install:esbuildinstalled esbuild 0.15, too old for theesbuild.config.jsit copies:yarn build --watch(andbin/dev) crashed withesbuild.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, runyarn 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_specsnow also installs mailer specs covering it. Upgrade note: inapp/mailers/admin_mailer.rbandapp/mailers/admin/passwords_mailer.rb, move the@expiration_delay = …line inside theI18n.with_localeblock, beforemail; re-runrails g trek:install:model_specsfor the new specs.
[2.3.2] - 2026-10-02
Fixed
- Security:
Trek::Scopes#apply_scopescalled any name found inparams[:scopes]that the relation answered to, not only the controller'savailable_scopes, which let a request run unintended methods on the listed records. Only the names inavailable_scopesare applied now; others are ignored. Upgrade note: update Trek as soon as possible. A controller passingapply_scopesa scope name it does not list inavailable_scopesmust 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 theseamlessaspect. Upgrade note: in apps generated before this version, passaspect: :seamlesstoTrek::Form::ActionsComponent.newinapp/views/admin/user_sessions/new.html.slimandapp/views/admin/user_password_resets/{new,edit}.html.slim.
[2.3.1] - 2026-10-02
Changed
Trek::Filtersonly filters on the columns a controller lists in its newavailable_filters(empty by default), read through strong params (filter_params): other keys ofparams[:filters], and values that aren't a single value, are ignored. Until now any key became awherecondition, so a request could filter an index on columns it was never meant to expose. Upgrade note: in each controller includingTrek::Filters, defineavailable_filtersreturning the filtered columns (e.g.def available_filters = %i[country_code]), or its filters stop applying.Trek::Users::Validationsstores emails stripped and in lower case (sign-in and password resets now ignore case), checks them withemail_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) }); inAdmin::UserPasswordResetsController#update, redirect back with an error whenparams[:password]is blank (see Trek's template); re-runrails g trek:install:model_specsfor 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 newtrigger_classnames:option styles the trigger button. Upgrade note: if you reuseTrek::DropdownComponent, move trigger styling (padding, hover) fromclassnames:totrigger_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) anduser_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). Adb/seeds/setup/fragments.rbmakesfragmentsavailable to seeds and specs. Upgrade note: re-runrails g trek:install:model_specsto add them. Trek::Dashboard::TileComponentacceptsmethod:anddata:. Withmethod: :post(or:delete…) the tile issues that request through Turbo, as adata-turbo-methodlink likeTrek::ButtonComponent'sto: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 asturbo_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_specsinstallsspec/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), andrails g trek:taxonomiesinstalls the tag and tag category ones (a tag or category in use can't be deleted). Upgrade note: re-runrails g trek:install:model_specs(andrails g trek:taxonomiesif 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:uploadersnow installs anAudioUploader(MP3, AAC/M4A, Ogg, WAV, WebM, FLAC, up to the field's 200 MB), the generatedAdmin::UploadsControllergains anaudioaction, andtrek:install:admin:routesaddspost "upload/audio", to: "uploads#audio"to the admin namespace. Attach it withinclude AudioUploader::Attachment(:audio)on a record with anaudio_datajsonb column. Upgrade note: copyapp/uploaders/audio_uploader.rbfrom Trek (or re-runrails g trek:install:uploaders), add theaudioaction toapp/controllers/admin/uploads_controller.rb(set_rack_response AudioUploader.upload_response(:cache, request.env)), and addpost "upload/audio", to: "uploads#audio"insidenamespace :admininconfig/routes.rb; you can then drop a hand-written audio uploader, action and route that did the same. rails g trek:install:model_specsalso 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 existingn_plus_one_spec.rb, with an image fixture inspec/fixtures/files. The Oaken users setup gains ausers.passwordhelper to sign in with. Upgrade note: re-runrails g trek:install:model_specsto add them, thenbin/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) andspec/models/user_spec.rb(validations, roles,privileged,to_s). Their test data uses Oaken seed scripts rather than factories:db/seeds/setup/users.rbsets defaults forusers.create/users.new(a unique email, a valid password), anddb/seeds/test/cases/page_tree.rblays out a named page tree the Page spec loads withseed "cases/page_tree". The step adds theoakengem (in every group, sincedb/seeds.rbnow callsOaken.seed, which also makes these seeds available tobin/rails db:seed) andrequire "oaken/rspec_setup"inspec/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: runrails g trek:install:model_specsto add them (it skips the gem, thedb/seeds.rbline and therails_helperline when already there, and asks before overwriting an existing file), thenbundle exec rspec spec/models.oaken/rspec_setuptruncates every table and runsdb/seeds.rbbefore the suite, so make sure yourdb/seeds.rbis 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-themeon the page without saving the choice (aTODOlogged 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'sthemethroughPATCH /admin/users/theme. The action answers with a turbo stream that refreshes the header icon. Logged-out pages (login, password reset) now render withauto-themeinstead oflight-theme. Auto follows the system setting (prefers-color-scheme) without JavaScript: a small PostCSS plugin inpostcss.config.jscopies Radix's.dark-themevariables into a@media (prefers-color-scheme: dark)block scoped to.auto-theme.color-schemeis now set for each theme too, so native controls and scrollbars follow it. The menu is a newTrek::DropdownComponent(a trigger slot pluswith_itementries, with keyboard navigation), which host apps can reuse. Upgrade note: inapp/controllers/admin/users_controller.rb, replacehead :no_contentin thethemeaction withrespond_to { |format| format.turbo_stream; format.html { head :no_content } }, and addapp/views/admin/users/theme.turbo_stream.slimfrom Trek's generator templates (or re-runrails 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 theautoDarkThemeplugin from Trek'spostcss.config.jsinto yours and add it topluginsright afterpostcss-import; without it, Auto renders the light theme. phonable_attributesaccepts astrictness:option —:possible(the default, unchanged) or:valid.:possibleonly asks Phonelib whether the digits could form a number for the country;:validasks 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 wantphonable_attributes :phone, strictness: :valid.- Plain file inputs are now styled: inside a
Trek::Form::GroupComponent(f.group), a standardf.file_fieldgets its native::file-selector-buttonstyled like aTrek::ButtonComponentwithsize: :narrow, using the accent variables sodata-accent-colorapplies 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-backedimage_field/sound_fieldare unaffected (their file inputs are visually hidden). Upgrade note: you can now drop custom::file-selector-buttonstyles that mirrored Trek's buttons for plain file inputs. - The confirm dialog (
Trek::DialogComponentwired toTurbo.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-buttonanddata-turbo-confirm-coloralongsidedata-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 ondata-turbo-methodlinks (the clicked element is remembered, since Turbo only forwardsdata-turbo-confirmto the form it builds for a link). Upgrade note: you can now dropcontent_for(:dialog)-style overrides that swapped the whole dialog just to rename the button — pass the data attributes on the confirming button instead. Trek::MenuComponentitems accept atarget: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_blankitem automatically getsrel="noopener"and thetrek/externalnew-tab icon after its text, as the toast link already did.Trek::Dashboard::TileComponent(which already acceptedtarget:/rel:) now also defaultsreltonoopenerwhentargetis_blank. Upgrade note: you can now drop initializer patches that reopenedItemComponentto add atargetoption.Trek::ButtonComponentaccepts aname:option, rendered on the plain-button branch (paired withvalue:) so atype: :submitbutton 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 submittingthen_preview). Upgrade note: you can now drop initializer patches that reopened the component to add anameoption.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 theappsignalgem, injectsAPPSIGNAL_PUSH_API_KEYinto the Rails credentials (as the Postmark generator does with its API token —bundle exec appsignal installwould write the key in plain text and needs a terminal), and writes aconfig/appsignal.rbthat reads the key from the credentials, activates the agent in production as soon as a key exists, and ignores the/healthcheckaction. It also wraps the boot inconfig.ruso a broken initializer or a missing credential is reported, and installs anAppsignalContextcontroller concern that sets theadmin/webnamespace and tags transactions with the acting user — included inApplicationControllerand, through an initializer, inTrek::BaseController.APPSIGNAL_APP_ENVis added to.env.example; the rescued Postmark delivery inApplicationMailernow reports throughRails.errorrather than only logging, which keeps app code independent of the monitoring vendor. Check an install withbundle exec appsignal diagnose. Upgrade note: existing apps can runrails g trek:install:appsignal— it skips the gem and the credentials when AppSignal is already set up, but review the generatedconfig/appsignal.rbagainst your own (it takes the push API key from the credentials, so drop a plain-text key or anAPPSIGNAL_PUSH_API_KEYenvironment variable you no longer need), and remove your hand-writtenAppsignalContextequivalent if you have one.bin/rails trek:attachments:promote_cached, a rake task finding the records whose Shrine attachments still point to the temporarycachestorage — for every model and attachment, whatever the uploader — and promoting those whose cached file still exists (backed byTrek::CachedAttachments). It prints one line per attachment with its status:promoted,missingwhen the cached file is gone (it has to be uploaded again), orchangedwhen the record changed meanwhile;DRY_RUN=1only lists them, aspromotable. 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 freshrails newapp 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:userhas its own spec against the dummy app's database. Line coverage goes from 35% to 49%; the minimum is raised to 49%. trek:install:rubynow pins Ruby 4.0.7 (was 4.0.6), and the CI images (repo + generated.gitlab-ci.ymltemplate) use the matching4.0.7tag. Upgrade note: existing apps can set4.0.7in.ruby-versionand bump theci-rubyimage tag in.gitlab-ci.yml.- Trek's own specs now run in random order (
--seedreplays an order,--bisectfinds the spec leaking state), and no longer depend on it:page_path_helper_specswitched the default locale to French insideI18n.with_locale, which leftI18n.localeset to:frfor the specs that ran after it, solocalization_specandi18n_helper_specfailed under some orders. - Trek's own suite can now test code that needs Rails and a database. Specs that
require "rails_helper"bootspec/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 whatrails g trek:installgives a real app, and a spec fails when a model or migration template is added without the suite generating it. FirstUserandPagemodel 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
testsjob that had been commented out — so nothing flagged thatspec/helpers/trek/page_path_helper_spec.rbrequired arails_helperthe 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=1turns on SimpleCov, which reports line and branch coverage over every Ruby file inapp/andlib/(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 thefile_systemstorage (the development and test storage), 3.7 stopscolumnfrom failing on an empty string, 3.9 fixesremove_invalidleaving the attribute set after an invalid file is deassigned and stops theactiverecordplugin 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: runbundle 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::Unauthorizedwas raised and handled nowhere.Trek::Authorizationnow 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 generatedfragmentsmigration createdcontentas a jsonb column without the{}default its translatedtitlehas, and Mobility's jsonb backend reads a translation ascolumn[locale]. The migration now defaultscontentto{}. Upgrade note: in apps generated before this version, add a migration withchange_column_default :fragments, :content, from: nil, to: {}andup_only { execute "UPDATE fragments SET content = '{}' WHERE content IS NULL" }. - The generated
Admin::ExternalLinkPolicyscoped external links for non-privileged users withrelation.where(user:), a leftover from an example:external_linkshas nousercolumn, so applying the scope raised a SQL error. Non-privileged users now get none, as in Trek's other policies. Upgrade note: inapp/policies/admin/external_link_policy.rb, replacerelation.where(user:)withrelation.none. - With
rails g trek:taxonomies, asking whether a tag could be deleted (Admin::TagPolicy#destroy?) raisedHasManyThroughAssociationPolymorphicSourceError: it calledTag#taggables, ahas_many :taggables, through: :taggingsthat Rails can't build over the polymorphicTagging#taggable. The policy now checkstaggings, andTag#taggablesis a method returning the tagged records. Upgrade note: inapp/models/tag.rb, replacehas_many :taggables, through: :taggingswithdef taggables = taggings.includes(:taggable).map(&:taggable), and inapp/policies/admin/tag_policy.rb, replacerecord.taggables.empty?withrecord.taggings.empty?. - The docs and README still described Sorcery as Trek's authentication, and the
Userreference listed itscrypted_passwordcolumn. Trek authenticates with Rails'has_secure_passwordand database-backedUserSessionrecords; the Getting started guide, the introduction, the home page, the customization guide, the installer andUserreferences now say so, and theUsercolumns table listspassword_digestandlast_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 generatedPageandPageVersion, declaredhas_many :sections, dependent: :destroy, but no generator creates aSectionmodel. 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 calledpage_path, which components don't include. The header now includesTrek::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:routeswrotepost "upload", to: "uploads#image", namespace: :adminat the top of the routes (thenamespace:ended up inside the route string), while image fields post to/admin/upload/image. It now writespost "upload/image", to: "uploads#image"insidenamespace :admin. Upgrade note: inconfig/routes.rb, replacepost "upload", to: "uploads#image", namespace: :adminwithpost "upload/image", to: "uploads#image"inside yournamespace :admin do … endblock. - Trek's components use
Turbo::FramesHelper, but the gemspec didn't declareturbo-rails: it only worked becauserails newadds it by default. Trek now depends onturbo-rails(>= 1.0) and requires it. Trek::Users::Validationsonly enforced the 8-character password minimum when a user was created: the check for a later change looked atcrypted_password, a column from Trek's former authentication library thathas_secure_passwordusers don't have, so an existing user could set a 1-character password (e.g. through the password reset form). It now also applies wheneverpassword_digestchanges, and still skips saves that leave the password alone. The generatedspec/models/user_spec.rbcovers both cases. Upgrade note: nothing to change; to get the new examples in an existing app, re-runrails g trek:install:model_specsand accept overwritingspec/models/user_spec.rb(or copy the two examples).rails g trek:upgrade:v2left two blank lines in.yarnrc.ymlwhere it removed thegit.etamin.studioregistry, right before thenpmPreapprovedPackagesblock it adds, so running it again changed the file. It now leaves a single one.rails g trek:scaffoldwrote an invalidconfig/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) aftertrek:installandtrek:scaffold, which is how this surfaced. Upgrade note: in models scaffolded before this version, put each attribute ofconfig/locales/models/*.en.ymlon its own line.- Freshly generated apps got a broken
db/schema.rb: they resolvejson3, whoseJSON.parsetakes 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 ArgumentErrorin place ofpages,page_paths,fragmentsandmenu_nodes, so the test database, loaded fromschema.rb, lacks those tables and the app's specs cannot run. Trek now requiresjson< 3 until Rails supports json 3, and its suite dumps the generated schema to catch a regression. Upgrade note: runbundle update trek(it movesjsonback to 2.x if you had 3), thenbin/rails db:schema:dumpand check thatdb/schema.rbhas no "Could not dump" comment left. - Three generated migrations tripped RuboCop in a freshly generated app:
trek_tag_categoriesandtrek_taggings(fromrails g trek:taxonomies) indentedcreate_tableby one space, andtrek_menu_nodesended 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 runbundle exec rubocop --autocorrect db/migrateto fix them. Trek::PagePathHelper#page_path(andpage_url) raisedNoMethodError: undefined method 'shop/product_path'for a page attached to a namespaced pageable: the route name was built frompageable.class.name.underscore, which keeps the/of the namespace. It now usespageable.model_name.singular_route_key, the key Rails' polymorphic routes use —shop_product_pathforShop::Product, andpost_pathfor an isolated engine'sBlog::Post. Non-namespaced pageables are unaffected.Trek::PagePathHelper#page_urlreturned a path, not a URL, for a page with a named route (a keyed page such asabout): that branch always called#{key}_pathand ignored theurlsuffix the other branches honour. It now callsabout_url, so mailers, sitemaps and canonical links built withpage_urlget an absolute URL. Upgrade note: you can now drop workarounds that madepage_urlabsolute for keyed pages (e.g. prefixingrequest.base_url).trek:install:scalingopinned the buildpack withBUILDPACK_URL=https://github.com/etaminstudio/trek-buildpack#v0.1.0, and Scalingo resolves a#refsuffix as a branch: the tag was looked up asarchive/refs/heads/v0.1.0, which does not exist, and every build failed withfail 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, runscalingo --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.0form 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.targetshad been copied fromtrek.fr.ymlrather 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_usersmigration tripped RuboCop'sRails/NotNullColumnonadd_column :users, :password_digest, :string, null: false, somake lint(and thelintCI job) failed in a freshly generated app — the installer's own closingmake fixalready exited non-zero on it. The column is fine as it stands: the precedingtrek_create_usersmigration creates the table empty, so there is nothing to backfill and no default is wanted on a password digest. The line now carries an inlinerubocop:disable Rails/NotNullColumnwith a comment saying why. Upgrade note: apps generated before this version can add the same disable comment todb/migrate/*_trek_users.rb. - The generated
.slim-lint.ymllisted anAllCops:key (NewCops: disable) among its linters. slim-lint validates linter names since 0.34 ("Add check for invalid keys in config files"), somake lintin a freshly generated app died withNo 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 theRuboCoplinter'signored_cops). Upgrade note: remove the twoAllCops:/NewCops: disablelines from your.slim-lint.ymlif it still has them. - The generated
config/appsignal.rbnow setsconfig.log_path = "/tmp"in production, so the AppSignal agent stops announcingappsignal: 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/tmpby itself — nothing was lost, but the message reads like a misconfiguration. Upgrade note: addconfig.log_path = "/tmp" if Rails.env.production?to aconfig/appsignal.rbwritten before this version, or regenerate it withrails g trek:install:appsignal. Trek::Phonable#normalize_phonenormalized every declared attribute from the value ofphone: a model declaringphonable_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'sdisabledoption 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:orto:) renders an inert anchor — nohref, no Turbo method — witharia-disabled="true", since<a>has nodisabledattribute. Both get a dimmed, non-interactive style (opacity: 0.5,pointer-events: none,cursor: not-allowed). Upgrade note: you can now dropclassnames: "is-disabled"-style workarounds and their CSS, and passdisabled:with a realhrefinstead of blanking the href conditionally.- The icon of a
Trek::ButtonComponentno longer shrinks next to a long label: as a flex item it was squeezed (and, with amin-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::GroupComponenthad no model to resolveactiverecord.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 passmodel_name: User.model_nametof.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 — addmodel_name: User.model_nameto thef.groupcalls inapp/views/admin/user_sessions/new.html.slimandapp/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::LinkComponentreferenced anexternalicon that didn't ship with the gem (it only resolved when the host app provided its ownapp/assets/icons/external.svg). Trek now bundlestrek/external(Lucide'sexternal-link, ISC) and the toast link uses it; the icon is also available to host apps asTrek::IconComponent.new("trek/external"). Upgrade note: you can now drop anapp/assets/icons/external.svgthat only existed to back the toast link (keep it if your own code renders the bareexternalkey). Trek::MenuComponentitems pointing at a namespaced model doubled the namespace in the path used for the active-state check (href: [:admin, :missive, Missive::List]compared againstadmin/missive/missive/lists), so those items never gotis-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 dropactive:overrides that only worked around this (e.g.active: "admin/#{model.model_name.collection}").make releasenow chains its steps with&&, so a failing step halts the run. Previously the steps were separated by;: whenrake releasefailed (e.g. RubyGems asking for an MFA code),npm publishstill ran and its output buried the error, leaving the gem unpublished while the tag and npm package were out.make releaseprompts for the RubyGems and npm one-time passwords right before each publish step (passed asGEM_HOST_OTP_CODEandnpm 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
Pageand its image — stayed in Shrine's temporarycachestorage forever when attached on create or together with a parent change. closure_tree reloads the record in itsafter_savecallback to reorder siblings, and Shrine'sactiverecordplugin resets the attacher on reload, so theafter_commitpromotion 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 generatedRecordImageUploader, 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: addplugin Trek::SavedAttachmentsReloadunderplugin :activerecordinapp/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 — runbin/rails trek:attachments:promote_cachedto promote those whose file still exists, and have the ones it reports asmissinguploaded again.
[2.2.0] - 2026-08-28
Added
Trek::Dashboard::ChartComponent, a dashboard card wrapping Chartkick — passtitle,data, atype(:line_chart,:bar_chart, …), optionalinfo/options, and acolor(default:slate); it renderspublic_send(type, data, **options)inside a coloured card matching the tiles. Chartkick is an optional dependency: the component only includesChartkick::Helperwhen it is loaded, so Trek keeps working without it.Trek::Dashboard::AnalyticsComponentgained arenders_many :chartsslot (alongside the existingsection) so charts can be dropped straight into the dashboard witha.with_chart(...). Upgrade note: to useChartComponent, addgem "chartkick"to your app andimport "chartkick/chart.js"in your admin JS bundle (Trek does not pull Chartkick in for you).Trek::Dashboard::TileComponentgained atitleoption (a large heading above the text, coloured per the tile'scolor) and awideoption (defaultfalse) that spans two tile columns. Itshrefis now optional: without one the tile renders as a plaindivinstead 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 thetrek--layout-stacked-panelframe and thestackedPanelClosertarget, as Trek's own views now do.
Fixed
- Internal links inserted through the content editor (stored as
gid://URLs) rendered as literalgid://…hrefs — or were dropped by the sanitizer — in formatted content:Prosemirror#format_contentnow resolves GlobalIDs to page paths before rendering, and the editor's Link extension accepts thegidprotocol so reopening such a link shows the linked page instead of an empty field. Trek::Form::ActionsComponent: the sticky footer styling (sticky positioning,--accent-1background,4remvertical padding) now only applies to thedefaultaspect. It was set on the root rule, so theseamlessaspect — used byTrek::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:prosopitegenerator (wired intotrek:install) sets up Prosopite N+1 query detection (#211): addsprosopiteandpg_queryto thedevelopment/testGemfile group and aconfig/initializers/prosopite.rbthat mounts the Rack middleware — so every request is scanned, Trek's engine controllers included — raisingProsopite::NPlusOneQueriesErrorin test and logging in development. It also generatesspec/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 theProsopite.pauseescape hatch. Upgrade note: existing apps can runrails g trek:install:prosopite; apps that already wired Prosopite by hand can keep their setup (the generator skips the gems whenprosopiteis already in the Gemfile and won't overwrite an existing initializer).make lintandmake fixtargets in Trek's own Makefile, mirroring the ones generated for host apps (RuboCop, slim-lint, ESLint, Stylelint, Prettier — thefixvariant autocorrects all but slim-lint). A.prettierignoreexcludes the gitignored esbuild output inapp/assets/builds/, which made the Prettier check fail on a local checkout that had runyarn build.Trek::MenuComponentitems accept anactive:option for menu entries whose pages live under other controllers: aStringis matched as acontroller_pathprefix, aRegexpagainst 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_editoraccepts anautofocus:option (defaultfalse): 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 onetrek:install:rubocopcopies into host apps) re-enablesLayout/IndentationWidthandLayout/IndentationConsistencywithEnforcedStyle: normal. rubocop-rails-omakase disables both to allow its house style of indenting methods underprivate; Trek's style keeps methods at the class-body level, and a plainrubocop --autocorrectnow fixes indentation — in generated files too. Upgrade note: copy the two cop blocks from the template (rails g trek:install:rubocopwill prompt to overwrite) into your app's.rubocop.yml, then runbundle exec rubocop --autocorrectonce; methods indented underprivateand mis-indented generated files get fixed. trek:install:scalingonow 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 theenv-setcommands to run (BUILDPACK_URLpinned to the buildpack's release tag, and theDATABASE_URL=$SCALINGO_POSTGRESQL_URLmapping). The buildpack pins Ruby/Node/Yarn from.ruby-version, the.node-versionthatrails newalready generates, andpackage.json'spackageManager, runsyarn installonce, 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, setBUILDPACK_URL=https://github.com/etaminstudio/trek-buildpack#v0.1.0, delete.buildpacks, make sure.node-versionexists (apps generated withrails new -j esbuildhave it), and check thatconfig/database.ymlreadsDATABASE_URLin production (mapped withscalingo 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.2rembottom margin) so a titled fieldset reads like a labelled group; thesectionmodifier only adds its larger font size on top. Hosts that restyled the plain fieldset title themselves can drop that override.trek:install:yarnnow installs Yarn 4.18.0 (was 4.17.0). Upgrade note: existing apps can upgrade withyarn set version 4.18.0followed byyarn install.
Fixed
trek:scaffoldnow references the model top-level qualified (::Shop::Product) in the generated controller'smodeland in theAdmin::Dashboard::MODELS_IN_MENUentry: once the scaffold creates theAdmin::Shopnamespace, a bareShopundermodule Adminresolves to that module instead of the model — the scaffolded controller raisedNameError, and previously generated code referencing aShopmodel silently got the module. The generator also warns when a namespace segment matches an existing model file thatrails g modelwould replace with a namespace module. (#201) Upgrade note: if a scaffold introduced a namespace named like an existing model, qualify that model as::Shopin the existingAdmin::…Controller#modelandMODELS_IN_MENUentries.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:postmarkis idempotent again: re-running it on an app where it had already run appended a secondconfig.action_mailer.delivery_method = :postmark/postmark_settingsblock toconfig/environments/production.rb(the gem, credentials and mailer steps were already guarded). The config injection is now skipped when the file already setsdelivery_method = :postmark. (#210)- Namespaced models (e.g.
Shop::Product, routed undernamespace :shop) now work out of the box withTrek::ResourceController(#212):object_paramsrequiresmodel_name.param_key(shop_product) instead of the demodulized element, so create/update no longer 400;model_element_routeno longer prefixes the namespace when the route key already carries it (shop_product), so the delete button and list item links buildadmin_shop_product_pathinstead of the double-namespacedadmin_shop_shop_product_path— models from isolated engines with relative naming (e.g. an engine'sBlog::Post, route keypost) keep the[:blog, :post]form; the controller definesimplicit_authorization_target(@object || model) soauthorized(params)andTrek::Reorderable#reorder's bareauthorize!find the policy; andi18n_model_genderreadsactiverecord.models.<i18n_key>.gender(shop/product), soi18n_resource_labelpicks the declared gender instead of the neutral fallback. Upgrade note: hosts that worked around this with a controller concern overridingimplicit_authorization_target/object_params/model_element_route, or a prepend onTrek::I18nHelper#i18n_model_gender, can drop them. Trek::Form::GroupComponentnow stylesinput[type="time"]likeinput[type="date"](field frame, 16rem max width, lighter picker colour);time_fields used to render as bare browser inputs.- Removed the dangling
sourceMappingURLcomment from the vendoredtom_select/css/tom-select.custom.css: the referenced.mapfile is not shipped, so every host-app esbuild build (includingassets:precompileon deploy) logged amissing-source-mapdebug warning. (#209) trek:scaffoldnow re-indents the generated controller and policy: for a namespaced model the nestedmodulewrappers were emitted without indenting the class body, tripping the host app's linters. The generator runs a targetedrubocop --autocorrect --only Layout/IndentationWidth,Layout/IndentationConsistencyon both files (forced with--onlysince rubocop-rails-omakase leaves those cops disabled). (#199)- The
config/initializers/host.rbgenerated bytrek:install:configno longer referencesActionMailer::Baseduring 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 customconfig.action_mailer.delivery_job(e.g."MailDeliveryJob") crashed with aNameErroron every production boot — breakingassets:precompileon deploy. The assignment is now deferred withActiveSupport.on_load(:action_mailer). (#208) Upgrade note: apply the same change to your app'sconfig/initializers/host.rb— replace theActionMailer::Base.default_url_options = … if Rails.env.production?line with the deferred block (see the generator template). trek:scaffoldnow generates the right controller class for a namespaced model (e.g.rails g trek:scaffold Shop::Product): the template used the still-namespacedcontroller_class_nameinside the per-namespacemodulewrappers, definingAdmin::Shop::Shop::ProductsControllerwhile the generated route expectedAdmin::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.slimgenerated bytrek:scaffoldno 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 withTrailingBlankLines: Multiple empty lines in the end of file. (#205) trek:upgrade:v2now upgrades Yarn to 4.18.0 first (applying the 0.1.26 upgrade note:yarn set version+ dropping theinteractive-toolsplugin) when the app still runs Yarn 3. Previously the generator wrotenpmPreapprovedPackages— a Yarn 4-only setting — unconditionally, which under Yarn 3 is a hard error that breaks every yarn command, including the generator's ownyarn addstep.- 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)
npmPreapprovedPackagesentry, set up automatically bytrek:installandtrek:upgrade:v2, replaces it. - Fixed whitespace in
model.en.yml.ttwhich 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) importvendor/tom_select/css/tom-select.custom.cssfrom their JS sidecars, so host-app esbuild builds failed with "Could not resolve" errors. AGENTS.mdnow describes the public distribution (RubyGems + npmjs.com, npm package renamed totrek) 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/trekto the unscopedtrek— 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 npmtrekname had already published versions up to1.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 runningrails g trek:upgrade:v2(see 0.1.27's changelog entry) — the generator now also adds thenpmPreapprovedPackagesexemption below. Afterwards, remove the now-unneeded CI variables —NPM_AUTH_TOKENandBUNDLE_GIT__ETAMIN__STUDIO(spelledBUNDLE_GIT__ETAMINSTUD__IOon apps created before the domain migration) — along with any localTREK_DEPLOY_TOKENexport, and revoke the project's Trek deploy token on GitLab. trek:installnow wires new apps to the public packages: the generatedpackage.jsondepends ontrek, the JS entrypoint imports"trek", and the generated.yarnrc.ymlno longer configures the GitLab registry/scope — instead it liststrekundernpmPreapprovedPackages, 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 releasenow publishes the npm package withnpm publish(requiresnpm loginas atrekmaintainer) instead ofyarn npm publishto the GitLab registry.
[0.1.27] - 2026-08-18
Added
trek:upgrade:v2generator: upgrades a host app to Trek 2.0 by switching it from the private GitLab packages to the public ones — thetrekgem on RubyGems and the unscopedtreknpm package. It rewrites the Gemfile (git:source →gem "trek", "~> 2.0"), drops thegit.etamin.studioBundler credentials, swaps the@etaminstudio/treknpm dependency and imports fortrek, 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 runrails g trek:upgrade:v2to move to Trek 2.0 and stop depending on the private GitLab sources and their deploy token.
Changed
trek:install:rubynow pins Ruby 4.0.6 (was 4.0.5), and the CI images (repo + generated.gitlab-ci.ymltemplate) use the matching4.0.6tag. Upgrade note: existing apps can set4.0.6in.ruby-versionand bump theci-rubyimage tag in.gitlab-ci.yml.
[0.1.26] - 2026-07-02
Changed
Trek::Localizationnow resolves the request locale through an overridablelocale_for_requestmethod (stillparams→current_user→headersby default). Apps that need another source can override it and fall back to the default chain withsuperinstead of forking the whole concern — e.g.def locale_for_request = Current.event&.locale.presence || super. No behavior change for existing consumers.trek:install:authenticationno longer injectsgenerates_token_fordeclarations into the generatedUser::password_reset(15 min) is provided automatically by Rails'has_secure_password, and:invite(1 week) now lives in theTrek::Users::Invitableconcern (which owns the rest of the invite behaviour). Upgrade note: existing apps can delete bothgenerates_token_forblocks from theirUser— Rails and the concern provide them; customise the reset window viahas_secure_password reset_token: { expires_in: … }if needed.trek:install:rubynow pins Ruby 4.0.5 (was 4.0.0), and the CI images (repo + generated.gitlab-ci.ymltemplate) use the matching4.0.5tag. Upgrade note: existing apps can set4.0.5in.ruby-versionand bump theci-rubyimage tag in.gitlab-ci.yml.trek:install:yarnnow installs Yarn 4.17.0 (was 3.8.7) and no longer imports theinteractive-toolsplugin, whoseupgrade-interactivecommand is built into Yarn 4. Yarn 4 is also what supports the minimal-age-gate exception below. Upgrade note: existing apps can upgrade withyarn set version 4.17.0followed byyarn install, and drop the now-redundantinteractive-toolsplugin.- Bumped Node to 24.18.0 (was 22.21.1) in
.node-versionand the CI images (repo + generated.gitlab-ci.ymltemplate, nowci-ruby/node-24). Upgrade note: existing apps can set24.18.0in.node-versionand bump theci-ruby/node-*image in.gitlab-ci.yml. - CI now runs
i18n-tasks missing -t diff(config inconfig/i18n-tasks.yml) so a base-locale key with no translation in another locale fails the build. (Fullhealthisn'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) andadmin.{pages,fragments}.{show.intro,reorder.success,search.alert}. Also added the Englishadmin.section/admin.sectionsflash messages that previously existed only in French. (#194) trek:install:yarnnow exempts the@etaminstudioscope from Yarn's minimal-age gate (npmMinimalAgeGate: 0undernpmScopes.etaminstudioin the generated.yarnrc.yml), so a freshly published@etaminstudio/trekrelease installs without being quarantined; the gate still applies to public packages. Upgrade note: existing apps can addnpmMinimalAgeGate: 0undernpmScopes.etaminstudioin their.yarnrc.yml. (Only relevant to the private GitLab distribution used before 2.0.0: the@etaminstudioscope is retired since then, so this scope exemption does nothing for the public unscopedtrekpackage. From 2.0.0 the equivalent istreklisted undernpmPreapprovedPackages— a Yarn 4-only setting, hence the Yarn upgrade above — whichtrek:installgenerates andtrek:upgrade:v2adds during migration, so no manual step is needed.)trek:install:localesno longer generates an invalidconfig/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
:invitetoken purpose (1 week) — distinct from the 15-minute:password_resettoken — and the generatedAdmin::UserPasswordResetsControlleraccepts 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: addgenerates_token_for :invite, expires_in: 1.week do password_salt&.last(10) endto yourUser; haveAdminMailer#inviteissue an:invitetoken (andinclude ActionView::Helpers::DateHelper); and updateAdmin::UserPasswordResetsController#set_user_by_tokento try both purposes, e.g.User.privileged.find_by_token_for(:invite, @token) || User.privileged.find_by_token_for!(:password_reset, @token). trek:installnow generates anAGENTS.md(onboarding guidance for AI coding agents: commands, conventions, and a "read the changelog on every upgrade" checklist) plus aCLAUDE.mdthat imports it viaSee @AGENTS.md, so Claude Code and other agents share one source of truth. Upgrade note: existing apps can add both files by runningrails g trek:install:agents.Trek::Filterscontroller concern: filter the index byparams[:filters](attribute equality viawhere). Callapply_filters!in yourindex.- Pedagogical
NotAppliedErrorguard onTrek::FiltersandTrek::Scopes: raises (outside production) when the concern is included butindexnever callsapply_filters!/apply_scopes!. trek:install:scalingonow addslibjemalloc2to the generatedAptfile, 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 onallowed_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 sethref: %i[admin users]and addallowed_to: allowed_to?(:index?, User)on that tile inapp/views/admin/dashboards/index.html.slim. trek:installnow points Action Mailer at the same host as the routes'default_url_options(driven byDEFAULT_HOSTNAME) in the generatedconfig/initializers/host.rb, in production only (development and test keep the host from their environment files). Previously the mailer kept Rails'example.comdefault, so invitation and password-reset email links pointed tohttps://example.com/...in production. Upgrade note: existing apps can addActionMailer::Base.default_url_options = Rails.application.routes.default_url_options if Rails.env.production?to theirconfig/initializers/host.rb(and drop any hardcodedconfig.action_mailer.default_url_optionshost).- The generated
Admin::UsersController(trek:install) seeds a random password increate_hook, so users created via the admin form — which never collects one — satisfyhas_secure_passwordand the non-nullpassword_digestcolumn. Invitees set their real password via the invite email; others via password reset. (Password seeding lives in the controller rather thanTrek::Users::Invitableso it also covers users created without an invite.) Upgrade note: ensure yourAdmin::UsersController#create_hookseeds a password when blank:@object.password = SecureRandom.alphanumeric(24) if @object.password.blank?. trek:install:cinow setsPOSTGRES_DBin.gitlab-ci.ymlfrom the application name (e.g.myapp_test) instead of the hardcodedproject_test.trek:install:yarnnow rewritesbin/setup'syarn install --check-filestoyarn install, since--check-filesis not a valid flag under Yarn berry and was breakingbin/setup(and therefore CI).trek:installnow adds a!/.env.exampleexception to.gitignoreso the generated.env.exampletemplate stays tracked (Rails ignores all/.env*files by default).trek:install:postmarknow matches the production.rb SMTP comment with or without thebin/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 :itemsfromTrek::ListComponent, which collided with itsrenders_many :itemsslot. 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 —itemsalways returns[]and everyListComponentlist 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