feat(dev): add live-reload dev server with Juice CSS inlining
- tools/server/: Node.js dev server (Express + WebSocket + fs.watch) Watches themes/ for .tmpl changes, rebuilds via Go preview command, applies Juice CSS inlining for email client compatibility, pushes live reload to browser via WebSocket. - tools/cli/dev.go: 'go run . dev' subcommand — starts Node.js server with automatic npm install, initial preview generation, and Ctrl+C graceful shutdown. - tools/tools.go: register dev command in CLI app. - .gitignore: add node_modules exclusion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
0bf982ffb9
commit
05d76a90af
6 files changed
+377
No files matched your search
@@ -0,0 +1,51 @@
|
||||
import juice from 'juice';
|
||||
|
||||
/** Juice CSS inlining options optimized for HTML email. */
|
||||
const JUICE_OPTIONS = {
|
||||
removeStyleTags: false, // keep <style> for modern clients that support it
|
||||
preserveMediaQueries: true,
|
||||
preserveFontFaces: true,
|
||||
applyWidthAttributes: true,
|
||||
applyHeightAttributes: true,
|
||||
applyAttributesTableElements: true,
|
||||
xmlMode: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Inline CSS from <style> blocks and <link> tags into inline style attributes.
|
||||
* Produces HTML that renders consistently across Gmail, Outlook, Apple Mail.
|
||||
*/
|
||||
export function inlineCSS(html) {
|
||||
return juice(html, JUICE_OPTIONS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply juice inlining to a rendered.js-style HTML snippet and return the
|
||||
* result. Handles escaped HTML (JSON-stringified) by first unescaping,
|
||||
* inlining, then re-escaping for storage in rendered.js.
|
||||
*/
|
||||
export function inlineRenderedHTML(rawhtml) {
|
||||
try {
|
||||
// If this is already a standalone HTML document fragment
|
||||
if (rawhtml.includes('<style') || rawhtml.includes('</style>')) {
|
||||
return inlineCSS(rawhtml);
|
||||
}
|
||||
// Otherwise pass through unchanged (inline-only content)
|
||||
return rawhtml;
|
||||
} catch (err) {
|
||||
console.error('Juice inline error:', err.message);
|
||||
return rawhtml;
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url.endsWith(process.argv[1])) {
|
||||
// CLI mode: juice <input> [output]
|
||||
const { readFileSync, writeFileSync } = await import('fs');
|
||||
const input = process.argv[2];
|
||||
const output = process.argv[3];
|
||||
if (!input) { console.error('Usage: node inliner.mjs <input.html> [output.html]'); process.exit(1); }
|
||||
const html = readFileSync(input, 'utf8');
|
||||
const result = inlineCSS(html);
|
||||
if (output) { writeFileSync(output, result, 'utf8'); console.log(`Inlined → ${output}`); }
|
||||
else { console.log(result); }
|
||||
}
|
||||
Reference in new issue
Block a user