Skip to content

Instantly share code, notes, and snippets.

@Criomby
Last active July 6, 2025 20:25
Show Gist options
  • Save Criomby/c002254eba079855a132c8bfdb1d95bf to your computer and use it in GitHub Desktop.
Save Criomby/c002254eba079855a132c8bfdb1d95bf to your computer and use it in GitHub Desktop.
gethomepage/homepage local-remote href slider
// -----------------
// Slider to switch between local/remote addresses.
// Allows you to change the domains of the href targets to the current domain (e.g. when using a VPN).
// -----------------
// these domain (href) will be changed to the current browser domain
// prevents changing links to other services/servers which should not be changed
const host = "pi-server.local"; // TODO: change this
const updateLocation = () => {
const serviceWidgetsLink = document.getElementsByClassName("service-title-text");
let browserAddr = window.location.href.split(":");
browserAddr = browserAddr[1].slice(2, -1);
for (let i = 0; i < serviceWidgetsLink.length; i++) {
// widgets might not have an href property set in homepage config
if (!serviceWidgetsLink[i].getAttribute("href")) {continue;}
let href = serviceWidgetsLink[i].href;
let split = href.split(":");
if (split.length < 3) {
continue;
}
let currServiceAddr = split[1].slice(2, href.length);
if (currServiceAddr === host) {
// change domain to current browser
split[1] = "//" + browserAddr;
serviceWidgetsLink[i].href = split.join(":");
} else if (currServiceAddr === browserAddr) {
// switch back to original domain
split[1] = "//" + host;
serviceWidgetsLink[i].href = split.join(":");
}
}
};
const sliderOFF = new DOMParser().parseFromString(
'<svg id="sliderOFF" stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" class="text-theme-800 dark:text-theme-200 w-8 h-8 cursor-pointer" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="M0 0h24v24H0z"></path><path d="M17 7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h10c2.76 0 5-2.24 5-5s-2.24-5-5-5zM7 15c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"></path></svg>',
"text/html"
).getElementById("sliderOFF");
const sliderON = new DOMParser().parseFromString(
'<svg id="sliderON" stroke="currentColor" fill="#75FB4C" stroke-width="0" viewBox="0 0 24 24" class="text-theme-800 dark:text-theme-200 w-8 h-8 cursor-pointer" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="M0 0h24v24H0z"></path><path d="M17 7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h10c2.76 0 5-2.24 5-5s-2.24-5-5-5zm0 8c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"></path></svg>',
"text/html"
).getElementById("sliderON");
const iconHome = new DOMParser().parseFromString(
'<svg id="iconHome" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 -960 960 960" width="1em" fill="currentColor" class="text-theme-800 dark:text-theme-200 w-5 h-5 m-1.5"><path d="M240-200h120v-200q0-17 11.5-28.5T400-440h160q17 0 28.5 11.5T600-400v200h120v-360L480-740 240-560v360Zm-80 0v-360q0-19 8.5-36t23.5-28l240-180q21-16 48-16t48 16l240 180q15 11 23.5 28t8.5 36v360q0 33-23.5 56.5T720-120H560q-17 0-28.5-11.5T520-160v-200h-80v200q0 17-11.5 28.5T400-120H240q-33 0-56.5-23.5T160-200Zm320-270Z"/></svg>',
"text/html"
).getElementById("iconHome");
const iconAway = new DOMParser().parseFromString(
'<svg id="iconAway" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 -960 960 960" width="1em" fill="currentColor" class="text-theme-800 dark:text-theme-200 w-5 h-5 m-1.5"><path d="M436-360 371-72q-3 14-14.5 23T330-40q-20 0-32-15t-8-34l113-573q6-29 27-43.5t44-14.5q23 0 42.5 10t31.5 30l40 64q18 29 46.5 52.5T700-529v-41q0-13 8.5-21.5T730-600q13 0 21.5 8.5T760-570v500q0 13-8.5 21.5T730-40q-13 0-21.5-8.5T700-70v-376q-48-11-89-35t-71-59l-24 120 72 68q6 6 9 13.5t3 15.5v243q0 17-11.5 28.5T560-40q-17 0-28.5-11.5T520-80v-200l-84-80Zm-178-82-46-9q-16-3-25-16.5t-6-30.5l30-157q6-34 36-51.5t65-10.5q17 3 25.5 16.5T343-670l-38 196q-3 17-16.5 26t-30.5 6Zm282-298q-33 0-56.5-23.5T460-820q0-33 23.5-56.5T540-900q33 0 56.5 23.5T620-820q0 33-23.5 56.5T540-740Z"/></svg>',
"text/html"
).getElementById("iconAway");
// create slider container element
const changeSlider = document.createElement("div");
changeSlider.setAttribute("id", "location");
changeSlider.setAttribute("class", "rounded-full flex self-end ml-3");
changeSlider.appendChild(iconHome);
changeSlider.appendChild(sliderOFF);
changeSlider.appendChild(iconAway);
// append to footer
const footer = document.querySelector("#style");
footer.appendChild(changeSlider);
// event listener
changeSlider.addEventListener("click", () => {
updateLocation();
if (sliderON.parentElement === changeSlider) {
sliderON.replaceWith(sliderOFF);
} else {
sliderOFF.replaceWith(sliderON);
}
});
@Criomby
Copy link
Author

