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
72 changes: 63 additions & 9 deletions apps/docs/app/[lang]/blog/[[...slug]]/page.tsx
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { blog } from '@/lib/source';
import { getMDXComponents } from '@/mdx-components';
Expand DownExpand Up@@ -181,33 +182,86 @@ export async function generateStaticParams() {
}));
}

/**
* The blog's social card.
*
* ⚠️ The card generator at `app/og/docs/[...slug]/route.tsx` is docs-only: it
* renders from `source` (the `content/docs` loader) and has no branch for `blog`,
* so there is no per-post card to reference and this card is shared by the index
* and every post. Giving posts their own generated cards is a separate decision,
* not a gap in this wiring — it would mean a second `app/og/**` route.
*
* ⚠️ Same path as `apps/docs/app/[lang]/page.tsx`'s `HOME_CARD`; that file
* carries the note on why it is spelled twice and what must change together.
*/
const BLOG_CARD = {
url: '/hero-cover-dark.png',
width: 2400,
height: 1200,
alt: 'ObjectStack — the metadata framework for AI-written apps',
};

const BLOG_INDEX_TITLE = 'Blog';
const BLOG_INDEX_DESCRIPTION =
'Insights, updates, and best practices from the ObjectStack team.';

export async function generateMetadata({
params,
}: {
params: Promise<{ slug?: string[] }>;
}) {
}): Promise<Metadata> {
const { slug } = await params;

// If no slug, return default metadata for blog index
if (!slug || slug.length === 0) {
// The index has no MDX file behind it, so its route is spelled out here — the
// same literal `app/sitemap.ts` lists it under.
const canonical = absoluteUrl('/blog');

return {
title: 'Blog',
description: 'Insights, updates, and best practices from the ObjectStack team.',
// The index has no MDX file behind it, so its route is spelled out here — the
// same literal `app/sitemap.ts` lists it under.
alternates: { canonical: absoluteUrl('/blog') },
title: BLOG_INDEX_TITLE,
description: BLOG_INDEX_DESCRIPTION,
alternates: { canonical },
openGraph: {
type: 'website',
title: BLOG_INDEX_TITLE,
description: BLOG_INDEX_DESCRIPTION,
url: canonical,
images: [BLOG_CARD],
},
twitter: {
card: 'summary_large_image',
title: BLOG_INDEX_TITLE,
description: BLOG_INDEX_DESCRIPTION,
images: [BLOG_CARD.url],
},
};
}

const page = blog.getPage(slug);

if (!page) {
notFound();
}

const canonical = absoluteUrl(page.url);

return {
title: page.data.title,
description: page.data.description,
alternates: { canonical: absoluteUrl(page.url) },
alternates: { canonical },
openGraph: {
type: 'article',
title: page.data.title,
description: page.data.description,
url: canonical,
images: [BLOG_CARD],
},
twitter: {
card: 'summary_large_image',
title: page.data.title,
description: page.data.description,
images: [BLOG_CARD.url],
},
};
}
54 changes: 46 additions & 8 deletions apps/docs/app/[lang]/docs/[[...slug]]/page.tsx
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { source } from '@/lib/source';
import { getPageImage, source } from '@/lib/source';
import type { Metadata } from 'next';
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
import { notFound } from 'next/navigation';
Expand DownExpand Up@@ -61,15 +61,53 @@ export async function generateMetadata(props: {
const page = source.getPage(params.slug ?? [], params.lang);
if (!page) notFound();

/**
* `app/og/docs/[...slug]/route.tsx` already prerenders a 1200x630 card for every
* page this loader returns -- its `generateStaticParams` maps over
* `source.getPages()` through this very function. Calling `getPageImage()` here
* too is what makes the card *reachable*: the generator and the reference are
* then the same expression, so a slug shape that stops matching breaks the build
* rather than emitting an `og:image` that 404s. A 404ing card is worse than no
* card at all, because the crawler falls back to scraping whatever else it finds.
*
* The URL is left site-relative on purpose: `metadataBase` in `app/layout.tsx`
* absolutises it, so the origin stays spelled in exactly one place.
*/
const image = getPageImage(page);
/**
* `page.url` is the same locale-stripped route fumadocs uses for in-site links
* and that `app/sitemap.ts` lists, so the canonical link and the sitemap entry
* cannot drift apart. `absoluteUrl()` throws rather than emit a URL on another
* host if that ever stops being a site-relative path.
*/
const canonical = absoluteUrl(page.url);

return {
title: page.data.title,
description: page.data.description,
/**
* `page.url` is the same locale-stripped route fumadocs uses for in-site links
* and that `app/sitemap.ts` lists, so the canonical link and the sitemap entry
* cannot drift apart. `absoluteUrl()` throws rather than emit a URL on another
* host if that ever stops being a site-relative path.
*/
alternates: { canonical: absoluteUrl(page.url) },
alternates: { canonical },
openGraph: {
type: 'article',
title: page.data.title,
description: page.data.description,
// Same absolute URL as the canonical link, deliberately: `og:url` is the
// identity a social platform de-duplicates shares by, and pointing it at a
// different spelling than the canonical splits one page into two.
url: canonical,
images: [
{
url: image.url,
width: 1200,
height: 630,
alt: page.data.title,
},
],
},
twitter: {
card: 'summary_large_image',
title: page.data.title,
description: page.data.description,
images: [image.url],
},
};
}
49 changes: 46 additions & 3 deletions apps/docs/app/[lang]/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,17 +22,60 @@ const mono = IBM_Plex_Mono({
variable: '--font-l-mono',
});

const HOME_TITLE = 'Metadata framework for AI-written apps';
const HOME_DESCRIPTION =
'ObjectStack turns the whole app — data model, UI, workflows, permissions — into typed metadata: a complete CRM in under 150k tokens, one context window.';

/**
* The homepage's social card.
*
* ⚠️ Deliberately NOT the docs card generator. `app/og/docs/[...slug]/route.tsx`
* renders from a `source.getPage()` result, and the homepage has no MDX file
* behind it — there is no slug to hand it. It reuses the hero cover instead,
* which is already shipped and already the video poster on this page, so the
* shared card costs no extra bytes and no extra route.
*
* ⚠️ `apps/docs/app/[lang]/blog/[[...slug]]/page.tsx` spells this same path for
* the blog's card. Two spellings rather than one shared constant because
* `lib/site.ts` is the origin's home, not the asset manifest's; if a third page
* ever needs it, hoist it there. Anything that renames, re-encodes or deletes
* `public/hero-cover-dark.png` must update BOTH — an `og:image` that 404s is
* worse than none, because crawlers then scrape whatever else the page offers.
*
* Left site-relative: `metadataBase` in `app/layout.tsx` absolutises it.
*/
const HOME_CARD = {
url: '/hero-cover-dark.png',
width: 2400,
height: 1200,
alt: 'ObjectStack — the metadata framework for AI-written apps',
};

export const metadata: Metadata = {
title: 'Metadata framework for AI-written apps',
description:
'ObjectStack turns the whole app — data model, UI, workflows, permissions — into typed metadata: a complete CRM in under 150k tokens, one context window.',
title: HOME_TITLE,
description: HOME_DESCRIPTION,
/**
* `/` is the one indexable spelling of the homepage: `proxy.ts` rewrites `/` to
* this route internally, and the prefixed form `/en` 307s back to `/`. Every
* other spelling a crawler reaches it by — query strings, tracking parameters —
* points here.
*/
alternates: { canonical: absoluteUrl('/') },
openGraph: {
type: 'website',
title: HOME_TITLE,
description: HOME_DESCRIPTION,
// Same absolute URL as the canonical link — see the docs route for why the
// two must not drift.
url: absoluteUrl('/'),
images: [HOME_CARD],
},
twitter: {
card: 'summary_large_image',
title: HOME_TITLE,
description: HOME_DESCRIPTION,
images: [HOME_CARD.url],
},
};

const VOCABULARY: { tag: string; title: string; copy: string }[] = [
Expand Down
Loading