Files
ermnvldmr.com/services/www/rsbuild.config.ts
T
deytenit 24f3311006 fix(www): enable font content hashing and automated preloads (#68)
🤖 **Summary:**
- **Root cause:** Font static assets were configured with unhashed
filenames (`[name][ext]`) while served with `Cache-Control: immutable,
max-age=31536000`. This caused Cloudflare edge nodes and client browsers
to permanently cache the older Latin-only font files from Sept 2026
under `https://www.ermnvldmr.com/ru/static/font/lato-400.woff2`,
ignoring the updated Cyrillic glyphs.
- **Changes:**
- Removed explicit `filename: { font: '[name][ext]' }` in
`packages/rsbuild-config/src/lib/config/rsbuild.ts` to enable Rsbuild's
default content-hashed font filenames (`[name].[contenthash:8][ext]`).
- Updated `services/www/rsbuild.config.ts` to remove hardcoded unhashed
font preload tags and configure Rsbuild's `performance.preload` to
automatically discover and emit hashed font `<link rel="preload"
as="font" crossorigin>` tags.
2026-10-25 21:15:04 +03:00

115 lines
3.8 KiB
TypeScript

import { createReadStream, existsSync } from 'node:fs';
import { extname, join, sep } from 'node:path';
import {
defineSSGServiceConfig,
discoverEntries,
mergeConfig,
umamiAnalytics,
} from '@ermnvldmr/ssg/dev';
import { localDevServer } from '@ermnvldmr/rsbuild-config/dev';
import { THEME_INIT_SCRIPT, THEME_INIT_STYLES } from '@ermnvldmr/ui/dev';
import { TRUSTED_TYPES_INIT_SCRIPT } from '@ermnvldmr/stl';
import { localeRsbuildConfig } from '@ermnvldmr/i18n/dev';
import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin';
import { pluginMdx } from '@rsbuild/plugin-mdx';
import remarkFrontmatter from 'remark-frontmatter';
import remarkMdxFrontmatter from 'remark-mdx-frontmatter';
const MIME: Record<string, string> = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.svg': 'image/svg+xml',
'.avif': 'image/avif',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.txt': 'text/plain',
};
const UMAMI_WWW_WEBSITE_ID = '4fca5ac2-4fe4-4626-b708-933c37bfe918';
export default defineSSGServiceConfig(
mergeConfig(localeRsbuildConfig(), {
server: localDevServer({ port: 3000 }),
analytics: [
umamiAnalytics({
serverUrl: '/beacon',
scriptName: 'beacon.js',
hostUrl: '/beacon',
websiteId: UMAMI_WWW_WEBSITE_ID,
domains: 'ermnvldmr.com,www.ermnvldmr.com',
doNotTrack: true,
}),
],
dev: {
// Serve public/static/* at /static/* directly, bypassing server.base.
// Without this, server.base (e.g. /en) would push the public dir to
// /en/static/*, making /static/* unreachable in dev.
setupMiddlewares: [
(middlewares) => {
middlewares.unshift((req, res, next) => {
const url = (req as { url?: string }).url ?? '';
if (!url.startsWith('/static/')) return next();
// Strip the leading slash so the '/static/...' segment is treated as
// relative to public/ (an absolute segment would otherwise let a
// resolved path escape it), then confine the result to public/ to
// block '..' path traversal.
const publicDir = join(import.meta.dirname, 'public');
const requestedPath = (url.split('?')[0] ?? url).replace(/^\/+/, '');
const filePath = join(publicDir, requestedPath);
if (!filePath.startsWith(publicDir + sep) || !existsSync(filePath)) return next();
const mime = MIME[extname(filePath)] ?? 'application/octet-stream';
res.setHeader('Content-Type', mime);
createReadStream(filePath).pipe(res as import('node:stream').Writable);
});
return middlewares;
},
],
},
tools: {
rspack: {
plugins: [
process.env.RSDOCTOR === 'true' &&
new RsdoctorRspackPlugin({
disableClientServer: true,
output: { reportDir: './rsdoctor', mode: 'brief' },
}),
].filter(Boolean),
},
},
plugins: [
pluginMdx({
mdxLoaderOptions: {
remarkPlugins: [remarkFrontmatter, [remarkMdxFrontmatter, { name: 'frontmatter' }]],
},
}),
],
source: {
entry: discoverEntries(import.meta.dirname, './src/app/**/*.tsx'),
preEntry: ['./src/static/global.css'],
},
performance: {
preload: {
type: 'all-assets',
include: [/(lato-400|lato-700|eb-garamond-var)\..*\.woff2$/],
},
},
html: {
tags: [
{ tag: 'script', children: TRUSTED_TYPES_INIT_SCRIPT, head: true },
{ tag: 'script', children: THEME_INIT_SCRIPT, head: true },
{ tag: 'style', children: THEME_INIT_STYLES, head: true },
],
},
})
);