wacrm/src/app/layout.tsx
Arnas Donauskas 15e4b5366d
feat(seo): full SEO pass (reapply with OG-image build fix)
Re-applies the original SEO work from PR #47 that was reverted in
PR #54 because its opengraph-image.tsx crashed `next build` during
static prerender of /opengraph-image. Satori (next/og's renderer)
errored with:

  Error: Expected <div> to have explicit "display: flex",
         "display: contents", or "display: none" if it has more
         than one child node.
  Failed to load dynamic font for ✓ . Status: 400

Root cause in the OG image JSX:
- Several <div>s held text + a sibling element (e.g. "Run your
  WhatsApp business from <span>one inbox.</span>") without an
  explicit display value. Satori requires it whenever there is
  more than one child.
- The ✓ Unicode glyph isn't covered by the default font Satori
  loads, so next/og tried to download a dynamic font and failed.

Fix:
- Every <div> with more than one child now has display: flex.
- Multi-span headline sits in a flex-wrap row of two <span>s.
- ✓ replaced with plain text dot separators in the feature row.

Verified end-to-end: `npm run build` now completes with 27/27
pages prerendered, /opengraph-image among them.

All the other SEO-pass content is identical to the originally
merged-and-reverted work: metadataBase + title template, OpenGraph
+ Twitter Cards defaults, JSON-LD (WebSite + Organization +
SoftwareApplication + FAQPage), sitemap.ts, robots.ts, noindex on
auth + dashboard route-group layouts, shared faq-data + site-config
modules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:25:59 +03:00

112 lines
2.6 KiB
TypeScript

import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
import { Toaster } from "sonner";
import "./globals.css";
import {
OG_IMAGE_ALT,
SITE_DESCRIPTION,
SITE_KEYWORDS,
SITE_NAME,
SITE_TAGLINE,
SITE_URL,
} from "@/lib/seo/site-config";
const inter = Inter({
variable: "--font-sans",
subsets: ["latin"],
});
// Consolidated site-wide metadata. Page-specific metadata (landing,
// auth pages, etc.) override the narrower fields (title, description,
// robots); defaults defined here are inherited everywhere else so we
// never ship a page without OpenGraph / Twitter coverage.
export const metadata: Metadata = {
metadataBase: new URL(SITE_URL),
title: {
default: `${SITE_NAME} — ${SITE_TAGLINE}`,
template: `%s — ${SITE_NAME}`,
},
description: SITE_DESCRIPTION,
keywords: SITE_KEYWORDS,
applicationName: SITE_NAME,
authors: [{ name: SITE_NAME, url: SITE_URL }],
creator: SITE_NAME,
publisher: SITE_NAME,
category: "Business Software",
alternates: {
canonical: "/",
},
openGraph: {
type: "website",
siteName: SITE_NAME,
title: `${SITE_NAME} — ${SITE_TAGLINE}`,
description: SITE_DESCRIPTION,
url: SITE_URL,
locale: "en_US",
images: [
{
url: "/opengraph-image",
width: 1200,
height: 630,
alt: OG_IMAGE_ALT,
},
],
},
twitter: {
card: "summary_large_image",
title: `${SITE_NAME} — ${SITE_TAGLINE}`,
description: SITE_DESCRIPTION,
images: ["/opengraph-image"],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
"max-snippet": -1,
},
},
icons: {
icon: [{ url: "/icon" }],
shortcut: ["/favicon.ico"],
},
formatDetection: {
email: false,
address: false,
telephone: false,
},
};
// Dark theme color for the mobile browser chrome.
export const viewport: Viewport = {
themeColor: "#020617",
colorScheme: "dark",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className={`${inter.variable} h-full antialiased`}>
<body className="min-h-full bg-slate-950 text-white font-sans">
{children}
<Toaster
theme="dark"
position="top-right"
toastOptions={{
style: {
background: "rgb(30 41 59)",
border: "1px solid rgb(51 65 85)",
color: "white",
},
}}
/>
</body>
</html>
);
}