Blog Underlying Stack Update Log - MuxiaoWFSkip to main content
This page was machine-translated and may contain errors or omissions. / 本页面为机器翻译,可能存在错漏。
Blog Underlying Stack Update Log

Blog Underlying Stack Update Log

Thu Jul 30 2026
1729 words · 13 minutes

A few days ago, in a moment of madness and boredom, I suddenly wanted to update the underlying libraries of this blog. Plus, being someone who is purely idle and loves chasing the latest versions, this is just a write-up of my own notes. (The page is written very simply, for reference only — update with caution.)

It also happens to be the blog’s first anniversary, so consider this a “birthday celebration 😂”

First, the previous situation: it was an Astro 5 + Tailwind 3 + DaisyUI 4 architecture, and after the upgrade it became Astro 7 + Tailwind 4 + DaisyUI 5, with all the Astro companion components on the latest version. But note that when I was writing this, TypeScript 7 still didn’t support Astro, so I only used the latest TypeScript 6.

Let me mention the browser compatibility issues the update causes: Tailwind v4 requires Chrome ≥111, Edge ≥111, Firefox ≥128, Safari ≥16.4; the update will break compatibility on older devices.


Update Begins

Simple Version Update

package.json

// Added dependencies
"@astrojs/markdown-remark": "^7.2.2" // standalone markdown processing
"@iconify/tailwind4": "^1.2.3" // Tailwind 4 version of Iconify
"@tailwindcss/vite": "^4.3.3" // Tailwind Vite plugin (replaces @astrojs/tailwind)
// Removed dependencies
"@astrojs/tailwind": "^6.0.2" // no longer needed
"@iconify/tailwind": "^1.2.0" // replaced with the tailwind4 version
"@typescript-eslint/parser": "^8.48.1" // no longer needed
"sass-embedded": "^1.93.3" // SCSS → CSS migration
"medium-zoom": "^1.1.0" // removed
"remark": "^15.0.1" // replaced by @astrojs/markdown-remark
"typescript": "^5.9.3"
"typescript": "^6.0.3"
// Everything else is a direct version bump

pnpm-workspace.yaml

allowBuilds:
sharp: true
swup: false
unrs-resolver: true // required by Astro 7

astro.config.mjs Astro config migration

import tailwind from "@astrojs/tailwind";
const playformCompress = (await import("@playform/compress")).default; // sync import
import tailwindcss from "@tailwindcss/vite"; // Tailwind 4 uses the Vite plugin
import { unified } from "@astrojs/markdown-remark"; // standalone markdown processor
export default defineConfig({
integrations: [
// ... other integrations
tailwind({
configFile: "./tailwind.config.mjs",
}),
// Tailwind 4 no longer needs an integration
playformCompress(),
playformCompress({
Image: false, // must disable, sharp version incompatible
JavaScript: false, // JS handled by terser
}),
],
markdown: {
remarkPlugins: [...],
rehypePlugins: [...],
processor: unified({ // new markdown processor API
remarkPlugins: [...],
rehypePlugins: [...],
}),
},
vite: {
plugins: [tailwindcss()], // Tailwind 4 Vite plugin
build: {
cssMinify: "esbuild", // required! lightningcss doesn't support @apply
},
css: {
preprocessorOptions: {
scss: { api: "modern-compiler" }, // remove SCSS config
},
},
},
style: {
scss: {
includePaths: ["./src/styles"], // remove SCSS config
},
},
});
  • cssMinify: "esbuild": This is a mandatory option for the Tailwind 4 migration. Vite uses lightningcss to minify CSS by default, but it doesn’t recognize Tailwind’s @apply directive and will output about 120+ warnings. esbuild stays silent on unknown at-rules.
  • @playform/compress Image disabled: The bundled sharp 0.34.5 throws a colourspace: parameter space not set error when processing PNGs without an ICC profile. It’s recommended to pre-optimize images in the project, so no second compression is needed.
  • @playform/compress JavaScript disabled: JS minification is already handled by @rollup/plugin-terser, avoiding duplicate processing.

