diff --git a/apps/nextjs/eslint.config.js b/apps/nextjs/eslint.config.js index c131bab90..3d593e34e 100644 --- a/apps/nextjs/eslint.config.js +++ b/apps/nextjs/eslint.config.js @@ -10,4 +10,21 @@ export default [ ...baseConfig, ...reactConfig, ...nextjsConfig, + { + files: ["public/sw.js"], + languageOptions: { + globals: { + self: "readonly", + clients: "readonly", + skipWaiting: "readonly", + importScripts: "readonly", + caches: "readonly", + fetch: "readonly", + }, + }, + rules: { + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + }, + }, ]; diff --git a/apps/nextjs/next.config.ts b/apps/nextjs/next.config.ts index a3dabe151..4a0450165 100644 --- a/apps/nextjs/next.config.ts +++ b/apps/nextjs/next.config.ts @@ -42,9 +42,18 @@ const nextConfig: NextConfig = { }, ], }, + // skipcq: JS-0116 // eslint-disable-next-line @typescript-eslint/require-await,no-restricted-syntax async headers() { return [ + { + source: "/sw.js", + headers: [ + { key: "Content-Type", value: "application/javascript; charset=utf-8" }, + { key: "Cache-Control", value: "no-cache, no-store, must-revalidate" }, + { key: "Content-Security-Policy", value: "default-src 'self'; script-src 'self'" }, + ], + }, { source: "/(.*)", // Apply CSP to all routes headers: [ diff --git a/apps/nextjs/public/sw.js b/apps/nextjs/public/sw.js new file mode 100644 index 000000000..dbb12ebbb --- /dev/null +++ b/apps/nextjs/public/sw.js @@ -0,0 +1,7 @@ +self.addEventListener("install", () => { + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil(self.clients.claim()); +}); diff --git a/apps/nextjs/src/app/[locale]/layout.tsx b/apps/nextjs/src/app/[locale]/layout.tsx index daab79726..5449e1252 100644 --- a/apps/nextjs/src/app/[locale]/layout.tsx +++ b/apps/nextjs/src/app/[locale]/layout.tsx @@ -26,6 +26,7 @@ import { isLocaleRTL, isLocaleSupported } from "@homarr/translation"; import { Analytics } from "~/components/layout/analytics"; import { CrowdinLiveTranslation } from "~/components/layout/crowdin-live-translation"; import { SearchEngineOptimization } from "~/components/layout/search-engine-optimization"; +import { ServiceWorkerRegistration } from "~/components/layout/service-worker-registration"; import { getCurrentColorSchemeAsync } from "~/theme/color-scheme"; import { DayJsLoader } from "./_client-providers/dayjs-loader"; import { JotaiProvider } from "./_client-providers/jotai"; @@ -142,6 +143,7 @@ export default async function Layout(props: {