Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/website/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Metadata } from 'next';
import { EB_Garamond, Inter, JetBrains_Mono } from 'next/font/google';
import './global.css';
import { Nav } from '../components/shared/Nav';
import { Footer } from '../components/shared/Footer';
import { SiteFooter } from '../components/shared/SiteFooter';
import { AnnouncementToast } from '../components/shared/AnnouncementToast';
import { JsonLd } from '../components/shared/JsonLd';
import { rootJsonLd } from '../lib/structured-data';
Expand Down Expand Up @@ -70,7 +70,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<Nav />
<div id="site-content">
<main>{children}</main>
<Footer />
<SiteFooter />
<AnnouncementToast />
</div>
</body>
Expand Down
44 changes: 44 additions & 0 deletions apps/website/src/components/shared/SiteFooter.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// @vitest-environment jsdom
import React from 'react';
import { render } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';

const pathname = vi.hoisted(() => ({ current: '/' }));

vi.mock('next/navigation', () => ({
usePathname: () => pathname.current,
}));

import { SiteFooter } from './SiteFooter';

afterEach(() => {
pathname.current = '/';
});

/**
* The footer is mounted once, in the root layout, so it is the docs shell's
* only way to opt out of it. These cases are the contract that opt-out obeys.
*/
describe('SiteFooter', () => {
it.each(['/', '/pricing', '/blog/some-post', '/docs-adjacent'])(
'renders the marketing footer on %s',
(route) => {
pathname.current = route;

const { container } = render(<SiteFooter />);

expect(container.querySelector('footer')).toBeTruthy();
},
);

it.each(['/docs', '/docs/choosing-an-adapter', '/docs/langgraph/guides/testing'])(
'suppresses it on %s so the docs stay a single sidebar pane',
(route) => {
pathname.current = route;

const { container } = render(<SiteFooter />);

expect(container.querySelector('footer')).toBeNull();
},
);
});
17 changes: 17 additions & 0 deletions apps/website/src/components/shared/SiteFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client';
import { usePathname } from 'next/navigation';
import { Footer } from './Footer';

/**
* Route gate for the marketing footer.
*
* The footer is mounted once in the root layout, so every route gets it unless
* something opts out here. The docs tree opts out: every /docs route renders
* the sidebar control plane and ends at its own prev/next rail, and a marketing
* footer bolted under that column breaks the single-pane reading experience.
*/
export function SiteFooter() {
const pathname = usePathname();
if (pathname === '/docs' || pathname?.startsWith('/docs/')) return null;
return <Footer />;
}
Loading