Criomby commented Aug 1, 2024

Screenshots

screenshot_off screenshot_on

When the slider is OFF (home, 1st img) the addresses from your homepage file will be used as href.
When it is ON (remote, 2nd img) the addresses will be changed to your browser's current domain while retaining all other url parts like scheme, post and subdirectory.

How to

  1. Add your local address (href in services.yaml) to be changed to the current browser domain in the in line 8 (see code comments).
    The addresses should just contain the domain part of the url, everything before and after the domain will be retained as-is:
    url_schema
    This can also be an IP address.
    href: http://<enter_this_as_host>:8081
  2. Paste code to your custom.js file.
  3. Enjoy your homepage away from home!

@Criomby
Copy link
Author

Criomby commented Mar 1, 2025

Update - 2025-03-01

  • adjust for services/widgets which do not have an href property set in homepage config (diff)
  • be aware that you need to define a port, urls without port specified will be ignored

@OwenWright8
Copy link

OwenWright8 commented Apr 19, 2025

Thanks for making this! I found a bug where the remote IP was getting cut off. For example, something like 100.x.x.100 would turn into 100.x.x.10. The issue came from using window.location.href.split(":")[1].slice(2, -1) to grab the IP, which accidentally removed the last digit. I fixed it by using window.location.hostname instead. That gives the correct IP or domain without touching anything else. Everything works now when switching between local and remote addresses like with Tailscale or Meshnet. This really useful feature and this small change makes it even better. I added the full updateLocation function just so it's easier to see what's changed.

const updateLocation = () => {
const serviceWidgetsLink = document.getElementsByClassName("service-title-text");
const browserAddr = window.location.hostname;

for (let i = 0; i < serviceWidgetsLink.length; i++) {
    if (!serviceWidgetsLink[i].getAttribute("href")) { continue; }
    
    let href = serviceWidgetsLink[i].href;
    let split = href.split(":");
    if (split.length < 3) {
        continue;
    }

    let currServiceAddr = split[1].replace("//", "");
    if (currServiceAddr === host) {
        split[1] = "//" + browserAddr;
        serviceWidgetsLink[i].href = split.join(":");
    } else if (currServiceAddr === browserAddr) {
        split[1] = "//" + host;
        serviceWidgetsLink[i].href = split.join(":");
    }
}

};

@Binocularbath
Copy link

This looks perfect for what I'm looking for. Thank you for uploading this. I'll test it out and report back!

@Binocularbath
Copy link

I just want to check in about something. I'm still about of a tech novice haha.

  1. This is a script I can run in something like tamper monkey?

  2. for line 8. Where exact do I add the new IP address? For example the IP I use a VPN to access my home network

const host = "pi-server.local"; // TODO: change this

@Binocularbath
Copy link

Thanks for making this! I found a bug where the remote IP was getting cut off. For example, something like 100.x.x.100 would turn into 100.x.x.10. The issue came from using window.location.href.split(":")[1].slice(2, -1) to grab the IP, which accidentally removed the last digit. I fixed it by using window.location.hostname instead. That gives the correct IP or domain without touching anything else. Everything works now when switching between local and remote addresses like with Tailscale or Meshnet. This really useful feature and this small change makes it even better. I added the full updateLocation function just so it's easier to see what's changed.

const updateLocation = () => { const serviceWidgetsLink = document.getElementsByClassName("service-title-text"); const browserAddr = window.location.hostname;

for (let i = 0; i < serviceWidgetsLink.length; i++) {
    if (!serviceWidgetsLink[i].getAttribute("href")) { continue; }
    
    let href = serviceWidgetsLink[i].href;
    let split = href.split(":");
    if (split.length < 3) {
        continue;
    }

    let currServiceAddr = split[1].replace("//", "");
    if (currServiceAddr === host) {
        split[1] = "//" + browserAddr;
        serviceWidgetsLink[i].href = split.join(":");
    } else if (currServiceAddr === browserAddr) {
        split[1] = "//" + host;
        serviceWidgetsLink[i].href = split.join(":");
    }
}

};

hi there

I am just trying to get this one going on my set up. what should i change from the original document

@Binocularbath
Copy link

Ok, I havev figured out how to use it and how to implement @OwenWright8 addition via the help of chatgpt. But i understand it alot better now. That was a run few hours of learning. Thanks again for posting this. This is the updated version i am using, ive tested it and it works well.

   // -----------------
// Slider to switch between local/remote addresses.
// Allows you to change the domains of the href targets to the current domain (e.g. when using a VPN).
// -----------------

// these domain (href) will be changed to the current browser domain
// prevents changing links to other services/servers which should not be changed
const host = "pi-server.local"; // TODO: change this

