fix: resolve @custom-media queries in publisher CSS
Browse filesThe publisher reads raw CSS source files that use @custom-media
(e.g. --bp-content-collapse). Browsers don't support this natively -
PostCSS resolves it in the Vite build but not for the publisher.
This broke ALL responsive breakpoints in published HTML: the grid
never collapsed, mobile TOC elements were unaffected by media queries,
and the layout was broken on narrow screens.
Added resolveCustomMedia() that extracts @custom-media declarations
and replaces their usage with the resolved values before injection.
Made-with: Cursor
backend/src/publisher/index.ts
CHANGED
|
@@ -41,6 +41,31 @@ export interface PublishCSS {
|
|
| 41 |
* Reads the template CSS foundation + editor extensions.
|
| 42 |
* Tries multiple paths: dev source, Docker copy, relative fallback.
|
| 43 |
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
function loadCSS(): PublishCSS {
|
| 45 |
const candidates = [
|
| 46 |
join(__dirname, "..", "..", "..", "frontend", "src", "styles"),
|
|
@@ -64,15 +89,29 @@ function loadCSS(): PublishCSS {
|
|
| 64 |
.map((f) => readSafe(join(dir, "components", f)))
|
| 65 |
.join("\n");
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
return {
|
| 68 |
-
variables:
|
| 69 |
-
reset:
|
| 70 |
-
base:
|
| 71 |
-
layout:
|
| 72 |
-
print:
|
| 73 |
-
editorTokens:
|
| 74 |
-
article:
|
| 75 |
-
components:
|
| 76 |
};
|
| 77 |
}
|
| 78 |
}
|
|
|
|
| 41 |
* Reads the template CSS foundation + editor extensions.
|
| 42 |
* Tries multiple paths: dev source, Docker copy, relative fallback.
|
| 43 |
*/
|
| 44 |
+
/**
|
| 45 |
+
* Resolve @custom-media at-rules so the CSS works in browsers.
|
| 46 |
+
* PostCSS handles this in the Vite build, but the publisher reads raw source files.
|
| 47 |
+
*/
|
| 48 |
+
function resolveCustomMedia(allCss: string): string {
|
| 49 |
+
const mediaMap = new Map<string, string>();
|
| 50 |
+
|
| 51 |
+
// Extract @custom-media declarations
|
| 52 |
+
const declRegex = /@custom-media\s+(--[\w-]+)\s+\(([^)]+)\)\s*;/g;
|
| 53 |
+
let match: RegExpExecArray | null;
|
| 54 |
+
while ((match = declRegex.exec(allCss)) !== null) {
|
| 55 |
+
mediaMap.set(match[1], `(${match[2]})`);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// Remove @custom-media declarations
|
| 59 |
+
let resolved = allCss.replace(/@custom-media\s+--[\w-]+\s+\([^)]+\)\s*;/g, "");
|
| 60 |
+
|
| 61 |
+
// Replace @media (--custom-name) with the resolved value
|
| 62 |
+
for (const [name, value] of mediaMap) {
|
| 63 |
+
resolved = resolved.replaceAll(`(${name})`, value);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
return resolved;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
function loadCSS(): PublishCSS {
|
| 70 |
const candidates = [
|
| 71 |
join(__dirname, "..", "..", "..", "frontend", "src", "styles"),
|
|
|
|
| 89 |
.map((f) => readSafe(join(dir, "components", f)))
|
| 90 |
.join("\n");
|
| 91 |
|
| 92 |
+
// Read all CSS, resolve @custom-media, then split back
|
| 93 |
+
const variablesRaw = readFileSync(varsPath, "utf-8");
|
| 94 |
+
const resetRaw = readSafe(join(dir, "_reset.css"));
|
| 95 |
+
const baseRaw = readFileSync(basePath, "utf-8");
|
| 96 |
+
const layoutRaw = readSafe(join(dir, "_layout.css"));
|
| 97 |
+
const printRaw = readSafe(join(dir, "_print.css"));
|
| 98 |
+
const editorTokensRaw = readSafe(join(dir, "_editor-tokens.css"));
|
| 99 |
+
const articleRaw = readSafe(join(dir, "article.css"));
|
| 100 |
+
|
| 101 |
+
// @custom-media declarations live in _variables.css - resolve across all files
|
| 102 |
+
const allRaw = [variablesRaw, resetRaw, baseRaw, layoutRaw, printRaw, editorTokensRaw, articleRaw, componentsCss].join("\n/* __CSS_BOUNDARY__ */\n");
|
| 103 |
+
const resolved = resolveCustomMedia(allRaw);
|
| 104 |
+
const parts = resolved.split("/* __CSS_BOUNDARY__ */");
|
| 105 |
+
|
| 106 |
return {
|
| 107 |
+
variables: parts[0]?.trim() ?? "",
|
| 108 |
+
reset: parts[1]?.trim() ?? "",
|
| 109 |
+
base: parts[2]?.trim() ?? "",
|
| 110 |
+
layout: parts[3]?.trim() ?? "",
|
| 111 |
+
print: parts[4]?.trim() ?? "",
|
| 112 |
+
editorTokens: parts[5]?.trim() ?? "",
|
| 113 |
+
article: parts[6]?.trim() ?? "",
|
| 114 |
+
components: parts[7]?.trim() ?? "",
|
| 115 |
};
|
| 116 |
}
|
| 117 |
}
|