Tailwind 4 switches to a CSS-first configuration and no longer needs tailwind.config.mjs: it’s recommended to refer to the Tailwind 4 official upgrade guide, which includes an update plugin that can theoretically save you some of the deprecated/modified CSS style adaptations (mentioned later).

// delete tailwind.config.mjs
/** @type {import('tailwindcss').Config} */
export default {
content: ["./src/**/*.{astro,html,js,md,mdx,svelte,ts,tsx,vue}"],
plugins: [daisyUI, typography, addDynamicIconSelectors()],
daisyui: {
themes: ["winter", "night"],
darkTheme: "night",
logs: false,
},
};

Create src/styles/tailwind.css

@import "tailwindcss";
@import "./global.css"; /* import custom global styles */
@theme {
--font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
@plugin "@tailwindcss/typography";
@plugin "@iconify/tailwind4";
@plugin "daisyui" {
themes: winter --default, night --prefersdark;
logs: false;
}
  1. @theme directive replaces theme.extend — custom theme values are defined in the @theme block
  2. @plugin directive replaces plugins: [...] — Tailwind plugins are imported via @plugin
  3. No content config needed — Tailwind 4 auto-detects template files
  4. @import replaces @tailwind base/components/utilities

DaisyUI config changes

// DaisyUI v4 (tailwind.config.mjs)
daisyui: {
themes: ["winter", "night"],
darkTheme: "night",
logs: false,
}
// DaisyUI v5 (tailwind.css)
@plugin "daisyui" {
themes: winter --default, night --prefersdark;
logs: false;
}

Some breaking changes; see the official docs for more.

Featurev4v5
Config methodJS objectCSS @plugin directive
input-bordered✅ exists❌ removed, input has border by default
btn-outlinenormalbehavior slightly different
shadow-sm❌ changed to shadow-xs
collapse componentHTML structure❌ removed, must implement yourself
  • Many component style behaviors changed subtly in DaisyUI 5; each component needs to be checked individually

Detailed Update Adaptations

Content Layer API changes

Astro 7’s Content Layer API is significantly different from Astro 5’s.

Create src/content.config.ts; you can directly rename the original src/content/config.ts for clarity.

import { defineCollection } from "astro:content";
import { z } from "astro/zod";
import { glob } from "astro/loaders";
const blog = defineCollection({
loader: glob({
pattern: ["**/*.{md,mdx}"],
base: "./src/content/blog",
}),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updated: z.coerce.date().optional(),
// ... other fields
}),
});
export const collections = { blog };
Old API (Astro 5)New API (Astro 7)Explanation
blog.slugblog.idslug renamed to id
blog.render()render(blog)render becomes a standalone function
getCollection() from astro:contentunchangedbut you must import render separately

Code migration example

import { getCollection } from "astro:content";
import { getCollection, render } from "astro:content";
// slug → id
<a href={`/blog/${post.slug}`}>
<a href={`/blog/${post.id}`}>
// render() call
const { Content } = await blog.render();
const { Content } = await render(blog);
// getStaticPaths
params: { slug: blog.slug },
params: { slug: blog.id },

CSS System Migration: SCSS → Tailwind CSS

The project originally used SCSS (global.scss) and now migrates to plain CSS (global.css), leveraging Tailwind 4’s @apply + CSS nesting.

File structure changes

src/styles/global.scss (719 lines, SCSS)
tailwind.config.mjs (JS config)
src/styles/global.css (1207 lines, plain CSS)
src/styles/tailwind.css (Tailwind 4 entry)
  1. @tailwind directive removed — global styles no longer need @tailwind base/components/utilities
  2. SCSS feature replacements:
    • @mixin / @include → CSS @layer + reusable classes
    • $variable → CSS custom property --variable
    • nested selectors → native CSS Nesting (widely supported)
  3. @apply stays available — you can still use @apply in global.css

astro.config.mjs remove SCSS config (partly modified above already)

style: {
scss: {
includePaths: ["./src/styles"],
},
},
vite.css.preprocessorOptions.scss: {
api: "modern-compiler",
},

The Tailwind component switching mentioned above can theoretically be done with the official plugin, but be aware that the plugin seems to do simple search-and-replace. If you used a matching variable name in some files, it will be wrongly replaced too. It’s recommended to re-build and retry before committing.

