Sage 10 to 11 Migration with Vite + Tailwind 4 & Acorn 5
So, how to migrate a theme from Sage 10 to Sage 11? This involves several key changes due to the shift from Bud to Vite as the new build tool in Sage 11. But also due to the Tailwind 3 to 4 migration as it turns out. We also include the Acorn 4 to 5 upgrade in this post so there are a few extra steps to take for that part as well.
Vite Configuration
One of the first things I did was create a new vite.config.js
file, which replaces the old bud.config.js
used in Sage 10. Since Sage 11 now uses Vite, updating your asset pipeline configuration is essential.
Next, I replaced all references to the default sage
theme name with the name of our custom theme: nynaeve. So you wind up with this including base
adjustment
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite';
import laravel from 'laravel-vite-plugin'
import { wordpressPlugin, wordpressThemeJson } from '@roots/vite-plugin';
export default defineConfig({
base: '/app/themes/nynaeve/public/build/',
plugins: [
tailwindcss(),
laravel({
input: [
'resources/css/app.css',
'resources/js/app.js',
'resources/css/editor.css',
'resources/js/editor.js',
],
refresh: true,
}),
wordpressPlugin(),
// Generate the theme.json file in the public/build/assets directory
// based on the Tailwind config and the theme.json file from base theme folder
wordpressThemeJson({
disableTailwindColors: false,
disableTailwindFonts: false,
disableTailwindFontSizes: false,
}),
],
resolve: {
alias: {
'@scripts': '/resources/js',
'@styles': '/resources/css',
'@fonts': '/resources/fonts',
'@images': '/resources/images',
},
},
})
NB The bud.config.js
and jsconfig.json
should be removed
Package.json Update
We of course needed to update the JS package.json
. We basically copied the boilerplate code over from the Sage 11 theme:
{
"name": "nynaeve",
"private": true,
"engines": {
"node": ">=20.0.0"
},
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"translate": "npm run translate:pot && npm run translate:update",
"translate:pot": "wp i18n make-pot . ./resources/lang/nynaeve.pot --include=\"theme.json,patterns,app,resources\"",
"translate:update": "for file in ./resources/lang/*.po; do wp i18n update-po ./resources/lang/nynaeve.pot $file; done",
"translate:compile": "npm run translate:mo && npm run translate:js",
"translate:js": "wp i18n make-json ./resources/lang --pretty-print",
"translate:mo": "wp i18n make-mo ./resources/lang ./resources/lang"
},
"devDependencies": {
"@roots/vite-plugin": "^1.0.2",
"@tailwindcss/vite": "^4.0.9",
"laravel-vite-plugin": "^1.2.0",
"tailwindcss": "^4.0.9",
"vite": "^6.2.0"
}
}
I only renamed it nynaeve
and made the language files nynaeve as well.
Theme Restructuring
The scripts
directory becomes js
, the styles
directory becomes css
and fonts.css
is moved to root css
directory and no longer in css/common
. Not sure why this was done, but perhaps it does look clearer this way.
├── css
│ ├── app.css
│ ├── editor.css
│ └── fonts.css
├── fonts
│ ├── menlo-regular-webfont.woff2
│ ├── open-sans-v40-latin-300.woff2
│ ├── open-sans-v40-latin-500.woff2
│ ├── open-sans-v40-latin-600.woff2
│ ├── open-sans-v40-latin-700.woff2
│ ├── open-sans-v40-latin-800.woff2
│ └── open-sans-v40-latin-regular.woff2
├── images
│ ├── icons
│ ├── logo
│ │ └── logo-imagewize-smaller.png
│ └── profiles
│ ├── dall-e-profile image-male.webp
│ ├── dall-e-profile-female.webp
│ ├── dall-e-profile-stylish-male.webp
│ └── dall-e-stylized-illustration-woman-in-a-classic-en-profile-image.webp
├── js
│ ├── app.js
│ ├── editor.js
│ └── filters
│ └── button.filter.js
CSS & JS Updates
We did have to make changes in the main app.css
and app.js
. In the css we had to add
@import './fonts.css';
@import "tailwindcss" theme(static);
@source "../views/";
@source "../../app/";
@theme {
--font-open-sans: "Open Sans", sans-serif;
--font-menlo: "Menlo", monospace;
/* Custom colors */
--color-textbodygray: #98999a;
--color-bggray: #ebeced;
--color-bordergray: #cbcbcb;
--color-ctablue: #017cb6;
--color-ctabuttonblue: #026492;
--color-ctabuttonbluehover: #02567e;
--color-footerbg: #171b23;
--color-footertext: #465166;
}
...
to work with custom colors, our custom fonts and to import all the Sage 11 way.
In app.js we had to remove import domReady from '@roots/sage/client/dom-ready';
and use this at the beginning of the file
import.meta.glob([
'../images/**',
'../fonts/**',
]);
// Import our local domReady function with named import
import { domReady } from './utils/dom-ready';...
this loads our own domReady for the arrow up button and imports all images, fonts for the build . As for editor.js
we only updated it with the new setup so replaced the bud.js
stuff with
import domReady from '@wordpress/dom-ready';
domReady(() => {
//
});
Theme Setup Style Injection
The theme setup file site/web/app/themes/nynaeve/app/setup.php
also needs an update to use Vite for Style and JS Injection so we added
**
* Theme setup.
*/
namespace App;
use Illuminate\Support\Facades\Vite;
/**
* Inject styles into the block editor.
*
* @return array
*/
add_filter('block_editor_settings_all', function ($settings) {
$style = Vite::asset('resources/css/editor.css');
$settings['styles'][] = [
'css' => Vite::isRunningHot()
? "@import url('{$style}')"
: Vite::content('resources/css/editor.css'),
];
return $settings;
});
/**
* Inject scripts into the block editor.
*
* @return void
*/
add_filter('admin_head', function () {
if (! get_current_screen()?->is_block_editor()) {
return;
}
$dependencies = json_decode(Vite::content('editor.deps.json'));
foreach ($dependencies as $dependency) {
if (! wp_script_is($dependency)) {
wp_enqueue_script($dependency);
}
}
echo Vite::withEntryPoints([
'resources/js/editor.js',
])->toHtml();
});
/**
* Add Vite's HMR client to the block editor.
*
* @return void
*/
add_action('enqueue_block_assets', function () {
if (! is_admin() || ! get_current_screen()?->is_block_editor()) {
return;
}
if (! Vite::isRunningHot()) {
return;
}
$script = sprintf(
<<<'JS'
window.__vite_client_url = '%s';
window.self !== window.top && document.head.appendChild(
Object.assign(document.createElement('script'), { type: 'module', src: '%s' })
);
JS,
untrailingslashit(Vite::asset('')),
Vite::asset('@vite/client')
);
wp_add_inline_script('wp-blocks', $script);
});
/**
* Use the generated theme.json file.
*
* @return string
*/
add_filter('theme_file_path', function ($path, $file) {
return $file === 'theme.json'
? public_path('build/assets/theme.json')
: $path;
}, 10, 2);
...
The rest with initial theme setup seems to remain as is.
Underline Bug override
With the new Tailwind 4 and WordPress 6.6.x I also bumped into an issue with all menu links to be underlined even though we were using underline
. For that I added this to theme.json
:
...
"styles": {
"elements": {
"link": {
":hover": {
"typography": {
"textDecoration": "none"
}
},
"typography": {
"textDecoration": "none"
}
}
}
},
...
Theme.json Update
Besides the mentioned addition of no underlining to deal with WordPress 6.6 style injection I had to remove all Tailwind colors. They are automatically added via the vite.config.js
. I only kept the custom colors for them to be loaded in the editor color palette popup. So in the end it became:
{
"__preprocessed__": "This file is used to build the theme.json file in the `public/build/assets` directory. The built artifact includes Tailwind's colors, fonts, and font sizes.",
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"styles": {
"elements": {
"link": {
":hover": {
"typography": {
"textDecoration": "none"
}
},
"typography": {
"textDecoration": "none"
}
}
}
},
"settings": {
"background": {
"backgroundImage": true
},
"color": {
"custom": false,
"customDuotone": false,
"customGradient": false,
"defaultDuotone": false,
"defaultGradients": false,
"defaultPalette": false,
"duotone": [],
"palette": [
{
"color": "inherit",
"name": "Inherit",
"slug": "inherit"
},
{
"color": "currentcolor",
"name": "Current",
"slug": "current"
},
{
"color": "transparent",
"name": "Transparent",
"slug": "transparent"
},
{
"color": "#000",
"name": "Black",
"slug": "black"
},
{
"color": "#fff",
"name": "White",
"slug": "white"
},
{
"color": "#98999a",
"name": "TextBodyGray",
"slug": "textbodygray"
},
{
"color": "#ebeced",
"name": "BgGray",
"slug": "bggray"
},
{
"color": "#cbcbcb",
"name": "BorderGray",
"slug": "bordergray"
},
{
"color": "#017cb6",
"name": "CtaBlue",
"slug": "ctablue"
},
{
"color": "#026492",
"name": "CtaButtonBlue",
"slug": "ctabuttonblue"
},
{
"color": "#02567e",
"name": "CtaButtonBlueHover",
"slug": "ctabuttonbluehover"
},
{
"color": "#171b23",
"name": "FooterBg",
"slug": "footerbg"
},
{
"color": "#465166",
"name": "FooterText",
"slug": "footertext"
}
]
},
"custom": {
"spacing": {},
"typography": {
"font-size": {},
"line-height": {}
}
},
"spacing": {
"padding": true,
"margin": true,
"units": [
"px",
"%",
"em",
"rem",
"vw",
"vh"
],
"blockGap": false
},
"layout": {
"contentSize": "55rem",
"wideSize": "64rem",
"fullSize": "100%"
},
"typography": {
"customFontSize": false,
"fontFamilies": [
{
"fontFamily": "ui-sans-serif,system-ui,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\"",
"name": "Ui-sans-serif",
"slug": "sans"
},
{
"fontFamily": "ui-serif,Georgia,Cambria,\"Times New Roman\",Times,serif",
"name": "Ui-serif",
"slug": "serif"
},
{
"fontFamily": "ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace",
"name": "Ui-monospace",
"slug": "mono"
},
{
"fontFamily": "Open Sans, sans-serif",
"name": "Open Sans",
"slug": "open-sans"
},
{
"fontFamily": "Menlo, monospace",
"name": "Menlo",
"slug": "menlo"
}
],
"fontSizes": [
{
"name": "xs",
"size": "0.75rem",
"slug": "xs"
},
{
"name": "sm",
"size": "0.875rem",
"slug": "sm"
},
{
"name": "base",
"size": "1rem",
"slug": "base"
},
{
"name": "lg",
"size": "1.125rem",
"slug": "lg"
},
{
"name": "xl",
"size": "1.25rem",
"slug": "xl"
},
{
"name": "2xl",
"size": "1.5rem",
"slug": "2xl"
},
{
"name": "3xl",
"size": "1.875rem",
"slug": "3xl"
},
{
"name": "4xl",
"size": "2.25rem",
"slug": "4xl"
},
{
"name": "5xl",
"size": "3rem",
"slug": "5xl"
},
{
"name": "6xl",
"size": "3.75rem",
"slug": "6xl"
},
{
"name": "7xl",
"size": "4.5rem",
"slug": "7xl"
},
{
"name": "8xl",
"size": "6rem",
"slug": "8xl"
},
{
"name": "9xl",
"size": "8rem",
"slug": "9xl"
}
]
},
"blocks": {
"core/heading": {
"spacing": {
"padding": true,
"margin": true
},
"typography": {
"lineHeight": true
}
},
"core/paragraph": {
"typography": {
"lineHeight": true
}
},
"core/image": {
"border": {
"color": true,
"radius": true,
"style": true,
"width": true
}
}
}
}
}
This version of theme.json
is used to generate the build version together with settings in vite.config.js
Acorn Update
We also decided to update Acorn tot he latest for the latest Laravel goodies. Did that with one command
composer require roots/acorn ^5.0 -W
NB Got stuck with older beta 2 version so had to enforce updating to latest
Then reading the migration guide also needed
...
use Roots\Acorn\Application;
...
Application::configure()
->withProviders([
App\Providers\ThemeServiceProvider::class,
])
->boot();
...
in site/web/app/themes/nynaeve/functions.php
to register the bootloader instead of
if (! function_exists('\Roots\bootloader')) {
wp_die(
__('You need to install Acorn to use this theme.', 'sage'),
'',
[
'link_url' => 'https://roots.io/acorn/docs/installation/',
'link_text' => __('Acorn Docs: Installation', 'sage'),
]
);
}
\Roots\bootloader()->boot();
Installing Dependencies
After updating the theme name and configuration, you’ll need to install the required packages. Although we’re used to running yarn install
, the Sage 11 documentation recommends using npm:
- Run
npm install
from your theme directory to install all necessary dependencies (e.g.,vite
,@tailwindcss/vite
, etc.). - Run
npm run build
to compile the theme assets.
Important: You must compile the theme assets before accessing your site. If you skip this step, you’ll encounter the following error:
Vite manifest not found at [/path/to/sage/public/build/manifest.json] cannot be found.
NB we removed yarn.lock
as we are using npm
now.
Trellis Before Deployment
To do the deployment properly with Sage 11 that uses npm and different built location we also had to update trellis/deploy-hooks/build-before.yml
with
# Placeholder `deploy_build_before` hook for building theme assets on the
# host machine and then copying the files to the remote server
#
# ⚠️ This example assumes your theme is using Sage 11
#
# Uncomment the lines below if you are using Sage 11
# and replace `nynaeve` with your theme folder
#
---
- name: Install npm dependencies
command: npm install
delegate_to: localhost
args:
chdir: "{{ project_local_path }}/web/app/themes/nynaeve"
- name: Install Composer dependencies
command: composer install --no-ansi --no-dev --no-interaction --no-progress --optimize-autoloader --no-scripts --classmap-authoritative
args:
chdir: "{{ deploy_helper.new_release_path }}/web/app/themes/nynaeve"
- name: Compile assets for production
command: npm run build
delegate_to: localhost
args:
chdir: "{{ project_local_path }}/web/app/themes/nynaeve"
- name: Check for manifest
stat:
path: "{{ project_local_path }}/web/app/themes/nynaeve/public/build/manifest.json"
delegate_to: localhost
register: entrypoints_data
- name: Entrypoints missing
ansible.builtin.fail:
msg: "The theme is missing the build manifest file"
when: not entrypoints_data.stat.exists
- name: Copy production assets
synchronize:
src: "{{ project_local_path }}/web/app/themes/nynaeve/public"
dest: "{{ deploy_helper.new_release_path }}/web/app/themes/nynaeve"
group: no
owner: no
rsync_opts: --chmod=Du=rwx,--chmod=Dg=rx,--chmod=Do=rx,--chmod=Fu=rw,--chmod=Fg=r,--chmod=Fo=r