Thimble

SetupAboutSrcDemoContributeDonate

Thimble is "the world's most minimal single-page-application front-end framework".

It can be integrated into any static website with a <main> section on each page, whether produced by a static site generator or by handwritten HTML.

Thimble gracefully degrades when javascript is disabled.

You can read a blog post about permacomputing and Thimble's origin story here.

The complete source code is below, as well as on codeberg.

/*
 *                 .--.
 *               .'_\/_'.
 *               '. /\ .'
 *                 "||"
 *                  || /\
 *               /\ ||//\)
 *              (/\\||/
 *           ______\||/_______
 *
 */
// constants for cache expiration
const CACHE_NAME = 'switcher-cache';
const MAX_CACHE_AGE = 2 * 24 * 60 * 60 * 1000;  // 2 days in milliseconds

// Helper function to check if a cached response is older than the maximum cache age
function isCacheExpired(response) {
    const cacheTime = response.headers.get('sw-cache-time');
    if (!cacheTime) return true;  // No timestamp, treat as expired

    const cacheAge = Date.now() - new Date(cacheTime).getTime();
    return cacheAge > MAX_CACHE_AGE;  // Expired if older than 2 days
}

// Helper function to save to cache along with timestamp
function saveResponseInCache(url, response, cache) {
    // Create a new Response object with a custom header to store the current timestamp
    const headers = new Headers(response.headers);
    headers.append('sw-cache-time', new Date().toISOString());

    const responseWithTimestamp = new Response(response.body, {
        status: response.status,
        statusText: response.statusText,
        headers: headers
    });

    console.log("++ storing url in cache: ", url);
    cache.put(url, responseWithTimestamp);  // Cache the response with timestamp
}


// function that prefetches any internal links on the current page which have not already been fetched
function prefetchLinks() {
    // Open the cache
    caches.open(CACHE_NAME).then(function (cache) {
        // Select all internal links (anchor tags starting with '/')
        const links = document.querySelectorAll('a[href^="/"]');

        links.forEach(function (link) {
            const url = link.href;  // Get the full URL of the link

            // Check if the URL is already in the cache
            cache.match(url).then(function (response) {
                if (!response || isCacheExpired(response)) {
                    // If not in cache, prefetch and store in cache
                    fetch(url).then(function (networkResponse) {
                        if (networkResponse.ok) {
                            saveResponseInCache(url, networkResponse.clone(), cache);  // Cache the response
                            console.log(`Prefetched and cached: ${url}`);
                        } else {
                            console.error(`Failed to fetch: ${url}`);
                        }
                    }).catch(function (error) {
                        console.error(`Fetch error: ${url}`, error);
                    });
                } else {
                    console.log(`Already cached: ${url}`);
                }
            }).catch(function (error) {
                console.error(`Cache match error: ${url}`, error);
            });
        });
    }).catch(function (error) {
        console.error('Cache open error:', error);
    });
}

// Function to add switcher click behavior to internal links
function attachLinkListeners(doc) {
    doc.querySelectorAll('a[href^="/"]').forEach(function (link) {
        link.addEventListener('click', function (event) {
            event.preventDefault();  // Prevent default navigation

            const url = link.href;  // Get the URL from the clicked link

            // Check the cache first
            caches.open(CACHE_NAME).then(function (cache) {
                cache.match(url).then(function (cachedResponse) {
                    if (cachedResponse && !isCacheExpired(cachedResponse)) {
                        return cachedResponse.text();  // Use the cached response if it's not expired
                    } else {
                        // Fetch the content of the URL using AJAX
                        return fetch(url)
                            .then(networkResponse => {
                                if (!networkResponse.ok) {
                                    throw new Error('Network response was not ok');
                                    // this thrown error should get caught by the catch at the end
                                }
                                // also cache the response while we are here
                                saveResponseInCache(url, networkResponse.clone(), cache);
                                return networkResponse.text();  // Return the response as text (HTML)
                            });
                    }
                }).then(function (html) {
                    // Parse the returned HTML to extract the main content
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(html, 'text/html');
                    const newMainContent = doc.querySelector('main');  // Select the main content

                    if (newMainContent) {
                        // Replace the current main content with the fetched content
                        document.querySelector('main').innerHTML = newMainContent.innerHTML;

                        // update the URL in the browser
                        history.pushState({}, '', url);

                        // scroll to top of page
                        window.scrollTo(0, 0);

                        // Reapply the link behavior to newly added content and prefetch new pages
                        const main = document.querySelector('main');
                        activate_document(main);
                    } else {
                        // if there was no main section of the returned html, then fallback to default link behavior
                        window.location.href = url;
                    }
                })
                    .catch(error => {
                        console.error('Fetch error:', error);
                        // if there was an error in the ajax call, then fallback to default link behavior
                        window.location.href = url;
                    });
            });
        });
    });
}

// this line makes it so when you use the back button in non-chromium browsers, it refreshes the page
// in order to properly load the page
window.addEventListener('popstate', (event) => {
    window.location.href = window.location.href;
});

// activate function which is called on first page load, and after any page change
function activate_document(doc) {
    attachLinkListeners(doc);
    prefetchLinks();
}

// on first page load, activate the whole page
document.addEventListener('DOMContentLoaded', function () {
    activate_document(document);
});