Files
2026-07-13 13:32:05 +08:00

219 lines
8.5 KiB
TypeScript

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 `<title>` 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>
);
}