Đoạn code dưới đây giúp bạn lưu trang web đang mở thành một file index.html, giữ lại tối đa giao diện, CSS, JavaScript, hình ảnh và nội dung hiện tại.
Phù hợp để lưu snapshot website, trang XenForo, bài viết, resource page hoặc giao diện cần tham khảo sau này.
Code có tác dụng gì?
Nhúng CSS trực tiếp vào HTML
Chuyển hình ảnh sang Base64 khi có thể
Nhúng JavaScript bên ngoài
Chuyển URL tương đối thành URL tuyệt đối
Hỗ trợ ảnh lazy-load và srcset
Giữ trạng thái input, textarea, checkbox, select
Tự động tải file index.html
Lưu ý: Một số tài nguyên từ domain/CDN khác có thể bị trình duyệt chặn do CORS. Khi đó code sẽ cố giữ lại URL gốc.
Cách sử dụng
Bước 1: Mở trang web cần lưu.Bước 2: Scroll từ đầu đến cuối trang để hình ảnh và nội dung lazy-load được tải đầy đủ.
Bước 3: Nhấn:
F12
hoặc:
Ctrl + Shift + I
Sau đó mở tab Console.
Bước 4: Copy toàn bộ code bên dưới, paste vào Console và nhấn Enter.
Nếu Chrome/Edge chặn paste, gõ:
allow pasting
rồi nhấn Enter.
Bước 5: Chờ đến khi Console hiển thị:
[HTML Export] Done. Downloaded: index.html
File index.html sẽ được tải về thư mục Downloads...
Full Code
HTML:
(async function downloadCompleteHTML() {
'use strict';
const FILE_NAME = 'index.html';
console.log('[HTML Export] Starting...');
function absoluteUrl(url, baseUrl = location.href) {
if (!url) return '';
if (
url.startsWith('data:') ||
url.startsWith('blob:') ||
url.startsWith('#') ||
url.startsWith('javascript:') ||
url.startsWith('mailto:') ||
url.startsWith('tel:')
) {
return url;
}
try {
return new URL(url, baseUrl).href;
} catch {
return url;
}
}
async function fetchText(url) {
try {
const response = await fetch(url, {
credentials: 'include',
cache: 'force-cache'
});
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`);
}
return await response.text();
} catch (error) {
console.warn('[HTML Export] Failed to fetch text:', url, error);
return null;
}
}
async function fetchAsDataURL(url) {
try {
if (!url || url.startsWith('data:')) {
return url;
}
const response = await fetch(url, {
credentials: 'include',
cache: 'force-cache'
});
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`);
}
const blob = await response.blob();
return await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
} catch (error) {
console.warn('[HTML Export] Failed to inline binary:', url, error);
return null;
}
}
function replaceCssUrls(css, cssUrl) {
return css.replace(
/url\(\s*(['"]?)(.*?)\1\s*\)/gi,
(match, quote, value) => {
value = value.trim();
if (
!value ||
value.startsWith('data:') ||
value.startsWith('blob:') ||
value.startsWith('#')
) {
return match;
}
try {
return `url("${new URL(value, cssUrl).href}")`;
} catch {
return match;
}
}
);
}
async function processCss(css, cssUrl, visited = new Set()) {
if (!css || visited.has(cssUrl)) return '';
visited.add(cssUrl);
const importRegex =
/@import\s+(?:url\(\s*)?["']?([^"')\s]+)["']?\s*\)?([^;]*);/gi;
let result = '';
let lastIndex = 0;
let match;
while ((match = importRegex.exec(css)) !== null) {
result += css.slice(lastIndex, match.index);
const importUrl = absoluteUrl(match[1], cssUrl);
const mediaQuery = (match[2] || '').trim();
const importedCss = await fetchText(importUrl);
if (importedCss !== null) {
const processed = await processCss(
importedCss,
importUrl,
visited
);
result += mediaQuery
? `\n@media ${mediaQuery} {\n${processed}\n}\n`
: `\n${processed}\n`;
} else {
result += match[0];
}
lastIndex = importRegex.lastIndex;
}
result += css.slice(lastIndex);
return replaceCssUrls(result, cssUrl);
}
async function inlineStylesheets(doc) {
const stylesheets = [...doc.querySelectorAll('link[rel~="stylesheet"]')];
console.log(
`[HTML Export] Processing ${stylesheets.length} stylesheets...`
);
for (const link of stylesheets) {
const href = absoluteUrl(
link.getAttribute('href') || link.href,
location.href
);
if (!href) continue;
const css = await fetchText(href);
if (css === null) {
link.setAttribute('href', href);
continue;
}
const style = doc.createElement('style');
if (link.media) {
style.media = link.media;
}
style.setAttribute('data-original-href', href);
style.textContent = await processCss(css, href);
link.replaceWith(style);
}
}
async function processInlineStyles(doc) {
for (const style of doc.querySelectorAll('style')) {
if (style.textContent) {
style.textContent = replaceCssUrls(
style.textContent,
location.href
);
}
}
for (const element of doc.querySelectorAll('[style]')) {
const value = element.getAttribute('style');
if (value) {
element.setAttribute(
'style',
replaceCssUrls(value, location.href)
);
}
}
}
async function inlineImages(doc) {
const images = [...doc.querySelectorAll('img')];
console.log(
`[HTML Export] Processing ${images.length} images...`
);
for (const img of images) {
const source =
img.getAttribute('src') ||
img.getAttribute('data-src') ||
img.getAttribute('data-lazy-src');
if (!source) continue;
const url = absoluteUrl(source, location.href);
if (!url || url.startsWith('data:')) continue;
const dataUrl = await fetchAsDataURL(url);
if (dataUrl) {
img.setAttribute('src', dataUrl);
img.removeAttribute('srcset');
img.removeAttribute('data-srcset');
img.removeAttribute('data-src');
img.removeAttribute('data-lazy-src');
} else {
img.setAttribute('src', url);
}
}
}
async function processPictureSources(doc) {
for (const source of doc.querySelectorAll('picture source')) {
source.removeAttribute('srcset');
source.removeAttribute('data-srcset');
}
}
async function inlineIcons(doc) {
const icons = [
...doc.querySelectorAll(
'link[rel~="icon"], link[rel="apple-touch-icon"]'
)
];
for (const icon of icons) {
const href = icon.getAttribute('href');
if (!href) continue;
const url = absoluteUrl(href, location.href);
const dataUrl = await fetchAsDataURL(url);
icon.setAttribute('href', dataUrl || url);
}
}
async function inlineScripts(doc) {
const scripts = [...doc.querySelectorAll('script[src]')];
console.log(
`[HTML Export] Processing ${scripts.length} scripts...`
);
for (const script of scripts) {
const src = script.getAttribute('src');
if (!src) continue;
const url = absoluteUrl(src, location.href);
const js = await fetchText(url);
if (js === null) {
script.setAttribute('src', url);
continue;
}
const replacement = doc.createElement('script');
for (const attr of script.attributes) {
if (attr.name !== 'src' && attr.name !== 'integrity') {
replacement.setAttribute(attr.name, attr.value);
}
}
replacement.setAttribute('data-original-src', url);
replacement.textContent = js.replace(/<\/script/gi, '<\\/script');
script.replaceWith(replacement);
}
}
function resolveDocumentUrls(doc) {
const attributes = [
['a[href]', 'href'],
['form[action]', 'action'],
['iframe[src]', 'src'],
['video[src]', 'src'],
['audio[src]', 'src'],
['source[src]', 'src'],
['track[src]', 'src'],
['object[data]', 'data']
];
for (const [selector, attribute] of attributes) {
for (const element of doc.querySelectorAll(selector)) {
const value = element.getAttribute(attribute);
if (value) {
element.setAttribute(
attribute,
absoluteUrl(value, location.href)
);
}
}
}
}
function preserveFormState(sourceDoc, targetDoc) {
const sourceInputs = [
...sourceDoc.querySelectorAll('input, textarea, select')
];
const targetInputs = [
...targetDoc.querySelectorAll('input, textarea, select')
];
sourceInputs.forEach((source, index) => {
const target = targetInputs[index];
if (!target) return;
if (source instanceof HTMLInputElement) {
if (
source.type === 'checkbox' ||
source.type === 'radio'
) {
source.checked
? target.setAttribute('checked', '')
: target.removeAttribute('checked');
} else if (
source.type !== 'file' &&
source.type !== 'password'
) {
target.setAttribute('value', source.value);
}
}
if (source instanceof HTMLTextAreaElement) {
target.textContent = source.value;
}
if (source instanceof HTMLSelectElement) {
const options = target.querySelectorAll('option');
[...source.options].forEach((option, i) => {
if (!options) return;
option.selected
? options.setAttribute('selected', '')
: options.removeAttribute('selected');
});
}
});
}
function downloadHTML(content, fileName) {
const blob = new Blob(
['<!DOCTYPE html>\n', content],
{ type: 'text/html;charset=utf-8' }
);
const objectUrl = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = objectUrl;
anchor.download = fileName;
anchor.style.display = 'none';
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
setTimeout(() => URL.revokeObjectURL(objectUrl), 10000);
}
const doc = document.cloneNode(true);
preserveFormState(document, doc);
doc.querySelectorAll(
'[data-html-export-ignore], .html-export-overlay'
).forEach(el => el.remove());
try {
await inlineStylesheets(doc);
await processInlineStyles(doc);
await inlineImages(doc);
await processPictureSources(doc);
await inlineIcons(doc);
await inlineScripts(doc);
resolveDocumentUrls(doc);
downloadHTML(
doc.documentElement.outerHTML,
FILE_NAME
);
console.log(
`[HTML Export] Done. Downloaded: ${FILE_NAME}`
);
} catch (error) {
console.error('[HTML Export] Export failed:', error);
}
})();
Lưu ý
Đây là công cụ tạo snapshot frontend, không phải clone toàn bộ website. Những chức năng cần server như đăng nhập, đăng bài, tìm kiếm, thanh toán hoặc API có thể không hoạt động khi mở file offline.
Last edited: