diff --git a/resources/inline.js b/resources/inline.js
new file mode 100644
index 00000000..84ed3096
--- /dev/null
+++ b/resources/inline.js
@@ -0,0 +1,157 @@
+// Copyright (c) 2026 Cesanta Software Limited
+// All rights reserved
+
+const fs = require('fs');
+const path = require('path');
+const https = require('https');
+const http = require('http');
+
+const args = process.argv.slice(2);
+let outputFile = null;
+let inputFile = null;
+
+for (let i = 0; i < args.length; i++) {
+ if (args[i] === '-o' && i + 1 < args.length) {
+ outputFile = args[++i];
+ } else if (!args[i].startsWith('-')) {
+ inputFile = args[i];
+ }
+}
+
+if (!inputFile) {
+ console.error('Usage: node inline.js [-o OUTPUT_FILE] HTML_FILE');
+ process.exit(1);
+}
+
+const baseDir = path.dirname(path.resolve(inputFile));
+const html = fs.readFileSync(inputFile, 'utf8');
+
+function fetchUrl(url) {
+ return new Promise((resolve, reject) => {
+ const client = url.startsWith('https') ? https : http;
+ client.get(url, res => {
+ let data = '';
+ res.on('data', chunk => data += chunk);
+ res.on('end', () => resolve(data));
+ res.on('error', reject);
+ }).on('error', reject);
+ });
+}
+
+async function inlineCss(match) {
+ const href = match.match(/href=["']([^"']+)["']/)[1];
+ let css = '';
+ if (href.startsWith('http')) {
+ css = await fetchUrl(href);
+ } else {
+ const cssPath = path.join(baseDir, href);
+ css = fs.readFileSync(cssPath, 'utf8');
+ }
+ return ``;
+}
+
+async function inlineJs(match) {
+ const match2 = match.match(/src=["']([^"']+)["']/);
+ if (!match2) return match;
+ const src = match2[1];
+ let js = '';
+ if (src.startsWith('http')) {
+ js = await fetchUrl(src);
+ } else {
+ const jsPath = path.join(baseDir, src);
+ js = fs.readFileSync(jsPath, 'utf8');
+ }
+ return ``;
+}
+
+async function inlineImg(match) {
+ const match2 = match.match(/src=["']([^"']+)["']/);
+ if (!match2) return match;
+ const src = match2[1];
+ const ext = path.extname(src).toLowerCase();
+ let mime = 'image/png';
+ if (ext === '.svg') mime = 'image/svg+xml';
+ else if (ext === '.jpg' || ext === '.jpeg') mime = 'image/jpeg';
+ else if (ext === '.gif') mime = 'image/gif';
+ else if (ext === '.webp') mime = 'image/webp';
+ else if (ext === '.ico') mime = 'image/x-icon';
+ let data;
+ if (src.startsWith('http')) {
+ data = await fetchUrl(src);
+ } else {
+ const imgPath = path.join(baseDir, src);
+ data = fs.readFileSync(imgPath);
+ }
+ const base64 = Buffer.from(data).toString('base64');
+ if (ext === '.svg') {
+ const svgData = typeof data === 'string' ? data : data.toString('utf8');
+ return match.replace(/src="[^"]+"/, `src="data:${mime};utf8,${encodeURIComponent(svgData)}"`);
+ }
+ return match.replace(/src="[^"]+"/, `src="data:${mime};base64,${base64}"`);
+}
+
+async function inlineSvg(match) {
+ const match2 = match.match(/href=["']([^"']+)["']/);
+ if (!match2) return match;
+ const href = match2[1];
+ if (!href.toLowerCase().endsWith('.svg')) return match;
+ let data;
+ if (href.startsWith('http')) {
+ data = await fetchUrl(href);
+ } else {
+ const svgPath = path.join(baseDir, href);
+ data = fs.readFileSync(svgPath, 'utf8');
+ }
+ const base64 = Buffer.from(data).toString('base64');
+ return match.replace(/href="[^"]+"/, `href="data:image/svg+xml;base64,${base64}"`);
+}
+
+async function processHtml(content) {
+ let result = content;
+ const cssRegex = /]+rel=["']stylesheet["'][^>]*>/g;
+ const jsRegex = /