mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-21 01:53:26 +00:00
Simplify and generate og:image for each page
This commit is contained in:
@@ -1,54 +0,0 @@
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
import { api } from '@/lib/api';
|
||||
import { SpaceContent } from '@/components/SpaceContent';
|
||||
|
||||
interface PageParams {
|
||||
pathname?: string[];
|
||||
}
|
||||
|
||||
export default async function Page(props: { params: PageParams }) {
|
||||
const {
|
||||
params: { pathname },
|
||||
} = props;
|
||||
|
||||
const { space, revision } = await fetchData(props.params);
|
||||
|
||||
return (
|
||||
<SpaceContent
|
||||
space={space}
|
||||
revision={revision}
|
||||
pagePath={pathname ? pathname.join('/') : ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: PageParams }) {
|
||||
const { space, revision } = await fetchData(params);
|
||||
|
||||
return {
|
||||
title: space.title,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the data needed for the page.
|
||||
*/
|
||||
async function fetchData(params: PageParams) {
|
||||
const headersList = headers();
|
||||
const spaceId = headersList.get('x-gitbook-space');
|
||||
|
||||
if (!spaceId) {
|
||||
throw new Error('Missing space id');
|
||||
}
|
||||
|
||||
const [{ data: space }, { data: revision }] = await Promise.all([
|
||||
api().spaces.getSpaceById(spaceId),
|
||||
api().spaces.getCurrentRevision(spaceId),
|
||||
]);
|
||||
|
||||
return {
|
||||
space,
|
||||
revision,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
|
||||
import { NextRequest, ImageResponse } from 'next/server';
|
||||
import { PageIdParams, fetchPageData } from '../../../fetch';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
/**
|
||||
* Render the OpenGraph image for a space.
|
||||
*/
|
||||
export async function GET(req: NextRequest, { params }: { params: PageIdParams }) {
|
||||
const { space, page } = await fetchPageData(params);
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'white',
|
||||
}}
|
||||
>
|
||||
<div tw="bg-gray-50 flex flex-1">
|
||||
<div tw="flex flex-col w-full py-16 px-14">
|
||||
<h2 tw="text-7xl font-bold tracking-tight text-gray-900 text-left">
|
||||
{space.title}
|
||||
</h2>
|
||||
<p tw="text-4xl text-indigo-600">{page ? page.title : 'Not found'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { SpaceContent } from '@/components/SpaceContent';
|
||||
import { pageHref } from '@/lib/links';
|
||||
import { Metadata } from 'next';
|
||||
import { notFound, redirect } from 'next/navigation';
|
||||
import { PagePathParams, fetchPageData, getPagePath } from '../fetch';
|
||||
|
||||
/**
|
||||
* Fetch and render a page.
|
||||
*/
|
||||
export default async function Page(props: { params: PagePathParams }) {
|
||||
const { params } = props;
|
||||
|
||||
const { space, revision, page } = await fetchPageData(props.params);
|
||||
|
||||
if (!page) {
|
||||
notFound();
|
||||
} else if (page.path !== getPagePath(params)) {
|
||||
redirect(pageHref(page.path));
|
||||
}
|
||||
|
||||
return <SpaceContent space={space} revision={revision} page={page} />;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: PagePathParams }): Promise<Metadata> {
|
||||
const { space, page } = await fetchPageData(params);
|
||||
if (!page) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
title: { default: page.title, template: `%s | ${space.title}` },
|
||||
description: page.description,
|
||||
generator: 'GitBook',
|
||||
openGraph: {
|
||||
images: [pageHref('.gitbook/ogimage/' + page.id)],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { api } from '@/lib/api';
|
||||
import { Revision, RevisionPage, RevisionPageDocument } from '@gitbook/api';
|
||||
|
||||
export interface SpaceParams {
|
||||
spaceId: string;
|
||||
}
|
||||
|
||||
export interface PagePathParams extends SpaceParams {
|
||||
pathname?: string[];
|
||||
}
|
||||
|
||||
export interface PageIdParams extends SpaceParams {
|
||||
pageId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the data needed for the page.
|
||||
*/
|
||||
export async function fetchPageData(params: PagePathParams | PageIdParams) {
|
||||
const { spaceId } = params;
|
||||
|
||||
const [{ data: space }, { data: revision }] = await Promise.all([
|
||||
api().spaces.getSpaceById(spaceId),
|
||||
api().spaces.getCurrentRevision(spaceId),
|
||||
]);
|
||||
|
||||
const page =
|
||||
'pageId' in params && params.pageId
|
||||
? resolvePageId(revision, params.pageId)
|
||||
: resolvePagePath(revision, getPagePath(params));
|
||||
|
||||
return {
|
||||
space,
|
||||
revision,
|
||||
page,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the page path from the params.
|
||||
*/
|
||||
export function getPagePath(params: PagePathParams): string {
|
||||
const { pathname } = params;
|
||||
return pathname ? pathname.join('/') : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a page path to a page document.
|
||||
*/
|
||||
function resolvePagePath(revision: Revision, pagePath: string): RevisionPageDocument | undefined {
|
||||
const iteratePages = (pages: RevisionPage[]): RevisionPageDocument | undefined => {
|
||||
for (const page of pages) {
|
||||
if (page.type === 'link') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (page.path !== pagePath) {
|
||||
// TODO: can be optimized to count the number of slashes and skip the entire subtree
|
||||
const result = iteratePages(page.pages);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
return resolvePageDocument(page);
|
||||
}
|
||||
};
|
||||
|
||||
if (!pagePath) {
|
||||
const firstPage = resolveFirstDocument(revision.pages);
|
||||
if (!firstPage) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return firstPage;
|
||||
}
|
||||
|
||||
return iteratePages(revision.pages);
|
||||
}
|
||||
|
||||
function resolvePageId(revision: Revision, pageId: string): RevisionPageDocument | undefined {
|
||||
const iteratePages = (pages: RevisionPage[]): RevisionPageDocument | undefined => {
|
||||
for (const page of pages) {
|
||||
if (page.type === 'link') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (page.id === pageId) {
|
||||
return resolvePageDocument(page);
|
||||
}
|
||||
|
||||
const result = iteratePages(page.pages);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
};
|
||||
return iteratePages(revision.pages);
|
||||
}
|
||||
|
||||
function resolveFirstDocument(pages: RevisionPage[]): RevisionPageDocument | undefined {
|
||||
for (const page of pages) {
|
||||
if (page.type === 'link') {
|
||||
continue;
|
||||
}
|
||||
|
||||
return resolvePageDocument(page);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function resolvePageDocument(page: RevisionPage): RevisionPageDocument | undefined {
|
||||
if (page.type === 'group') {
|
||||
const firstDocument = resolveFirstDocument(page.pages);
|
||||
if (firstDocument) {
|
||||
return firstDocument;
|
||||
}
|
||||
|
||||
return;
|
||||
} else if (page.type === 'link') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import './globals.css';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
export default function SpaceRootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
@@ -1,95 +1,31 @@
|
||||
import { Revision, RevisionPage, RevisionPageDocument, Space } from '@gitbook/api';
|
||||
import { Revision, RevisionPageDocument, Space } from '@gitbook/api';
|
||||
|
||||
import { TableOfContents } from '@/components/TableOfContents';
|
||||
import clsx from 'clsx';
|
||||
import { Header } from '@/components/Header';
|
||||
import { PageBody } from '@/components/PageBody';
|
||||
import { notFound, redirect } from 'next/navigation';
|
||||
import { pageHref } from '@/lib/links';
|
||||
|
||||
/**
|
||||
* Render the entire content of the space (header, table of contents, footer, and page content).
|
||||
*/
|
||||
export function SpaceContent(props: { space: Space; revision: Revision; pagePath: string }) {
|
||||
const { space, revision, pagePath } = props;
|
||||
const activePage = resolvePagePath(revision, pagePath);
|
||||
export function SpaceContent(props: {
|
||||
space: Space;
|
||||
revision: Revision;
|
||||
page: RevisionPageDocument;
|
||||
}) {
|
||||
const { space, revision, page } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header space={space} />
|
||||
<div className={clsx('max-w-8xl mx-auto px-4 sm:px-6 md:px-8')}>
|
||||
<TableOfContents revision={revision} activePage={activePage} />
|
||||
<TableOfContents revision={revision} activePage={page} />
|
||||
<div className={clsx('lg:pl-[19.5rem]')}>
|
||||
<div className={clsx('max-w-3xl', 'py-8', 'px-4')}>
|
||||
<PageBody space={space} revision={revision} page={activePage} />
|
||||
<PageBody space={space} revision={revision} page={page} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function resolvePagePath(revision: Revision, pagePath: string): RevisionPageDocument {
|
||||
const resolveFirstDocument = (pages: RevisionPage[]): RevisionPageDocument | undefined => {
|
||||
for (const page of pages) {
|
||||
if (page.type === 'link') {
|
||||
continue;
|
||||
}
|
||||
|
||||
return resolvePage(page);
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
const resolvePage = (page: RevisionPage): RevisionPageDocument => {
|
||||
if (page.type === 'group') {
|
||||
const firstDocument = resolveFirstDocument(page.pages);
|
||||
if (firstDocument) {
|
||||
redirect(pageHref(firstDocument.path));
|
||||
}
|
||||
|
||||
notFound();
|
||||
} else if (page.type === 'link') {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return page;
|
||||
};
|
||||
|
||||
const iteratePages = (pages: RevisionPage[]): RevisionPageDocument | undefined => {
|
||||
for (const page of pages) {
|
||||
if (page.type === 'link') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (page.path !== pagePath) {
|
||||
// TODO: can be optimized to count the number of slashes and skip the entire subtree
|
||||
const result = iteratePages(page.pages);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
return resolvePage(page);
|
||||
}
|
||||
};
|
||||
|
||||
if (!pagePath) {
|
||||
const firstPage = resolveFirstDocument(revision.pages);
|
||||
if (!firstPage) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return firstPage;
|
||||
}
|
||||
|
||||
const result = iteratePages(revision.pages);
|
||||
if (!result) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
+9
-4
@@ -2,12 +2,17 @@ import 'server-only';
|
||||
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
/**
|
||||
* Return the base path for the current request.
|
||||
*/
|
||||
export function basePath(): string {
|
||||
const headersList = headers();
|
||||
return headersList.get('x-gitbook-basepath') ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a link to a page path in the current space.
|
||||
*/
|
||||
export function pageHref(pagePath: string): string {
|
||||
const headersList = headers();
|
||||
const basePath = headersList.get('x-gitbook-basepath') ?? '';
|
||||
|
||||
return `${basePath}/${pagePath.startsWith('/') ? pagePath.slice(1) : pagePath}`;
|
||||
return `${basePath()}/${pagePath.startsWith('/') ? pagePath.slice(1) : pagePath}`;
|
||||
}
|
||||
|
||||
+12
-11
@@ -2,24 +2,25 @@ import { NextResponse } from 'next/server';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
/**
|
||||
* Rewrite the request to extract the spaceId from the URL
|
||||
* and pass it as a header.
|
||||
* Middleware to add the base path to the request headers.
|
||||
*/
|
||||
export function middleware(request: NextRequest) {
|
||||
const url = new URL(request.url);
|
||||
const [space, ...pathRest] = url.pathname.slice(1).split('/');
|
||||
|
||||
const headers = new Headers(request.headers);
|
||||
headers.set('x-gitbook-space', space);
|
||||
headers.set('x-gitbook-basepath', `/${space}`);
|
||||
headers.set('x-gitbook-basepath', headers.get('x-gitbook-basepath') ?? getDefaultBasePath(url));
|
||||
|
||||
url.pathname = `/${pathRest.join('/')}`;
|
||||
|
||||
return NextResponse.rewrite(url, {
|
||||
headers,
|
||||
return NextResponse.next({
|
||||
request: {
|
||||
headers,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getDefaultBasePath(url: URL) {
|
||||
const [space] = url.pathname.slice(1).split('/');
|
||||
return `/${space}`;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)',
|
||||
matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user