import type { Metadata } from 'next'; import Script from 'next/script'; import { RootProvider } from 'fumadocs-ui/provider/next'; import { LanguageProvider } from '@/components/lang/language-provider'; import './global.css'; import 'katex/dist/katex.css'; import { Geist, Space_Grotesk } from 'next/font/google'; import UtmCapture from '@/src/layouts/UtmCapture'; import SchemaInjector from '@/src/components/SchemaInjector/SchemaInjector'; import { buildWebSiteSchema } from '@/src/utils/schema-helpers'; import { appName, kapaConfig, siteDescription, siteTitle, siteUrl, } from '@/lib/shared'; const sans = Geist({ subsets: ['latin'], variable: '--font-sans', display: 'swap', }); const heading = Space_Grotesk({ subsets: ['latin'], weight: ['300', '400', '500', '600', '700'], variable: '--font-heading', display: 'swap', }); const disabledSearchHotKey = [ { key: "__disabled__", display: null, }, ]; // `%s` template mirrors Docusaurus' default `` format so every // SERP entry still reads "Page Title | {siteTitle}". // // `openGraph` / `twitter` defaults here set the site-wide baseline that // every section inherits (Next's `generateMetadata` deep-merges onto // this object). Per-page routes can override individual fields — the // blog section adds `openGraph.type = 'article'` + publishedTime, the // docs section adds per-page OG images, etc. export const metadata: Metadata = { metadataBase: new URL(siteUrl), title: { default: siteTitle, template: `%s | ${siteTitle}`, }, description: siteDescription, alternates: { canonical: '/' }, openGraph: { type: 'website', siteName: appName, url: siteUrl, title: siteTitle, description: siteDescription, // Site-wide fallback preview image. Every section/page inherits // this unless it overrides `openGraph.images` (the docs section // does, swapping in a per-page `/og/docs/.../image.png`). Mirrors // the old Docusaurus `themeConfig.image = 'img/social_card.png'` // default so guides/tutorials/integrations/blog/changelog/home // never end up with a blank link preview on social shares. images: '/img/social_card.png', }, twitter: { card: 'summary_large_image', site: '@deepeval', creator: '@deepeval', title: siteTitle, description: siteDescription, // Deliberately no `images:` here — we rely on X's documented // fallback to `og:image` when `twitter:image` is absent. Setting // it explicitly would stick the generic social card even on // docs pages whose `og:image` is overridden per-page (Next // replaces the whole `twitter` block across nested // `generateMetadata` calls instead of deep-merging, so a section // override of just `twitter.images` would clobber the `card` / // `site` / `creator` fields here). LinkedIn / Slack / Discord / // Facebook read `og:image` directly, so the single `og:image` // source-of-truth covers every surface. }, }; // Organization schema mirrored from the old Docusaurus `headTags` block // (docusaurus.config.ts:161-181). Rendered once in <head> via the App // Router layout — Next will keep JSON-LD scripts where they are placed. const organizationJsonLd = { '@context': 'https://schema.org', '@type': 'Organization', name: 'Confident AI Inc.', alternateName: 'Confident AI', url: siteUrl, logo: `${siteUrl}/icons/DeepEval.svg`, sameAs: [ 'https://github.com/confident-ai/deepeval', 'https://x.com/deepeval', 'https://discord.gg/a3K9c8GRGt', 'https://www.reddit.com/r/deepeval/', ], }; export default function Layout({ children }: LayoutProps<'/'>) { return ( <html lang="en" className={`${sans.variable} ${heading.variable}`} suppressHydrationWarning > <head> {/* Two site-wide JSON-LD blocks rendered once per page: `Organization` (mirrored from the old Docusaurus `headTags`) and `WebSite` (so crawlers have a canonical top-level entity to hang everything else off of). Both use the shared `SchemaInjector` helper, which safely escapes `</` inside the serialized JSON. */} <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationJsonLd) }} /> <SchemaInjector schema={buildWebSiteSchema()} /> {/* Kapa.ai "Ask AI" widget. Deliberately rendered as a native `<script async>` in <head> rather than `next/script` — Kapa's bundle reads its config off `document.currentScript.dataset` during its initial parse, and `next/script`'s loader rewrites / relocates the tag in a way that drops those attributes from the currentScript reference at runtime. A plain `<script async>` lands in the initial HTML exactly the way Docusaurus' old `scripts` block did, which is what the widget expects. Attribute names here match Kapa's current (post-2024) widget API, which renamed several of the legacy attributes the old deepeval.com config used: legacy → current data-button-hide → data-launcher-button-hidden data-button-text-color → (removed — use component styles) data-modal-disclaimer → data-chat-disclaimer data-modal-example-questions → data-example-questions data-modal-override-selector → data-modal-override-open-selector (or -open-class for bare names) `data-launcher-button-hidden="true"` hides the floating launcher; `data-modal-override-open-class="ask-ai-trigger"` opens the modal on any click that hits an element with that class — see `<AskAIButton>`. Using the `-class` variant instead of `-selector` keeps the config value as a bare class name (no leading dot to escape). */} {/* Kapa component-style overrides (see the component table at docs.kapa.ai/integrations/website-widget/configuration/component-styles). The default modal, inner wrapper, and the example-question buttons all ship with generous rounded corners; we flatten every layer so Kapa's modal reads as one sharp rectangle that matches the rest of the DeepEval site (buttons, callouts, and code blocks are all square). */} <script async src="https://widget.kapa.ai/kapa-widget.bundle.js" data-website-id={kapaConfig.websiteId} data-project-name={kapaConfig.projectName} data-project-color={kapaConfig.projectColor} data-project-logo={kapaConfig.projectLogo} data-modal-title={kapaConfig.modalTitle} data-chat-disclaimer={kapaConfig.chatDisclaimer} data-example-questions={kapaConfig.exampleQuestions} data-uncertain-answer-callout={kapaConfig.uncertainAnswerCallout} data-launcher-button-hidden="true" data-modal-override-open-class={kapaConfig.triggerClass} data-modal-border-radius="0" data-modal-inner-border-radius="0" data-modal-content-border-radius="0" data-modal-header-border-radius="0" data-example-question-button-border-radius="0" data-query-input-border-radius="0" /> </head> <body className="flex flex-col min-h-screen font-sans"> <UtmCapture /> <RootProvider search={{ hotKey: disabledSearchHotKey }}> <LanguageProvider>{children}</LanguageProvider> </RootProvider> {/* Analytics parity with the old Docusaurus site (docusaurus.config.ts:111-127). `afterInteractive` keeps these out of the critical path while still firing on every page navigation — same effective behavior as the old `<script defer>` tags. */} <Script src="https://www.googletagmanager.com/gtag/js?id=G-N2EGDDYG9M" strategy="afterInteractive" /> <Script id="ga-init" strategy="afterInteractive"> {`window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-N2EGDDYG9M');`} </Script> <Script src="https://plausible.io/js/script.tagged-events.js" data-domain="deepeval.com" strategy="afterInteractive" /> </body> </html> ); }