feat(pwa): add fullscreen installable PWA with manifest, service worker, and cross-platform app icons. (#1728)

Integrated into release/v3.7.4
This commit is contained in:
Yash Ghule
2026-04-28 12:59:18 -04:00
committed by GitHub
parent cbd8344789
commit 4ac8d82c81
7 changed files with 147 additions and 2 deletions

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

BIN
public/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

75
public/sw.js Normal file
View File

@@ -0,0 +1,75 @@
const CACHE_NAME = "omniroute-pwa-v1";
const APP_SHELL = [
"/",
"/offline",
"/manifest.webmanifest",
"/icon-512.png",
"/apple-touch-icon.png",
];
const EXCLUDED_PATH_PREFIXES = ["/api/", "/a2a", "/dashboard/endpoint"];
self.addEventListener("install", (event) => {
event.waitUntil(
caches
.open(CACHE_NAME)
.then((cache) => cache.addAll(APP_SHELL))
.then(() => self.skipWaiting()),
);
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches
.keys()
.then((keys) => Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))))
.then(() => self.clients.claim()),
);
});
self.addEventListener("fetch", (event) => {
if (event.request.method !== "GET") {
return;
}
const requestUrl = new URL(event.request.url);
const isSameOrigin = requestUrl.origin === self.location.origin;
const isExcludedPath = EXCLUDED_PATH_PREFIXES.some((prefix) =>
requestUrl.pathname.startsWith(prefix),
);
const destination = event.request.destination;
const isStaticAsset = ["style", "script", "image", "font"].includes(destination);
const isNavigateRequest = event.request.mode === "navigate";
// Never cache API/dashboard traffic with potentially auth-sensitive content.
if (!isSameOrigin || isExcludedPath) {
return;
}
event.respondWith(
(async () => {
if (isNavigateRequest) {
try {
return await fetch(event.request);
} catch {
return (await caches.match("/offline")) || Response.error();
}
}
if (!isStaticAsset) {
return fetch(event.request);
}
const cachedResponse = await caches.match(event.request);
if (cachedResponse) {
return cachedResponse;
}
const networkResponse = await fetch(event.request);
if (networkResponse && networkResponse.status === 200) {
const responseClone = networkResponse.clone();
void caches.open(CACHE_NAME).then((cache) => cache.put(event.request, responseClone));
}
return networkResponse;
})(),
);
});

View File

@@ -5,12 +5,19 @@ import { NextIntlClientProvider } from "next-intl";
import { getMessages, getLocale } from "next-intl/server";
import { RTL_LOCALES } from "@/i18n/config";
import { getSettings } from "@/lib/db/settings";
import type { Viewport } from "next";
import { PwaRegister } from "@/shared/components/PwaRegister";
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
});
export const viewport: Viewport = {
themeColor: "#0b0f1a",
viewportFit: "cover",
};
export async function generateMetadata() {
const settings = await getSettings();
const instanceName = settings?.instanceName || "OmniRoute";
@@ -20,9 +27,25 @@ export async function generateMetadata() {
title: `${instanceName} — AI Gateway for Multi-Provider LLMs`,
description:
"OmniRoute is an AI gateway for multi-provider LLMs. One endpoint for all your AI providers.",
manifest: "/manifest.webmanifest",
applicationName: instanceName,
appleWebApp: {
capable: true,
title: instanceName,
statusBarStyle: "black-translucent",
},
other: {
"mobile-web-app-capable": "yes",
},
icons: {
icon: customFaviconUrl ? "/api/settings/favicon" : "/favicon.svg",
apple: "/apple-touch-icon.svg",
icon: customFaviconUrl
? "/api/settings/favicon"
: [
{ url: "/favicon.ico", sizes: "any" },
{ url: "/favicon.svg", type: "image/svg+xml" },
{ url: "/icon-512.png", type: "image/png", sizes: "512x512" },
],
apple: [{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }],
},
};
}
@@ -67,6 +90,7 @@ export default async function RootLayout({ children }) {
Skip to content
</a>
<NextIntlClientProvider locale={locale} messages={messages}>
<PwaRegister />
<ThemeProvider>{children}</ThemeProvider>
</NextIntlClientProvider>
</body>

29
src/app/manifest.ts Normal file
View File

@@ -0,0 +1,29 @@
import type { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "OmniRoute",
short_name: "OmniRoute",
description:
"OmniRoute is an AI gateway for multi-provider LLMs. One endpoint for all your AI providers.",
start_url: "/",
scope: "/",
display: "fullscreen",
orientation: "any",
background_color: "#0b0f1a",
theme_color: "#0b0f1a",
icons: [
{
src: "/icon-512.png",
sizes: "512x512",
type: "image/png",
purpose: "any maskable",
},
{
src: "/apple-touch-icon.png",
sizes: "180x180",
type: "image/png",
},
],
};
}

View File

@@ -0,0 +1,17 @@
"use client";
import { useEffect } from "react";
export function PwaRegister() {
useEffect(() => {
if (typeof window === "undefined" || !("serviceWorker" in navigator)) {
return;
}
navigator.serviceWorker.register("/sw.js").catch(() => {
// Ignore registration failures to avoid blocking app rendering.
});
}, []);
return null;
}