Other

js-yaml ^4 → ^5

import yaml from "js-yaml";
import * as yaml from "js-yaml";

Waline component’s define:vars syntax change (Astro 7 doesn’t support inline referencing of Astro.props.X in define:vars)

serverURL: Astro.props.serverURL,
lang: Astro.props.lang ?? "zh",
// destructure first, then pass the variable name
const { serverURL, lang = "zh" } = Astro.props;
// reference the variable directly in define:vars
serverURL,
lang,

Other than that, Astro 7’s whitespace rules changed multi-line inline elements: the old version auto-generated spaces for line breaks; the new JSX-style version does not generate spaces, so inline text is directly concatenated. The most noticeable case is probably that the license author and the license name no longer have a space between them. There are other places you can adjust yourself.

Also, regarding fonts: Tailwind seems to have updated its fonts in v4 (or system fonts were moved to the front). To keep the v3 fonts, you can add the following to tailwind.css:

@theme {
--font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}

One more thing: previously it seemed that md:xx lg:xx would cause some components to display with the md screen style even on the lg screen. After the update this is fixed, so the lg screen correctly displays the component style, which can make things appear a slightly different size. To preserve the old behavior you can adjust some components (I just changed lg to 2xl and it was fixed XD).

Yet another thing: you can also add to global.css:

@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
input::placeholder,
textarea::placeholder {
color: var(--color-gray-400);
}
/* (optional) cursor style for clickable elements */
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
/* (recommended) force uniform top/bottom margin for hr, mainly the license area */
hr {
margin-block: 1rem !important;
}
}

If you updated pagefind from 1.4.0 to 1.5.x, you may also need to update the search page, which may report “Pagefind: WASM Error (No pointer)”.

  1. bundlePath resolution failure: the @pagefind/default-ui package determines the location of pagefind.js by checking document.currentScript. But when loaded via dynamic import() (and since Astro bundles via Vite), document.currentScript is always null. So bundlePath becomes undefined.
  2. The build output uses a wrong baseUrl: the current dist/ build passes baseUrl: "/pagefind/", which constructs wrong links like /pagefind/blog/article-path (it should use / as the baseUrl) — but this is not the root cause; the root cause is the missing bundlePath.

So the fix is:

const search = new PagefindUI({
element: "#pagefind-search",
bundlePath: "/pagefind/",
showImages: false,
...
});

If you want, you can also migrate the pagefind default-ui to component-ui; tinker with it if you’re bored.


Postscript

Damn, after deploying the update, no matter how I refreshed it kept erroring, yet it worked fine locally. I thought it was a component issue of mine, but it turned out to be a browser cache problem. I spent a long time deleting the cache; at first I thought it was on the Vercel/Cloudflare side, but then I found that only Edge couldn’t display it properly. I fiddled with the Service Worker for a while and still couldn’t fix it, until finally I cleared everything directly in DevTools → Application → Storage and it worked. Took me a whole day…

Summary

  1. Update all dependency versions in package.json
  2. Update pnpm-workspace.yaml to add unrs-resolver: true
  3. Run pnpm install
  4. Create src/styles/tailwind.css as the Tailwind 4 entry
  5. Delete tailwind.config.mjs
  6. Migrate global.scssglobal.css (remove @tailwind directive)
  7. Update astro.config.mjs (Tailwind Vite plugin, Markdown processor, cssMinify, etc.)
  8. Create src/content.config.ts
  9. Batch-replace Tailwind utility classes across all .svelte and .astro files
  10. Replace blog.slugblog.id, blog.render()render(blog)
  11. Run pnpm build to verify a successful build

In closing: I’m exhausted, this was purely thankless busywork, just out of sheer idleness. My personal feeling is that the build was just a bit faster — on Vercel the original build time of two and a half minutes is now just under two minutes. But what does that have to do with me… the range of supported devices actually got smaller.


Thanks for reading! Follow me if you'd like~

Blog Underlying Stack Update Log

Thu Jul 30 2026
1729 words · 13 minutes
Cover
Sample track
Sample artist
Cover
Sample track
Sample artist
0:00 / 0:00