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
2 changes: 2 additions & 0 deletions apps/website/app/(docs)/docs/(landing)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { ArrowRight } from "lucide-react";
import { Card, CardContent } from "@repo/ui/components/ui/card";
import { PlatformBadge } from "~/components/PlatformBadge";
import { Logo } from "~/components/Logo";
import { getCanonicalMetadata, PUBLIC_STATIC_PATHS } from "~/seo";

export const metadata: Metadata = {
title: "Documentation",
description:
"Choose the Discourse Graphs documentation for Roam Research or Obsidian.",
...getCanonicalMetadata(PUBLIC_STATIC_PATHS.docs),
};

const DOCS_DESTINATIONS = [
Expand Down
13 changes: 11 additions & 2 deletions apps/website/app/(docs)/docs/obsidian/[[...mdxPath]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { generateStaticParamsFor, importPage } from "nextra/pages";
import DocsPageTemplate from "../../_components/DocsPageTemplate";
import { getCanonicalMetadata, getDocsPath } from "~/seo";

type DocsPageProps = {
params: Promise<{
Expand Down Expand Up @@ -52,16 +53,24 @@ const Page = async ({ params }: DocsPageProps): Promise<React.ReactElement> => {
export const generateMetadata = async ({
params,
}: DocsPageProps): Promise<Metadata> => {
const { mdxPath } = await params;
const canonicalMetadata = getCanonicalMetadata(
getDocsPath({ mdxPath, platform: "obsidian" }),
);

try {
const { mdxPath } = await params;
const { metadata } = await loadPage(mdxPath);

return metadata;
return {
...metadata,
...canonicalMetadata,
};
} catch (error) {
console.error("Error generating Obsidian docs metadata:", error);

return {
title: "Obsidian docs",
...canonicalMetadata,
};
}
};
Expand Down
13 changes: 11 additions & 2 deletions apps/website/app/(docs)/docs/roam/[[...mdxPath]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { generateStaticParamsFor, importPage } from "nextra/pages";
import DocsPageTemplate from "../../_components/DocsPageTemplate";
import { getCanonicalMetadata, getDocsPath } from "~/seo";

type DocsPageProps = {
params: Promise<{
Expand Down Expand Up @@ -52,16 +53,24 @@ const Page = async ({ params }: DocsPageProps): Promise<React.ReactElement> => {
export const generateMetadata = async ({
params,
}: DocsPageProps): Promise<Metadata> => {
const { mdxPath } = await params;
const canonicalMetadata = getCanonicalMetadata(
getDocsPath({ mdxPath, platform: "roam" }),
);

try {
const { mdxPath } = await params;
const { metadata } = await loadPage(mdxPath);

return metadata;
return {
...metadata,
...canonicalMetadata,
};
} catch (error) {
console.error("Error generating Roam docs metadata:", error);

return {
title: "Roam docs",
...canonicalMetadata,
};
}
};
Expand Down
2 changes: 2 additions & 0 deletions apps/website/app/(extract)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import "~/globals.css";
import Image from "next/image";
import Link from "next/link";
import { Inter } from "next/font/google";
import { getCanonicalMetadata, PUBLIC_STATIC_PATHS } from "~/seo";

export const metadata: Metadata = {
title: "Extract Nodes | Discourse Graphs",
description:
"Extract structured discourse graph nodes from academic papers using AI.",
...getCanonicalMetadata(PUBLIC_STATIC_PATHS.extractNodes),
};

const inter = Inter({
Expand Down
12 changes: 9 additions & 3 deletions apps/website/app/(home)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { notFound } from "next/navigation";
import { importPage } from "nextra/pages";
import type { BlogData } from "../blogSchema";
import { getAllBlogs, getBlogBySlug } from "../readBlogs";
import { getBlogPostPath, getCanonicalMetadata, getCanonicalUrl } from "~/seo";

type Params = {
params: Promise<{
Expand Down Expand Up @@ -39,6 +40,7 @@ const buildBlogPostMetadata = ({
description: blog.description,
metadata: pageMetadata,
});
const canonicalPath = getBlogPostPath(blog.slug);

return {
...pageMetadata,
Expand All @@ -48,7 +50,7 @@ const buildBlogPostMetadata = ({
keywords: blog.tags.length ? blog.tags : pageMetadata.keywords,
alternates: {
...pageMetadata.alternates,
canonical: `/blog/${blog.slug}`,
...getCanonicalMetadata(canonicalPath).alternates,
},
openGraph: {
...pageMetadata.openGraph,
Expand All @@ -58,7 +60,7 @@ const buildBlogPostMetadata = ({
publishedTime: blog.date,
authors: [blog.author],
tags: blog.tags.length ? blog.tags : undefined,
url: `/blog/${blog.slug}`,
url: getCanonicalUrl(canonicalPath),
},
twitter: {
...pageMetadata.twitter,
Expand Down Expand Up @@ -124,13 +126,16 @@ export const generateStaticParams = async (): Promise<
export const generateMetadata = async ({
params,
}: Params): Promise<Metadata> => {
const { slug } = await params;
const canonicalMetadata = getCanonicalMetadata(getBlogPostPath(slug));

try {
const { slug } = await params;
const blog = await getBlogBySlug(slug);

if (!blog) {
return {
title: "Blog Post",
...canonicalMetadata,
};
}

Expand All @@ -144,6 +149,7 @@ export const generateMetadata = async ({
console.error("Error generating metadata:", error);
return {
title: "Blog Post",
...canonicalMetadata,
};
}
};
Expand Down
7 changes: 6 additions & 1 deletion apps/website/app/(home)/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from "next";
import Link from "next/link";
import { importPage } from "nextra/pages";
import { getAllBlogs } from "./readBlogs";
import { getCanonicalMetadata, PUBLIC_STATIC_PATHS } from "~/seo";

type ImportedPage = Awaited<ReturnType<typeof importPage>>;

Expand Down Expand Up @@ -70,12 +71,16 @@ export const generateMetadata = async (): Promise<Metadata> => {
try {
const { metadata } = await loadBlogIndex();

return metadata;
return {
...metadata,
...getCanonicalMetadata(PUBLIC_STATIC_PATHS.blog),
};
} catch (error) {
console.error("Error generating blog index metadata:", error);

return {
title: "All Updates",
...getCanonicalMetadata(PUBLIC_STATIC_PATHS.blog),
};
}
};
Expand Down
6 changes: 6 additions & 0 deletions apps/website/app/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Metadata } from "next";
import type { ReactElement, ReactNode } from "react";
import Image from "next/image";
import Link from "next/link";
Expand All @@ -19,6 +20,11 @@ import { Logo } from "~/components/Logo";
import { PlatformBadge } from "~/components/PlatformBadge";
import { TeamPerson } from "~/components/TeamPerson";
import { TEAM_MEMBERS } from "~/data/constants";
import { getCanonicalMetadata, PUBLIC_STATIC_PATHS } from "~/seo";

export const metadata: Metadata = getCanonicalMetadata(
PUBLIC_STATIC_PATHS.home,
);

const SLACK_URL =
"https://join.slack.com/t/discoursegraphs/shared_invite/zt-37xklatti-cpEjgPQC0YyKYQWPNgAkEg";
Expand Down
7 changes: 7 additions & 0 deletions apps/website/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import type { Metadata } from "next";
import { PRODUCTION_SITE_URL } from "./seo";

type RootLayoutProps = {
children: React.ReactNode;
};

export const metadata: Metadata = {
metadataBase: PRODUCTION_SITE_URL,
};

const RootLayout = ({ children }: RootLayoutProps): React.ReactElement => {
return (
<html lang="en" suppressHydrationWarning>
Expand Down
50 changes: 50 additions & 0 deletions apps/website/app/seo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, it } from "vitest";
import {
PRODUCTION_SITE_URL,
PUBLIC_STATIC_PATHS,
getBlogPostPath,
getCanonicalMetadata,
getCanonicalUrl,
getDocsPath,
} from "./seo";

describe("canonical metadata", () => {
it("uses the production website origin", () => {
expect(PRODUCTION_SITE_URL.href).toBe("https://discoursegraphs.com/");
});

it("keeps the public static route inventory canonical and absolute", () => {
expect(
Object.fromEntries(
Object.entries(PUBLIC_STATIC_PATHS).map(([route, pathname]) => [
route,
getCanonicalUrl(pathname).href,
]),
),
).toEqual({
blog: "https://discoursegraphs.com/blog",
docs: "https://discoursegraphs.com/docs",
extractNodes: "https://discoursegraphs.com/extract-nodes",
home: "https://discoursegraphs.com/",
});
});

it("creates canonical metadata with an absolute URL", () => {
expect(getCanonicalMetadata(PUBLIC_STATIC_PATHS.docs)).toEqual({
alternates: {
canonical: new URL("https://discoursegraphs.com/docs"),
},
});
});

it("builds canonical blog and docs paths without duplicate variants", () => {
expect(getBlogPostPath("release notes")).toBe("/blog/release%20notes");
expect(getDocsPath({ platform: "roam" })).toBe("/docs/roam");
expect(
getDocsPath({
platform: "obsidian",
mdxPath: ["welcome", "getting started"],
}),
).toBe("/docs/obsidian/welcome/getting%20started");
});
});
36 changes: 36 additions & 0 deletions apps/website/app/seo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Metadata } from "next";

export const PRODUCTION_SITE_URL = new URL("https://discoursegraphs.com");

export const PUBLIC_STATIC_PATHS = {
blog: "/blog",
docs: "/docs",
extractNodes: "/extract-nodes",
home: "/",
} as const;

export const getCanonicalUrl = (pathname: string): URL =>
new URL(pathname, PRODUCTION_SITE_URL);

export const getCanonicalMetadata = (
pathname: string,
): Pick<Metadata, "alternates"> => ({
alternates: {
canonical: getCanonicalUrl(pathname),
},
});

export const getBlogPostPath = (slug: string): string =>
`/blog/${encodeURIComponent(slug)}`;

export const getDocsPath = ({
mdxPath,
platform,
}: {
mdxPath?: string[];
platform: "obsidian" | "roam";
}): string => {
const encodedPath = (mdxPath ?? []).map(encodeURIComponent).join("/");

return `/docs/${platform}${encodedPath ? `/${encodedPath}` : ""}`;
};