1
0
Эх сурвалжийг харах

Neo: manifest icons load from /static/, Vercel scripts only on Vercel (#5934)

* Neo: manifest reuses the page's connection, its icons load, Vercel scripts only on Vercel

Lighthouse on pr-5933.unacms.app put /static/manifest.json at the end of an
813 ms critical chain. Chrome fetches a manifest without cookies, and it keeps
those requests on separate sockets, so the manifest opened a second TCP + TLS
connection (350 ms there). The [...path] page now renders the manifest link
itself with crossOrigin="use-credentials", so it reuses the page's connection.
metadata.manifest only adds that attribute on Vercel previews.

On every deployment but api.neo.so the manifest is the /static/ copy, and its
relative icon paths pointed at /static/*.png files that were never copied.
Chrome requests one of them on every page load and got Next's 404 page back.
next.config.js now copies each icon the static manifest names from public/
when customization did not supply it. The default manifest's "any " purpose
(trailing space, ignored by Chrome) is "any".

Vercel's Analytics and Speed Insights scripts are served by Vercel only;
elsewhere (Railway, local) both 404 on every page load, rendering the 404 page
twice. They now mount only when process.env.VERCEL is set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Neo: keep the manifest link from metadata

Measured on the Railway previews (fresh Chrome profile, 4 runs each), the
credentialed manifest did reuse the page's HTTP/2 connection but finished
about 270 ms later relative to the document (+680-740 ms vs +425-510 ms):
on the shared connection it queues behind the CSS and JS downloads, which
costs more than the TLS handshake it saved. Lighthouse's dependency chain got
longer too. Back to metadata.manifest; the icon and Vercel script fixes stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Andrey Yasko 12 цаг өмнө
parent
commit
5ac43ff822

+ 1 - 1
neo/apps/next/app/layout.js

@@ -26,7 +26,7 @@ export default async function RootLayout({ children }) {
             suppressHydrationWarning
         >
             <body className={appSetting('layout', 'body')}>
-                <Providers initialLang={langCode} initialResources={initialResources}>{children}</Providers>
+                <Providers initialLang={langCode} initialResources={initialResources} onVercel={!!process.env.VERCEL}>{children}</Providers>
             </body>
         </html>
     )

+ 4 - 3
neo/apps/next/app/providers.js

@@ -59,7 +59,7 @@ function ensureI18n(lang, initialResources) {
     }
 }
 
-export function Providers({ children, initialLang = 'en', initialResources }) {
+export function Providers({ children, initialLang = 'en', initialResources, onVercel = false }) {
     // Must match server-rendered copy before the first child paints (menu aria-labels, etc.).
     ensureI18n(initialLang, initialResources)
 
@@ -97,8 +97,9 @@ export function Providers({ children, initialLang = 'en', initialResources }) {
         <I18nextProvider i18n={i18nInstance}>
             <JotaiProvider>
                 <QueryClientProvider client={queryClient}>
-                    <Analytics />
-                    <SpeedInsights />
+                    {/* Their scripts are served by Vercel only; elsewhere both 404 on every page load. */}
+                    {onVercel ? <Analytics /> : null}
+                    {onVercel ? <SpeedInsights /> : null}
                     {children}
                     <Subscriber />
                     <AnalyticsTracker />

+ 22 - 0
neo/apps/next/next.config.js

@@ -47,6 +47,28 @@ function copyCustomizationResources() {
             fs.copyFileSync(src, destPath);
         }
     });
+
+    // The manifest's icon paths are relative, so they resolve under /static/ too. Fill in the
+    // repo's default for each icon it names that customization did not supply; a missing one
+    // 404s on every page load (Chrome fetches it to check installability).
+    const staticManifest = path.join(staticDest, 'manifest.json');
+    if (fs.existsSync(staticManifest)) {
+        let icons = [];
+        try {
+            icons = JSON.parse(fs.readFileSync(staticManifest, 'utf8')).icons || [];
+        } catch (e) {
+            console.warn('public/static/manifest.json is not valid JSON:', e.message);
+        }
+        icons.forEach(({ src }) => {
+            if (!src || src.startsWith('/') || /^[a-z]+:/i.test(src)) return;
+            const destPath = path.join(staticDest, src);
+            const defaultPath = path.resolve(__dirname, 'public', src);
+            if (!fs.existsSync(destPath) && fs.existsSync(defaultPath)) {
+                fs.mkdirSync(path.dirname(destPath), { recursive: true });
+                fs.copyFileSync(defaultPath, destPath);
+            }
+        });
+    }
 }
 
 // Run copy BEFORE loading config

+ 1 - 1
neo/apps/next/public/manifest.json

@@ -30,7 +30,7 @@
       "src": "maskable_icon_x192.png",
       "sizes": "192x192",
       "type": "image/png",
-      "purpose": "any "
+      "purpose": "any"
     },
     {
       "src": "maskable_icon_x192.png",