🤖 **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.
115 lines
3.8 KiB
TypeScript
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 },
|
|
],
|
|
},
|
|
})
|
|
);
|