Hướng dẫn lưu toàn bộ trang web thành file HTML bằng Console

OneHub

Staff member
Mr Boss

1787417603860.webp

Đ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.

💡 Mẹo: Hãy scroll hết trang và đợi ảnh tải xong trước khi chạy code để file lưu được đầy đủ nhất.
 
Last edited:
Back
Top