Adding /rep to an iPad home screen and tapping it opened the main app, not the rep app — while the shortcut URL was still /rep. Cause: Next 16 renders `appleWebApp.capable: true` as the standardised <meta name="mobile-web-app-capable"> and no longer emits the apple-prefixed name. iOS only reads the apple-prefixed one. Without it the icon is a plain Safari bookmark rather than a standalone app, so tapping it foregrounds Safari on whatever tab was already open — which presents exactly as "it opened the main app instead". This was not specific to /rep. Verified against the running app that /m (Boss Phone) had the same gap, so it has never actually launched standalone on iOS either; /counter likewise. The comment in (counter)/layout.tsx asserting the root layout already supplied this tag was wrong — the root also only set `appleWebApp.capable`. Declared in the root layout so all four surfaces inherit it, and repeated in the (rep) and (mobile) layouts because a child's `metadata.other` replaces the parent's rather than merging into it. Corrected the misleading counter comment. Verified by fetching each route and asserting the rendered head: /rep, /m, /new-order and /dashboard now all emit apple-mobile-web-app-capable="yes" while still resolving their own manifest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
211 lines
6.5 KiB
TypeScript
211 lines
6.5 KiB
TypeScript
import type { Metadata, Viewport } from "next";
|
|
import {
|
|
Geist,
|
|
Geist_Mono,
|
|
Inter,
|
|
Space_Grotesk,
|
|
DM_Sans,
|
|
Playfair_Display,
|
|
Lato,
|
|
Nunito,
|
|
Outfit,
|
|
JetBrains_Mono,
|
|
} from "next/font/google";
|
|
import "leaflet/dist/leaflet.css";
|
|
import "./globals.css";
|
|
import { ThemeProvider } from "@/components/theme-provider";
|
|
import { AuthProvider } from "@/components/auth-provider";
|
|
import { Toaster } from "@/components/ui/sonner";
|
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
import { ServiceWorkerRegistration } from "@/components/service-worker-registration";
|
|
import { ThemeBootstrapScript } from "@/components/theme-bootstrap";
|
|
|
|
// Geist (default app fonts)
|
|
const geistSans = Geist({
|
|
variable: "--font-geist-sans",
|
|
subsets: ["latin"],
|
|
});
|
|
const geistMono = Geist_Mono({
|
|
variable: "--font-geist-mono",
|
|
subsets: ["latin"],
|
|
});
|
|
|
|
// Theme-bound fonts (registered as CSS vars for the theme picker to swap).
|
|
// Each font is restricted to the two weights actually used in the app
|
|
// (400 regular + 700 bold). Without these, next/font/google preloads
|
|
// every available weight and the browser logs warnings about unused
|
|
// preload tags (and the page wastes bandwidth fetching woff2 files
|
|
// that will never render).
|
|
const inter = Inter({
|
|
variable: "--font-inter",
|
|
subsets: ["latin"],
|
|
weight: ["400", "700"],
|
|
display: "swap",
|
|
});
|
|
const spaceGrotesk = Space_Grotesk({
|
|
variable: "--font-space-grotesk",
|
|
subsets: ["latin"],
|
|
weight: ["400", "700"],
|
|
display: "swap",
|
|
});
|
|
const dmSans = DM_Sans({
|
|
variable: "--font-dm-sans",
|
|
subsets: ["latin"],
|
|
weight: ["400", "700"],
|
|
display: "swap",
|
|
});
|
|
const playfair = Playfair_Display({
|
|
variable: "--font-playfair",
|
|
subsets: ["latin"],
|
|
weight: ["400", "700"],
|
|
display: "swap",
|
|
});
|
|
const lato = Lato({
|
|
variable: "--font-lato",
|
|
subsets: ["latin"],
|
|
weight: ["400", "700"],
|
|
display: "swap",
|
|
});
|
|
const nunito = Nunito({
|
|
variable: "--font-nunito",
|
|
subsets: ["latin"],
|
|
weight: ["400", "700"],
|
|
display: "swap",
|
|
});
|
|
const outfit = Outfit({
|
|
variable: "--font-outfit",
|
|
subsets: ["latin"],
|
|
weight: ["400", "700"],
|
|
display: "swap",
|
|
});
|
|
const jetbrainsMono = JetBrains_Mono({
|
|
variable: "--font-jetbrains-mono",
|
|
subsets: ["latin"],
|
|
weight: ["400", "700"],
|
|
display: "swap",
|
|
});
|
|
|
|
export const viewport: Viewport = {
|
|
width: "device-width",
|
|
initialScale: 1,
|
|
viewportFit: "cover",
|
|
themeColor: "#4C96D7",
|
|
};
|
|
|
|
// `metadataBase` is required for OpenGraph / Twitter absolute URL resolution,
|
|
// otherwise Next falls back to `http://localhost:4001` in dev and emits a
|
|
// warning on every page render. We re-use the public PocketBase URL since
|
|
// the app and PB sit on the same host in dev, and in prod the same env var
|
|
// points at the deployed origin (`NEXT_PUBLIC_*` is also bundle-visible).
|
|
const METADATA_BASE =
|
|
process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/$/, "") ||
|
|
(typeof process.env.NEXT_PUBLIC_POCKETBASE_URL === "string"
|
|
? process.env.NEXT_PUBLIC_POCKETBASE_URL.replace(/\/+$/, "")
|
|
: "http://localhost:4001");
|
|
|
|
export const metadata: Metadata = {
|
|
metadataBase: new URL(METADATA_BASE),
|
|
title: "WFDASH - Business CRM",
|
|
description: "Business CRM for managing customers, companies, suppliers, quotes, and orders",
|
|
manifest: "/manifest.json",
|
|
// Counter PWA manifest — iOS/Android pick the most-specific match
|
|
// for the installed app icon + start_url when this is the only
|
|
// <link rel="manifest"> they recognise. The main `/manifest.json`
|
|
// stays the default for the main app.
|
|
icons: {
|
|
icon: [
|
|
{ url: "/favicon.ico", sizes: "any" },
|
|
{ url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
|
|
{ url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
|
|
{ url: "/favicon-96x96.png", sizes: "96x96", type: "image/png" },
|
|
{ url: "/android-chrome-192x192.png", sizes: "192x192", type: "image/png" },
|
|
{ url: "/android-chrome-512x512.png", sizes: "512x512", type: "image/png" },
|
|
],
|
|
apple: "/apple-touch-icon.png",
|
|
},
|
|
authors: [{ name: "WFDASH" }],
|
|
keywords: ["CRM", "Business", "Quotes", "Orders", "Customers", "Suppliers"],
|
|
appleWebApp: {
|
|
capable: true,
|
|
statusBarStyle: "default",
|
|
title: "WFDASH",
|
|
},
|
|
other: {
|
|
// Next 16 renders `appleWebApp.capable: true` as the standardised
|
|
// `<meta name="mobile-web-app-capable">` and no longer emits the
|
|
// apple-prefixed name — which is the only one iOS actually reads.
|
|
//
|
|
// Without this, an "Add to Home Screen" icon is a plain Safari
|
|
// bookmark rather than a standalone app: tapping it foregrounds
|
|
// Safari on whatever tab was already open, which presents as "it
|
|
// opened the main app instead of mine". Verified missing on /m and
|
|
// /rep against the running app before this was added.
|
|
//
|
|
// Declared at the root so every surface inherits it (/, /m,
|
|
// /counter, /rep). The PWA layouts that define their own `other`
|
|
// repeat it, because a child's `other` replaces the parent's
|
|
// rather than merging into it.
|
|
"apple-mobile-web-app-capable": "yes",
|
|
},
|
|
openGraph: {
|
|
type: "website",
|
|
title: "WFDASH - Business CRM",
|
|
description: "Business CRM for managing customers, companies, suppliers, quotes, and orders",
|
|
siteName: "WFDASH",
|
|
images: [
|
|
{
|
|
url: "/og-image.png",
|
|
width: 1200,
|
|
height: 630,
|
|
alt: "WFDASH - Business CRM",
|
|
},
|
|
],
|
|
},
|
|
twitter: {
|
|
card: "summary_large_image",
|
|
title: "WFDASH - Business CRM",
|
|
description: "Business CRM for managing customers, companies, suppliers, quotes, and orders",
|
|
images: ["/twitter-image.png"],
|
|
},
|
|
};
|
|
|
|
// All theme-bound CSS vars must be present on <html> so themes can swap them
|
|
const FONT_VARS = [
|
|
geistSans.variable,
|
|
geistMono.variable,
|
|
inter.variable,
|
|
spaceGrotesk.variable,
|
|
dmSans.variable,
|
|
playfair.variable,
|
|
lato.variable,
|
|
nunito.variable,
|
|
outfit.variable,
|
|
jetbrainsMono.variable,
|
|
].join(" ");
|
|
|
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<html
|
|
lang="en"
|
|
className={`${FONT_VARS} h-full antialiased`}
|
|
data-theme="wfdash"
|
|
suppressHydrationWarning
|
|
>
|
|
<head>
|
|
{/* Apply persisted theme before hydration to prevent flash of wrong theme */}
|
|
<ThemeBootstrapScript />
|
|
</head>
|
|
<body className="min-h-full flex flex-col">
|
|
<ServiceWorkerRegistration />
|
|
<ThemeProvider>
|
|
<TooltipProvider>
|
|
<AuthProvider>
|
|
{children}
|
|
</AuthProvider>
|
|
</TooltipProvider>
|
|
<Toaster richColors />
|
|
</ThemeProvider>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|