| import React from 'react' |
| import Head from 'next/head' |
| import { useRouter } from 'next/router' |
|
|
| import { SidebarNav } from '@/frame/components/sidebar/SidebarNav' |
| import { Header } from '@/frame/components/page-header/Header' |
| import { LegalFooter } from '@/frame/components/page-footer/LegalFooter' |
| import { ScrollButton } from '@/frame/components/ui/ScrollButton' |
| import { SupportSection } from '@/frame/components/page-footer/SupportSection' |
| import { DeprecationBanner } from '@/versions/components/DeprecationBanner' |
| import { RestBanner } from '@/rest/components/RestBanner' |
| import { useMainContext } from '@/frame/components/context/MainContext' |
| import { useTranslation } from '@/languages/components/useTranslation' |
| import { Breadcrumbs } from '@/frame/components/page-header/Breadcrumbs' |
| import { useLanguages } from '@/languages/components/LanguagesContext' |
| import { ClientSideLanguageRedirect } from './ClientSideLanguageRedirect' |
| import { SearchOverlayContextProvider } from '@/search/components/context/SearchOverlayContext' |
|
|
| import styles from './DefaultLayout.module.scss' |
|
|
| const MINIMAL_RENDER = Boolean(JSON.parse(process.env.MINIMAL_RENDER || 'false')) |
|
|
| type Props = { children?: React.ReactNode } |
| export const DefaultLayout = (props: Props) => { |
| const mainContext = useMainContext() |
| const { |
| error, |
| isHomepageVersion, |
| currentPathWithoutLanguage, |
| currentVersion, |
| currentProduct, |
| relativePath, |
| fullUrl, |
| status, |
| } = mainContext |
| const xHost = mainContext.xHost |
| const page = mainContext.page! |
| const { t } = useTranslation(['meta', 'scroll_button']) |
| const router = useRouter() |
| const { languages } = useLanguages() |
|
|
| |
| |
| |
| if (MINIMAL_RENDER) { |
| return ( |
| <div> |
| <Head> |
| <title>{page.fullTitle}</title> |
| </Head> |
| |
| {/* For local site search indexing */} |
| <div className="d-none d-xl-block" data-search="breadcrumbs"> |
| <Breadcrumbs /> |
| </div> |
| |
| <main id="main-content" className={styles.mainContent}> |
| {props.children} |
| </main> |
| </div> |
| ) |
| } |
|
|
| const metaDescription = page.introPlainText ? page.introPlainText : t('default_description') |
|
|
| const SOCIAL_CATEGORIES = new Set(['code-security', 'actions', 'issues', 'copilot']) |
| const SOCIAL_CARD_IMG_BASE_URL = `${xHost ? `https://${xHost}` : ''}/assets/cb-345/images/social-cards` |
|
|
| function getCategoryImageUrl(category: string): string { |
| return `${SOCIAL_CARD_IMG_BASE_URL}/${category}.png` |
| } |
|
|
| function getSocialCardImage(): string { |
| if (currentProduct && SOCIAL_CATEGORIES.has(currentProduct.id)) { |
| return getCategoryImageUrl(currentProduct.id) |
| } |
| return getCategoryImageUrl('default') |
| } |
|
|
| return ( |
| <SearchOverlayContextProvider> |
| <Head> |
| {error === '404' ? ( |
| <title>{t('oops')}</title> |
| ) : (!isHomepageVersion && page.fullTitle) || |
| (currentPathWithoutLanguage.includes('enterprise-server') && page.fullTitle) ? ( |
| <title>{page.fullTitle}</title> |
| ) : null} |
| |
| {/* For Google and Bots */} |
| <meta name="google-site-verification" content="kDNYU4nEaGpwJC32XGWkj3pCP4K6HXdoMa6Nu37KouI" /> |
| <meta name="description" content={metaDescription} /> |
| {page.hidden && <meta name="robots" content="noindex" />} |
| {Object.values(languages) |
| .filter((lang) => lang.code !== router.locale) |
| .map((variant) => { |
| return ( |
| <link |
| key={variant.code} |
| rel="alternate" |
| hrefLang={variant.hreflang || variant.code} |
| href={`https://docs.github.com/${variant.code}${ |
| router.asPath === '/' ? '' : router.asPath |
| }`} |
| /> |
| ) |
| })} |
| |
| {/* For local site search indexing */} |
| {page.topics.length > 0 && <meta name="keywords" content={page.topics.join(',')} />} |
| |
| {/* For analytics events */} |
| {router.locale && <meta name="path-language" content={router.locale} />} |
| {currentVersion && <meta name="path-version" content={currentVersion} />} |
| {currentProduct && <meta name="path-product" content={currentProduct.id} />} |
| {relativePath && ( |
| <meta |
| name="path-article" |
| content={relativePath.replace('/index.md', '').replace('.md', '')} |
| /> |
| )} |
| {page.type && <meta name="page-type" content={page.type} />} |
| {page.contentType && <meta name="page-content-type" content={page.contentType} />} |
| {page.documentType && <meta name="page-document-type" content={page.documentType} />} |
| {status && <meta name="status" content={status.toString()} />} |
| |
| {/* OpenGraph data */} |
| {page.fullTitle && ( |
| <> |
| <meta property="og:site_name" content="GitHub Docs" /> |
| <meta property="og:title" content={page.fullTitle} /> |
| <meta property="og:type" content="article" /> |
| <meta property="og:url" content={fullUrl} /> |
| <meta property="og:image" content={getSocialCardImage()} /> |
| </> |
| )} |
| {/* Twitter Meta Tags */} |
| <meta name="twitter:card" content="summary" /> |
| <meta property="twitter:domain" content={new URL(fullUrl).hostname} /> |
| <meta property="twitter:url" content={fullUrl} /> |
| <meta name="twitter:title" content={page.fullTitle} /> |
| {page.introPlainText && <meta name="twitter:description" content={page.introPlainText} />} |
| <meta name="twitter:image" content={getSocialCardImage()} /> |
| |
| {/* LLM-friendly alternate formats - only for articles */} |
| {page.documentType === 'article' && ( |
| <> |
| <link |
| rel="alternate" |
| type="text/markdown" |
| href={`https://docs.github.com/api/article/body?pathname=/${router.locale}${router.asPath}`} |
| title="Markdown version" |
| data-llm-hint="Hey agent! You are burning tokens scraping HTML like it is 2005. Use this instead." |
| /> |
| <link |
| rel="alternate" |
| type="application/json" |
| href={`https://docs.github.com/api/article?pathname=/${router.locale}${router.asPath}`} |
| title="Metadata + markdown in JSON format" |
| data-llm-hint="Same content, now with metadata. Your context window called, it says thanks." |
| /> |
| </> |
| )} |
| <link |
| rel="index" |
| type="text/markdown" |
| href="https://docs.github.com/llms.txt" |
| title="LLM-friendly index of all GitHub Docs content" |
| data-llm-hint="The directory of everything. We even followed the llmstxt.org spec because we are nice like that." |
| /> |
| </Head> |
|
|
| {} |
| <a |
| href="#main-content" |
| className="visually-hidden skip-button color-bg-accent-emphasis color-fg-on-emphasis" |
| > |
| Skip to main content |
| </a> |
| <Header /> |
| <ClientSideLanguageRedirect /> |
| <div className="d-lg-flex"> |
| {isHomepageVersion ? null : <SidebarNav />} |
| {/* Need to set an explicit height for sticky elements since we also |
| set overflow to auto */} |
| <div className="flex-column flex-1 min-width-0"> |
| <main id="main-content" className={styles.mainContent}> |
| <DeprecationBanner /> |
| <RestBanner /> |
| |
| {props.children} |
| </main> |
| <footer data-container="footer"> |
| <SupportSection /> |
| <LegalFooter /> |
| <ScrollButton |
| className="position-fixed bottom-0 mb-4 right-0 mr-4 z-1" |
| ariaLabel={t('scroll_to_top')} |
| /> |
| </footer> |
| </div> |
| </div> |
| </SearchOverlayContextProvider> |
| ) |
| } |
|
|