SkillAgentSearch skills...

Htmf

Self-framing pages allow multipage web applications to preserve elements (audio, video) and other state across navigations

Install / Use

/learn @callionica/Htmf
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

HyperTextMicroFramework (htmf v0.7)

htmf provides shared state for multipage web applications

htmf is a micro-framework for allowing multipage web applications to preserve elements (such as audio and video) across navigations using the powerful technique of self framing.

Self Framing

A self-framing web page is a page that detects whether it is hosted in a frame. If it is not in a frame, it alters the structure of its own page to create an iframe and load itself again within the iframe.

The page understands which parts of itself are common to the web application and should be preserved across navigations (such as a video element or navigation controls) and which parts of itself are page-specific content, to be replaced when the user navigates to another page.

Marking up the page is as simple as applying id="htmf" to the element that contains all the page-specific content. Everything outside of that element is common content.

When the page is loaded in the browser, the common parts of the page live in the top-level, outer document and the unique parts of the page live in the iframe in the inner document.

In the outer page, the unique parts are removed: the micro-framework replaces the #htmf element (which contains the unique parts of the page) with the iframe that loads the inner document.

In the inner page, the shared parts are removed: the micro-framework removes all elements outside of the #htmf element.

Your code and CSS can detect if you are in the outer or inner document by looking for the htmf-document attribute on the document's body (htmf-document="outer" for the outer document and htmf-document="inner" for the inner document) or by looking for the presence or absence of common elements or by using a standard technique to tell if the page is framed: if (window.frameElement !== null) or by comparing document === outerDocument.

Self Preservation

Normally dividing a web page into an outer parent and an inner child within an iframe could change its behavior, so the htmf micro-framework takes steps to preserve the behavior of the page.

First, htmf creates a base element in the head of the outer document that retargets links from the current document to the inner document: <base target="htmf"> where htmf is the name given to the automatically created iframe. That means that if you click a link in the outer document, the outer page won't be replaced by the new document; instead, the inner iframe will load the new content.

Next, the micro-framework hooks various events on the iframe which tell it when a new document has been loaded. When it detects that a new document has been loaded in to the iframe, it copies some of the information from the inner document into the outer document, so that the user feels as though a normal page navigation has occurred. These items are copied from the inner document to the outer document:

  1. The URL
  2. The title
  3. The description
  4. The canonical URL link
  5. Alternate representation links (RSS, Atom, etc)
  6. rel=me links

In this way, the document preserves the appearance of a single page instead of an outer and inner page.

Quick Start

Mark up the page-specific content by applying id="htmf" to the single element that contains it. All other content in your page is shared content.

Add this script to the bottom of the body element to turn your normal web page into a self-framing page.

    <script id="htmf-script">
        // htmf - (c) Callionica 2024 - https://github.com/callionica/htmf
        const body = document.body;
        body.querySelector("script#htmf-script").remove();

        /** The documents in the frame hierarchy from top to inner (for example: [top, parent, parent, self, inner], but typically: [outer, inner]) */
        Object.defineProperty(globalThis, "documentList", {
            get() {
                const result = [];

                if (document !== innerDocument) {
                    result.push(innerDocument);
                }

                let current = globalThis;
                while (true) {
                    result.push(current.document);
                    if (current === current.parent) {
                        break;
                    }
                    current = current.parent;
                }

                return result.reverse();
            },
            enumerable: true,
            configurable: true,
        });

        /** Returns a document given a target string ("_top", "htmf", "_self", "_parent", etc) */
        function targetToDocument(target) {
            switch (target) {
                case "_top": return top.document;
                case "_parent": return parent?.document;
                case "_self": return document;
                case "_outer": return outerDocument;
                case "_inner": return innerDocument;
                case "htmf": return innerDocument;
            }
            return undefined;
        }

        function queryAll(selector, roots = documentList) {
            const result = [];
            for (const root of roots) {
                result.push(...root.querySelectorAll(selector));
            }
            return result;
        }

        if (window.frameElement !== null) {
            globalThis.innerDocument = document;
            globalThis.outerDocument = parent.document;

            body.setAttribute("htmf-document", "inner");
            const htmf = body.querySelector("#htmf") ?? undefined;
            if (htmf !== undefined) {
                body.replaceChildren(htmf);
            }
        } else {
            Object.defineProperty(globalThis, "innerDocument", {
                get() {
                    return document.querySelector("#htmf").contentDocument;
                },
                enumerable: true,
                configurable: true,
            });

            globalThis.outerDocument = document;

            body.setAttribute("htmf-document", "outer");

            const self = body.querySelector("#htmf");
            if (self != undefined) {
                self.outerHTML = `<iframe name="htmf" id="htmf"></iframe>`;
            } else {
                body.innerHTML = `<iframe name="htmf" id="htmf"></iframe>`;
            }

            const head = document.head;
            const base = document.createElement("base");
            base.target = "htmf";
            head.append(base);

            const iframe = body.querySelector("iframe#htmf");
            iframe.src = document.location;

            function ready(document, callback) {
                if (document.readyState != 'loading') {
                    callback();
                } else {
                    document.addEventListener('DOMContentLoaded', callback, { once: true });
                }
            }

            function enableIFrameEvents(iframe) {

                function onLocationChanged(oldURL, newURL) {
                    const inner = iframe.contentDocument;

                    const preserve = () => {
                        document.title = inner.title;

                        const selectors = ["link[rel='canonical']", "link[rel='alternate']", "link[rel='me']", "meta[name='description']", "meta[name='robots']"];
                        for (const selector of selectors) {
                            [...document.head.querySelectorAll(selector)].map(e => e.remove());
                            const items = [...inner.head.querySelectorAll(selector)];
                            for (const item of items) {
                                document.head.append(item.cloneNode());
                            }
                        }

                        const visibleURL = new URL(newURL);
                        const hiddenParameters = (document.body.getAttribute("htmf-hidden-parameters") ?? "").split(" ");
                        for (const hiddenParameter of hiddenParameters) {
                            visibleURL.searchParams.delete(hiddenParameter);
                        }

                        history.replaceState({}, document.title, visibleURL);
                    };

                    ready(inner, preserve);

                    iframe.dispatchEvent(new CustomEvent("location-changed", { bubbles: true, cancelable: true, detail: { oldURL, newURL } }));
                }

                function onHashChange(e) {
                    const w = iframe.contentWindow ?? undefined;
                    if (w === undefined) {
                        return;
                    }

                    const { oldURL, newURL } = e;
                    console.log(oldURL.toString(), newURL.toString());
                    onLocationChanged(oldURL, newURL);
                }

                function onUnload() {
                    const w = iframe.contentWindow ?? undefined;
                    if (w === undefined) {
                        return;
                    }

                    const oldURL = new URL(w.location.href);
                    // Timeout of 0 because the URL changes immediately _after_ the `unload` event fires.
                    setTimeout(function () {
                        if (w) {
                            const newURL = new URL(w.location.href);
                            console.log(oldURL.toString(), newURL.toString());
                            onLocationChanged(oldURL, newURL);
                        }
                    }, 0);
                }

                function attach() {
                    const w = iframe.contentWindow ?? undefined;
                    if (w === undefined) {
                        return;
                    }

                    w.removeEventListener("unload", onUnload);
                    w.removeEventListener("hashchange", onHashChange);

                    w.addEventListener("unload", onUnload);
                    w.addEventListener("hashchange", onHashChange
View on GitHub
GitHub Stars5
CategoryContent
Updated1mo ago
Forks0

Security Score

85/100

Audited on Feb 7, 2026

No findings