const updateLocation = () => {
    const serviceWidgetsLink = document.getElementsByClassName("service-title-text");
    let browserAddr = window.location.hostname;
    for (let i = 0; i < serviceWidgetsLink.length; i++) {
        // widgets might not have an href property set in homepage config
        if (!serviceWidgetsLink[i].getAttribute("href")) {continue;}
        let href = serviceWidgetsLink[i].href;
        let split = href.split(":");
        if (split.length < 3) {
            continue;
        }
        let currServiceAddr = split[1].slice(2, href.length);
        if (currServiceAddr === host) {
            // change domain to current browser
            split[1] = "//" + browserAddr;
            serviceWidgetsLink[i].href = split.join(":");
        } else if (currServiceAddr === browserAddr) {
            // switch back to original domain
            split[1] = "//" + host;
            serviceWidgetsLink[i].href = split.join(":");
        }
    }
};

const sliderOFF = new DOMParser().parseFromString(
    '<svg id="sliderOFF" stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" class="text-theme-800 dark:text-theme-200 w-8 h-8 cursor-pointer" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="M0 0h24v24H0z"></path><path d="M17 7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h10c2.76 0 5-2.24 5-5s-2.24-5-5-5zM7 15c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"></path></svg>',
    "text/html"
).getElementById("sliderOFF");
const sliderON = new DOMParser().parseFromString(
    '<svg id="sliderON" stroke="currentColor" fill="#75FB4C" stroke-width="0" viewBox="0 0 24 24" class="text-theme-800 dark:text-theme-200 w-8 h-8 cursor-pointer" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="M0 0h24v24H0z"></path><path d="M17 7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h10c2.76 0 5-2.24 5-5s-2.24-5-5-5zm0 8c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"></path></svg>',
    "text/html"
).getElementById("sliderON");
const iconHome = new DOMParser().parseFromString(
    '<svg id="iconHome" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 -960 960 960" width="1em" fill="currentColor" class="text-theme-800 dark:text-theme-200 w-5 h-5 m-1.5"><path d="M240-200h120v-200q0-17 11.5-28.5T400-440h160q17 0 28.5 11.5T600-400v200h120v-360L480-740 240-560v360Zm-80 0v-360q0-19 8.5-36t23.5-28l240-180q21-16 48-16t48 16l240 180q15 11 23.5 28t8.5 36v360q0 33-23.5 56.5T720-120H560q-17 0-28.5-11.5T520-160v-200h-80v200q0 17-11.5 28.5T400-120H240q-33 0-56.5-23.5T160-200Zm320-270Z"/></svg>',
    "text/html"
).getElementById("iconHome");
const iconAway = new DOMParser().parseFromString(
    '<svg id="iconAway" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 -960 960 960" width="1em" fill="currentColor" class="text-theme-800 dark:text-theme-200 w-5 h-5 m-1.5"><path d="M436-360 371-72q-3 14-14.5 23T330-40q-20 0-32-15t-8-34l113-573q6-29 27-43.5t44-14.5q23 0 42.5 10t31.5 30l40 64q18 29 46.5 52.5T700-529v-41q0-13 8.5-21.5T730-600q13 0 21.5 8.5T760-570v500q0 13-8.5 21.5T730-40q-13 0-21.5-8.5T700-70v-376q-48-11-89-35t-71-59l-24 120 72 68q6 6 9 13.5t3 15.5v243q0 17-11.5 28.5T560-40q-17 0-28.5-11.5T520-80v-200l-84-80Zm-178-82-46-9q-16-3-25-16.5t-6-30.5l30-157q6-34 36-51.5t65-10.5q17 3 25.5 16.5T343-670l-38 196q-3 17-16.5 26t-30.5 6Zm282-298q-33 0-56.5-23.5T460-820q0-33 23.5-56.5T540-900q33 0 56.5 23.5T620-820q0 33-23.5 56.5T540-740Z"/></svg>',
    "text/html"
).getElementById("iconAway");

// create slider container element
const changeSlider = document.createElement("div");
changeSlider.setAttribute("id", "location");
changeSlider.setAttribute("class", "rounded-full flex self-end ml-3");
changeSlider.appendChild(iconHome);
changeSlider.appendChild(sliderOFF);
changeSlider.appendChild(iconAway);

// append to footer
const footer = document.querySelector("#style");
footer.appendChild(changeSlider);

// event listener
changeSlider.addEventListener("click", () => {
    updateLocation();
    if (sliderON.parentElement === changeSlider) {
        sliderON.replaceWith(sliderOFF);
    } else {
        sliderOFF.replaceWith(sliderON);
    }
});

@Criomby
Copy link
Author

Criomby commented Jul 6, 2025

Thanks for the update @Binocularbath and glad to hear it works for you now!

@Binocularbath
Copy link

Thanks for the update @Binocularbath and glad to hear it works for you now!

Thank you for sharing the script! It's really great

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment