Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save johnfmorton/b8c68455ac7644737d2c4d9d105207fd to your computer and use it in GitHub Desktop.

Select an option

Save johnfmorton/b8c68455ac7644737d2c4d9d105207fd to your computer and use it in GitHub Desktop.
SETUP_BEASTIES_CRITICAL_CSS.md
# Set up Beasties Critical CSS on this Craft + Vite project
You are an AI coding agent (Claude Code, Codex, Cursor, etc.) being asked to add Beasties-based Critical CSS generation to the Craft CMS + Vite repository you are currently running inside.
**Do not start changing files immediately.** This guide is interactive. You must first inspect the repository, then ask the developer the questions in the "Confirm With the Developer" section, then make the changes. If something does not match the assumptions in this document, surface it and ask before doing anything destructive.
Your goal at the end of this process:
1. Beasties generates one critical CSS file per unique template layout.
2. The files are written to `web/dist/criticalcss/` (or the project's equivalent webroot path).
3. The Twig layout consumes them via `craft.vite.includeCriticalCssTags()` (or the project's equivalent).
4. The developer can run `npm run build:critical` locally and on the production server, with no Puppeteer, no Chrome, no DDEV image hacks, and no Apple Silicon workarounds.
The reference implementation this is modeled after lives on [SuperGeekery.com](https://supergeekery.com). Follow that pattern unless this project deviates in ways the developer confirms.
---
## Phase 1 — Research the Repository
Do all of the following before asking any questions. Use the answers to fill in the question prompts in Phase 2.
### 1.1 Confirm this is the right kind of project
Look for these signals:
- `composer.json` with `craftcms/cms` in `require`.
- `vite.config.js`, `vite.config.ts`, or `vite.config.mjs` at the project root.
- A `web/` directory (Craft's web root).
- A `package.json` with build scripts that invoke `vite`.
If any of these is missing, stop and tell the developer this looks like a different kind of project than the guide assumes. Ask whether to continue anyway.
### 1.2 Find the Vite entry point
Open the Vite config and find the `build.rollupOptions.input` object. Note the entry path — typically something like `src/js/app.ts`, `src/js/app.js`, or `resources/js/app.js`. You will need this exact string to look it up in the Vite manifest later.
If there are multiple entries, ask the developer which one produces the main CSS bundle.
### 1.3 Detect an existing critical CSS setup
Search the codebase for any of these — they mean a previous solution is already wired in and you must remove or coexist with it carefully:
- `rollup-plugin-critical` in `package.json`.
- `critters` or `beasties` in `package.json`.
- A `GENERATE_CRITICAL` environment variable referenced anywhere.
- A `CRITICAL_URL` environment variable referenced anywhere.
- A `criticalcss/` directory under `web/dist/` or similar.
- Any `import critical` / `import critters` / `import Beasties` statements.
- Calls to `craft.vite.includeCriticalCssTags()` or `craft.vite.inline(...criticalcss...)` in templates.
Make a list of what you find. **Do not delete anything yet** — you will discuss it with the developer in Phase 2.
### 1.4 Check for `craft-vite`
Look in `composer.json` for `nystudio107/craft-vite`. If present, confirm the version. If not present, note that you will either need to install it or use a different mechanism to include critical CSS in templates.
### 1.5 Identify candidate templates
List the top-level Twig templates in `templates/` — at minimum:
- `index.twig` (or whatever the homepage template is).
- Any `_pages/` or `_posts/` templates that represent unique layouts.
- Any "standalone" templates that don't extend the main layout.
You don't need to enumerate every template — you need one representative URL per *unique layout*.
### 1.6 Detect the package manager
Look for `pnpm-lock.yaml`, `yarn.lock`, or `package-lock.json`. Use whichever the project already uses. Do not switch package managers.
### 1.7 Check Node version
Open `package.json` and look for an `engines.node` field. Beasties needs Node 18+; this project should already be on at least that.
### 1.8 Find the deployment configuration
Look for any of: `DEPLOYMENT.md`, a Laravel Forge deploy script in the repo, a `Makefile`, GitHub Actions workflows, `Envoyer.yml`, or a `scripts/` directory containing deploy or build helpers. Note what runs during deploy — you will need to recommend an addition there.
### 1.9 Read the existing `.env` / `.env.example` / `example.env`
Note which is the canonical example file in this repo. You will add `CRITICAL_URL` to it.
---
## Phase 2 — Confirm With the Developer
Ask the developer the following questions in a **single batch** if your tooling supports it. Wait for answers before editing files. Use what you learned in Phase 1 to make the questions specific (e.g., quote actual template names) rather than generic.
1. **Existing setup.** "I found these references to an existing critical CSS tool: *[list what you found in 1.3]*. May I remove them as part of this migration?"
2. **Unique layouts.** "Based on the templates I see, I think these are your unique layouts: *[list]*. Can you give me one representative URL on your live or staging site for each one?"
- Phrase each as `{ uri: '/some-path', template: '_posts/_entry' }`.
3. **CRITICAL_URL.** "What URL should I use as `CRITICAL_URL` for local critical CSS generation? Your DDEV site, your staging site, or your production site?"
- Reasonable defaults: the DDEV `.ddev.site` URL for local, staging for CI, production for deploy.
4. **`craft-vite`.** "I *did / did not* find `nystudio107/craft-vite` installed. Is it OK to require it, or do you have a different way you want to inline the critical CSS?"
5. **Deploy hook.** "Your deploy script appears to be *[location]*. Should I update it to run `npm run build:critical` instead of `npm run build`, and what should I do about setting `CRITICAL_URL` on the server?"
6. **Cleanup confirmation.** "If I find old Chromium/Puppeteer install configuration (e.g., `config.applesilicon.yaml` in `.ddev/`, `PUPPETEER_*` env vars, browser binaries in deploy scripts), can I remove them too? Or do other tools in this repo still need them?"
If the developer's answer to any of these is unclear, ask again before proceeding. Better to be slow than to delete the wrong file.
---
## Phase 3 — Install and Configure
Apply the changes below. After each step, briefly tell the developer what you did so they can follow along.
### 3.1 Install dependencies
Using the project's package manager:
```bash
npm install --save-dev beasties dotenv
```
If `rollup-plugin-critical` is present and the developer confirmed in Phase 2 that it can be removed:
```bash
npm uninstall rollup-plugin-critical
```
### 3.2 Clean up the Vite config
In the Vite config file, remove any `rollup-plugin-critical` (or `critters`) usage:
- Remove the `import critical from 'rollup-plugin-critical'` line.
- Remove the plugin from the `plugins` array.
- Remove any `loadEnv`/`mode`-based `GENERATE_CRITICAL` plumbing that exists *only* to drive critical CSS. Be careful not to remove env handling used for other purposes.
If you are unsure whether a block is critical-CSS-only or general, ask the developer.
### 3.3 Create `scripts/generate-critical-css.mjs`
Create a new file at `scripts/generate-critical-css.mjs` with the content below. Replace `<VITE_ENTRY_POINT>` with the entry path you found in 1.2 (e.g., `src/js/app.ts`), and replace the `pages` array with the values the developer gave you in Phase 2 question 2.
```js
#!/usr/bin/env node
/**
* Critical CSS generator using Beasties.
*
* Runs AFTER the Vite build. Fetches HTML from a live or staging URL,
* uses Beasties to extract critical (above-the-fold) CSS, and writes
* one file per unique template layout under web/dist/criticalcss/.
*
* Usage:
* node scripts/generate-critical-css.mjs
*
* Env:
* CRITICAL_URL - base URL to fetch from (e.g., https://example.com)
*/
import 'dotenv/config'
import Beasties from 'beasties'
import { mkdir, writeFile, readFile } from 'fs/promises'
import { dirname, join, resolve } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const projectRoot = resolve(__dirname, '..')
// One entry per UNIQUE template layout. The `template` value
// determines the output filename.
const pages = [
{ uri: '/', template: 'index' },
// Add more pages here.
]
const outputDir = join(projectRoot, 'web/dist/criticalcss')
async function fetchHtml(url) {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`)
}
return response.text()
}
async function getBuiltCssInfo() {
const manifestPath = join(projectRoot, 'web/dist/.vite/manifest.json')
try {
const manifest = JSON.parse(await readFile(manifestPath, 'utf-8'))
const appEntry = manifest['<VITE_ENTRY_POINT>']
if (appEntry && appEntry.css && appEntry.css.length > 0) {
const cssFile = appEntry.css[0]
return {
absolutePath: join(projectRoot, 'web/dist', cssFile),
href: `/dist/${cssFile}`,
}
}
} catch {
// fall through to glob fallback
}
const { glob } = await import('glob')
const cssFiles = await glob('web/dist/assets/*.css', { cwd: projectRoot })
if (cssFiles.length > 0) {
const cssFile = cssFiles[0].replace('web/dist/', '')
return {
absolutePath: join(projectRoot, cssFiles[0]),
href: `/dist/${cssFile}`,
}
}
throw new Error('No CSS files found in web/dist/. Run "npm run build" first.')
}
async function generateCriticalCss(baseUrl) {
console.log(`\nGenerating critical CSS from: ${baseUrl}\n`)
await mkdir(outputDir, { recursive: true })
const cssInfo = await getBuiltCssInfo()
console.log(`Using CSS from: ${cssInfo.absolutePath}`)
console.log(`CSS href: ${cssInfo.href}\n`)
const beasties = new Beasties({
path: join(projectRoot, 'web'),
publicPath: '/',
reduceInlineStyles: true,
preload: 'none',
fonts: true,
logLevel: 'info',
})
const results = []
for (const page of pages) {
const url = `${baseUrl}${page.uri}`
const outputFilename = `${page.template}_critical.min.css`
const outputPath = join(outputDir, outputFilename)
try {
console.log(`Processing: ${url} -> ${outputFilename}`)
let html = await fetchHtml(url)
html = html.replace(/<style[^>]*>([\s\S]*?)<\/style>/gi, '<!-- existing-style-placeholder -->')
html = html.replace(/<link[^>]*rel=["']stylesheet["'][^>]*>/gi, '')
html = html.replace(
'</head>',
`<link rel="stylesheet" href="${cssInfo.href}">\n</head>`,
)
const processedHtml = await beasties.process(html)
let criticalCss = ''
const dataHrefMatch = processedHtml.match(/<style[^>]*data-href[^>]*>([\s\S]*?)<\/style>/i)
if (dataHrefMatch) {
criticalCss = dataHrefMatch[1]
} else {
for (const match of processedHtml.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/gi)) {
if (!match[0].includes('existing-style-placeholder')) {
criticalCss += match[1]
}
}
}
criticalCss = criticalCss.trim()
if (!criticalCss) {
console.warn(` Warning: no critical CSS extracted for ${page.template}`)
results.push({ page, success: false, error: 'no critical CSS extracted' })
continue
}
await mkdir(dirname(outputPath), { recursive: true })
await writeFile(outputPath, criticalCss, 'utf-8')
const sizeKb = (Buffer.byteLength(criticalCss, 'utf-8') / 1024).toFixed(2)
console.log(` Created: ${outputFilename} (${sizeKb} KB)`)
results.push({ page, success: true })
} catch (error) {
console.error(` Error processing ${page.template}: ${error.message}`)
results.push({ page, success: false, error: error.message })
}
}
const failed = results.filter((r) => !r.success)
console.log(`\nGenerated ${results.length - failed.length}/${results.length} critical CSS files`)
if (failed.length > 0) {
console.log('Failed:')
failed.forEach((r) => console.log(` - ${r.page.template}: ${r.error}`))
}
return failed.length === 0
}
const baseUrl = process.env.CRITICAL_URL
if (!baseUrl) {
console.error('Error: CRITICAL_URL environment variable is required.')
console.error('Example: CRITICAL_URL=https://example.com npm run generate-critical')
process.exit(1)
}
generateCriticalCss(baseUrl.replace(/\/+$/, ''))
.then((ok) => process.exit(ok ? 0 : 1))
.catch((err) => {
console.error('Fatal error:', err)
process.exit(1)
})
```
### 3.4 Update `package.json` scripts
Add or replace:
```jsonc
{
"scripts": {
"generate-critical": "node scripts/generate-critical-css.mjs",
"build:critical": "npm run build && node scripts/generate-critical-css.mjs"
}
}
```
If a previous `build:critical` script existed for the Puppeteer version, replace it.
### 3.5 Update `.env` / example env file
Add a `CRITICAL_URL` entry. Use the example file the developer pointed you at in Phase 1.9.
```bash
# URL to fetch HTML from when generating critical CSS.
# Local: your DDEV/staging site. Production: your live site.
CRITICAL_URL=https://example.com
```
### 3.6 Verify Twig layout integration
If `nystudio107/craft-vite` is installed, the developer's `_layout.twig` should call:
```twig
{{ craft.vite.includeCriticalCssTags() }}
```
If it does not, ask the developer where to add it. If a template inlines critical CSS manually with `craft.vite.inline(...)`, do not change it without asking.
### 3.7 Update `.gitignore`
Critical CSS is built artifact output. Make sure `web/dist/` is gitignored and that there is no exception that brings back `web/dist/criticalcss/`.
If old critical CSS files are committed to the repo, suggest:
```bash
git rm -r --cached web/dist/criticalcss/
```
But do not run this without the developer's confirmation.
### 3.8 Clean up Puppeteer / Chromium leftovers
If you found any of these in Phase 1.3 *and* the developer confirmed in Phase 2.6, delete them:
- `.ddev/config.applesilicon.yaml` (or similar) added only to install Chromium.
- `PUPPETEER_EXECUTABLE_PATH`, `PUPPETEER_SKIP_CHROMIUM_DOWNLOAD`, `CPPFLAGS=-DPNG_ARM_NEON_OPT=0` env vars in DDEV config.
- Chromium / Chrome install steps in deploy scripts.
- The `rollup-plugin-critical` config block in `vite.config.js`.
After removing DDEV config files, the developer will need to run `ddev restart`.
### 3.9 Update the deploy script
Change deploy to use `npm run build:critical` instead of `npm run build`, and ensure `CRITICAL_URL` is set in the deploy environment. Show the developer the diff before applying it to anything that gets pushed to a server.
---
## Phase 4 — Verify
Run these checks. Report results back to the developer.
1. **Build runs.** `npm run build` completes successfully.
2. **Critical script runs.** `npm run generate-critical` with `CRITICAL_URL` set produces at least one file under `web/dist/criticalcss/`.
3. **Files are non-empty.** Each generated file is at least a few hundred bytes and contains valid CSS.
4. **Twig pickup works.** Open a page that should pick up critical CSS, view source, and confirm a `<style>` block exists in the `<head>`.
5. **No Puppeteer in `node_modules`.** Run `npm ls puppeteer 2>/dev/null` and confirm nothing is pulling it in (unless something else legitimately needs it).
If any check fails, describe the failure to the developer with the exact error message and the file or step it came from. **Do not retry by guessing** — surface the problem.
---
## Phase 5 — Hand Off
When you finish, summarize for the developer:
- Which packages were added and removed.
- Which files were created, edited, or deleted.
- Which env vars need to be set on production.
- The exact deploy script change required.
- Any open question or assumption that they should sanity-check.
That is the end of the setup. Beasties does not need a browser. It does not need Apple Silicon workarounds. It does not need a 20-package list in your DDEV config. If you came here from a Puppeteer-based critical CSS pipeline, you can now delete a lot of files and sleep slightly better.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment