v1.5.0
v1.5.0
This release brings Laravel Octane to the starter kit, moves the product and subscription catalog out of configuration files and into the database, lets configured descriptions be translated per locale, and improves frontend loading performance, accessibility, and page metadata.
Highlights
- Laravel Octane is installed, with FrankenPHP as the default server, and the application is prepared for long-lived workers.
- In order to support Octane, products and subscriptions config are now stored in the database instead of
config/env/products.phpandconfig/env/subscriptions.php. - The products and plans saved in the billing editors are now the checkout allowlist, replacing the
I_S_*_ALLOWED_PRICESandI_S_*_ALLOWED_DISCOUNTSenvironment variables. - Non-critical dependencies and layouts are lazy-loaded, and the primary font is preloaded.
- Suspicious activity notifications are sent again, and three listeners no longer run twice per event.
- Public pages carry meta descriptions, and navigation labels are translated instead of hard-coded in English.
Added
Laravel Octane
- Added
laravel/octaneand publishedconfig/octane.php, withfrankenphpas the default server. AddedOCTANE_SERVERandOCTANE_HTTPSto.env.example. - Added
App\Octane\Listeners\WarmRedisConnections, registered onWorkerStarting. It pings the Redis connections used by the session driver and the default cache store when a worker boots, so the first request does not pay the connection latency. - Enabled Spatie Permission's
register_octane_reset_listener, so the permission registrar is reset between requests instead of holding a stale cache in the worker. - Added
DevCommands::artisan('serve', 'server')toAppServiceProvider. Octane registersoctane:start --watchas theserverprocess ofphp artisan devat vendor priority, which would download a FrankenPHP binary into the project root on first run; registering from userland outranks it and keeps local development on the built-in server. - Added
frankenphpand/public/frankenphp-worker.phpto.gitignore. - Added feature coverage in
tests/Feature/DevCommandsTest.phpand unit coverage intests/Unit/Listeners/WarmRedisConnectionsTest.php. - Added the
billing_product_configurationsandbilling_subscription_configurationstables, theApp\Models\BillingProductConfigurationandApp\Models\BillingSubscriptionConfigurationmodels, and theApp\Concerns\StoresBillingConfigurationtrait they share.
Localized product and subscription descriptions
- A configured product, subscription group, or plan
descriptionmay now be an array keyed by locale. Only the entry matching the current locale is used. - A plain string keeps working. When it is a Laravel translation key it still resolves in every locale; otherwise it is literal copy that only applies to the application's fallback locale.
- Added
App\Rules\LocalizedText, used to validate a description that is either a string or an array keyed by an available locale, with a 5,000 character limit per value.
Link prefetching
- Added the
enable_link_prefetchingoption toconfig/inertia-start.php, built from the newI_S_ENABLE_LINK_PREFETCHINGenvironment variable and enabled by default. It is shared with the frontend as thelinkPrefetchingEnabledprop.
Page titles and metadata
- Added
messages.meta.home,messages.meta.login,messages.meta.register,messages.meta.password_lost,messages.meta.terms_of_service, andmessages.meta.privacy_policyin English, Spanish, and French. - The welcome, login, registration, registration completion, forgotten password, terms of service, privacy policy, and buy pages now set a meta description. The registration and forgotten password pages also gained their missing title.
MetaDatanow gives every tag it renders ahead-key, so a page that sets its own description or Open Graph values replaces the shared ones instead of adding a second tag.- Added browser coverage asserting that each public page exposes a meta description.
Accessibility
- Added the
messages.nav_menu,messages.nav_to,messages.nav_to_item,messages.sub_nav,messages.current_nav, andmessages.nav_none_selectedtranslations in English, Spanish, and French, replacing hard-coded English labels inAppHeaderandBreadcrumbs. - The mobile navigation trigger gained a screen-reader label, the breadcrumb group select is now wrapped in a
BreadcrumbItemso it sits in the breadcrumb list, and the language switcher trigger gained an accessible name.
Other
- Added
pooledanddirecthost settings to the PostgreSQL connection inconfig/database.php, built from the newDB_POOLEDandDB_DIRECT_HOSTvariables, for databases reached through a connection pooler. - Added
tests/Feature/EventListenerRegistrationTest.php, asserting that each application listener is registered exactly once for the event it handles. - Added
tests/Unit/frontendBundle.test.mjsandtests/Unit/persistentLayout.test.mjs, covering the lazy-loaded dependencies, the font preload, the date locale handling, and the persistent page layout. - Added browser coverage asserting that the language switcher updates the page without reloading the document.
Changed
Billing
BillingServicenow reads products and subscription groups from the database instead ofconfig('inertia-start.products')andconfig('inertia-start.subscriptions').- The checkout allowlist is now derived from the saved configuration: the price identifiers and discounts of the configured products and plans are the only ones accepted at checkout. There is no longer a
*allowlist, so a price or discount that is not configured is rejected with a404. - The billing editors no longer write PHP files. Saving now stores rows and takes effect immediately, so the "configuration is cached" warning and the
configurationIsCachedpage prop were removed, andbilling.config_editor.savedno longer takes a:fileplaceholder. - The product editor now fails validation when a selected price cannot be resolved from the provider catalog, when the selected prices do not share one active currency, or when a selected discount cannot be found. It previously saved the product without its price snapshot.
- Discount snapshots now respect the provider's own
enabled_for_checkoutflag, and the editors only offer discounts that are enabled for checkout.
Frontend performance
- Every layout except
AppLayoutis now loaded withdefineAsyncComponent, so a visit only downloads the shell it renders. useContext()now imports@thumbmarkjs/thumbmarkjsdynamically after the component mounts, and stops before registering its listeners when the component was unmounted meanwhile.configureEcho()was removed from the browser entry point, keeping the unused realtime client out of the initial bundle.- The Inter variable font file is now preloaded from
app.blade.php. - The manual
vue-vendorchunk group was removed fromvite.config.ts, leaving dependency chunking to Rolldown. - English dates now use date-fns' built-in locale instead of downloading the
en-USchunk, and switching back to English clears a previously selected locale.
Localization
- The switcher reads its languages from the live page props, so it keeps pointing at the current page's URLs when it sits in a persistent layout.
resolveCurrentLocale()is now backed by a reactive reference, so computed route helpers re-resolve after a locale change. Itswindow.history.statefallback was removed, since the synchronized locale and the locale meta tag already cover client and server rendering.
Events
- Application listeners now rely on Laravel's automatic listener discovery. The manual
Event::subscribe()andEvent::listen()calls were removed fromAppServiceProvider, along with thesubscribe()methods of the event subscribers. MediaEventSubscriber::conversionCompleted()was renamed tohandleConversionCompleted(), andSecurityEventSubscriber::notifyThrottle()tohandleTooManyAttempts(), so discovery picks them up.
Other
- The "or" separators on the login, registration, registration completion, and password confirmation pages now use the page background on small screens and the card background from the
lgbreakpoint, instead of always using the card background. --muted-foregroundis now#5d5d5din the gray, neutral, slate, stone, and zinc secondary themes, improving contrast against light backgrounds.- The base test case now calls
withoutVite(), so backend tests no longer depend on a stale or missing frontend build manifest. - Upgraded the bundled Inertia Table to 1.0.1, which adds accessible names to the per-page select and the page number input in the pagination.
- Synced with
laravel/vue-starter-kitup to commitd282e81: auth inputs and the passkey name field use thev-focusdirective instead of theautofocusattribute,PasswordInputgained anautofocusprop, and the directive is now typed through Vue'sGlobalDirectivesinterface.
Fixed
- Fixed suspicious activity notifications never being sent. Its handler is now discovered automatically and covered by a test that dispatches the event.
- Fixed three listeners running twice for every event.
SessionEventSubscriber::handleSuccessfulLogin(),RecordBillingCheckout, andProvisionGuestCheckoutCustomerwere registered manually inAppServiceProvideron top of being discovered automatically, so each login, Stripe webhook, Paddle webhook, and completed Paddle transaction invoked them twice. - Fixed the
focusdirective being missing during server-side rendering. - Fixed the language switcher using stale page properties when rendered inside a persistent layout, which could send the visitor to the previous page's localized URL.
- Fixed shared meta tags being duplicated when a page set its own description or Open Graph values.
Removed
- Removed the
I_S_STRIPE_ALLOWED_PRICES,I_S_STRIPE_ALLOWED_DISCOUNTS,I_S_PADDLE_ALLOWED_PRICES, andI_S_PADDLE_ALLOWED_DISCOUNTSenvironment variables and their config keys. The saved product and subscription selections are now the allowlist. - Removed the
inertia-start.productsandinertia-start.subscriptionsconfig keys.config/env/products.phpandconfig/env/subscriptions.phpare no longer read. - Removed
App\Listeners\PaddleEventListener, an unused example listener. - Removed the unused
Helpers::displaySettings(),Helpers::dumpMemory(),Helpers::increasePhpLimits(), andHelpers::generateArrayHash()helpers. - Removed
tests/Unit/navigation.test.mjs, whose assertions are now covered by the persistent layout and bundle tests.
Dependencies
- Added
laravel/octane2.20. - Updated minor and patch Composer and npm dependencies.
Upgrade Guide
- Run
composer install. - Run
npm install. - Run
php artisan migrateto create thebilling_product_configurationsandbilling_subscription_configurationstables. - Rebuild frontend assets with
npm run buildor your normal frontend workflow. - Move your billing configuration into the database.
config/env/products.phpandconfig/env/subscriptions.phpare no longer read, and both new tables start empty, so every/buy/{product}and/subscribe/{group}/{plan}page returns404until you re-create your catalog from/admin/billing/productsand/admin/billing/subscriptions. Keep the old files at hand while you do it, then delete them. - Remove
I_S_STRIPE_ALLOWED_PRICES,I_S_STRIPE_ALLOWED_DISCOUNTS,I_S_PADDLE_ALLOWED_PRICES, andI_S_PADDLE_ALLOWED_DISCOUNTSfrom your.envfile. Saved products and plans are now the allowlist, and a wildcard allowlist no longer exists: if your own code starts a checkout with a price or discount that is not part of a configured product or plan, configure it in the editors first, or that checkout will return404. - If your code reads
config('inertia-start.products')orconfig('inertia-start.subscriptions'), readBillingProductConfiguration::configuration()orBillingSubscriptionConfiguration::configuration()instead. Both return the same array shape, ordered, with the row'sslugmerged in. - If you customized
resources/js/pages/admin/billing/ProductsConfig.vueorSubscriptionsConfig.vue, merge the newlocalesprop and the per-locale description fields, and drop theconfigurationIsCachedprop. If you customizedlang/*/billing.php, note thatconfig_editor.savedno longer takes a:fileplaceholder and thatconfig_editor.config_cachedandconfig_editor.file_pathwere replaced byconfig_editor.database_storage. - To translate a description, save one per locale from the editors, or store an array keyed by locale. A plain string that is not a translation key now only shows in the fallback locale, and other locales fall back to the generic buy page description.
- Installing Octane does not change how your application is served.
php artisan devdeliberately stays onphp artisan serve, andconfig/octane.phpis already published. To serve with Octane, addOCTANE_SERVERandOCTANE_HTTPSto your.envfile, install the server binary for your chosen driver, and start it withphp artisan octane:start. Review any singleton, static property, or long-lived state in your own code before doing so, since workers keep the application booted between requests. - If you registered your own listeners in a service provider, check them against the new
EventListenerRegistrationTest: Laravel discovers any listener method namedhandleor starting withhandlethat type-hints its event, so a manual registration on top of that runs the listener twice. - If you customized
MediaEventSubscriberorSecurityEventSubscriber, rename their handlers tohandleConversionCompleted()andhandleTooManyAttempts(), or register them manually, otherwise they stop being called. - If you used
Helpers::displaySettings(),Helpers::dumpMemory(),Helpers::increasePhpLimits(), orHelpers::generateArrayHash(), copy the implementation you need into your own code. - Link prefetching stays enabled. Set
I_S_ENABLE_LINK_PREFETCHING=falseto turn it off, and if you customized the navigation components, the breadcrumbs, or the user menu, replace their hard-codedprefetchattribute with:prefetch="$page.props.linkPrefetchingEnabled". - If you customized
PageLayout.vue, merge the asynchronous layout imports, and if you customizedresources/js/ssr.ts, registerFocusPluginthere too. - If you customized
resources/views/app.blade.php, add the font preload link, pointing it at your own font file if you replaced Inter. - If you customized
MetaData.vue, add ahead-keyto each tag, and set a description on your own pages so they do not inherit another page's. - If you overrode
--muted-foregroundin a secondary theme, or relied on the previous value, review the new contrast. - If you use PostgreSQL behind a connection pooler, set
DB_POOLED=trueand pointDB_DIRECT_HOSTat the direct host used for operations the pooler cannot serve.