diff --git a/packages/commons/fdr-utils/src/getUnversionedSlug.ts b/packages/commons/fdr-utils/src/getUnversionedSlug.ts deleted file mode 100644 index e97b9aa28a..0000000000 --- a/packages/commons/fdr-utils/src/getUnversionedSlug.ts +++ /dev/null @@ -1,23 +0,0 @@ -export function getUnversionedSlug( - slug: string, - currentVersionSlug: string | undefined, - basePathSlug: string | undefined, -): string { - if ( - currentVersionSlug != null && - slug.startsWith(currentVersionSlug) && - (slug.length === currentVersionSlug.length || slug[currentVersionSlug.length] === "/") - ) { - return slug.slice(currentVersionSlug.length); - } - - if ( - basePathSlug != null && - slug.startsWith(basePathSlug) && - (slug.length === basePathSlug.length || slug[basePathSlug.length] === "/") - ) { - return slug.slice(basePathSlug.length); - } - - return slug; -} diff --git a/packages/fdr-sdk/src/navigation/NodeCollector.ts b/packages/fdr-sdk/src/navigation/NodeCollector.ts index 4ad4be368e..e3c02a77b0 100644 --- a/packages/fdr-sdk/src/navigation/NodeCollector.ts +++ b/packages/fdr-sdk/src/navigation/NodeCollector.ts @@ -64,6 +64,7 @@ export class NodeCollector { traverseNavigation(rootNode, (node, _index, parents) => { this.idToNode.set(node.id, node); + // if the node is the default version, make a copy of it and "prune" the version slug from all children nodes const parent = parents[parents.length - 1]; if ( node.type === "version" && diff --git a/packages/fdr-sdk/src/navigation/__test__/getUnversionedSlug.test.ts b/packages/fdr-sdk/src/navigation/__test__/getUnversionedSlug.test.ts deleted file mode 100644 index c792d975db..0000000000 --- a/packages/fdr-sdk/src/navigation/__test__/getUnversionedSlug.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import urljoin from "url-join"; -import { getUnversionedSlug } from "../utils/getUnversionedSlug"; - -describe("getUnversionedSlug", () => { - it("should return the slug without the current version", () => { - expect(getUnversionedSlug(urljoin(["docs", "v1", "foo"]), "v1", "docs")).toEqual(urljoin(["v1", "foo"])); - - expect(getUnversionedSlug(urljoin(["docs", "v1", "foo", "bar"]), urljoin(["docs", "v1"]), "docs")).toEqual( - urljoin(["foo", "bar"]), - ); - - expect(getUnversionedSlug(urljoin(["docs", "v1", "foo"]), "v1", "docs")).toEqual(urljoin(["v1", "foo"])); - - expect(getUnversionedSlug(urljoin(["docs", "v1", "foo", "bar"]), urljoin(["docs", "v1"]), "docs")).toEqual( - urljoin(["foo", "bar"]), - ); - - expect( - getUnversionedSlug(urljoin(["docs", "v1", "foo", "bar"]), urljoin(["docs", "v1", "foo"]), "docs"), - ).toEqual("bar"); - - expect( - getUnversionedSlug(urljoin(["docs", "v1", "foo", "bar"]), urljoin(["docs", "v2", "foo"]), "docs"), - ).toEqual(urljoin(["v1", "foo", "bar"])); - - expect(getUnversionedSlug(urljoin(["docs", "v1", "foo", "bar"]), "", "")).toEqual( - urljoin(["docs", "v1", "foo", "bar"]), - ); - - expect(getUnversionedSlug(urljoin(["docs", "v1", "foo", "bar"]), "", "docs")).toEqual( - urljoin(["v1", "foo", "bar"]), - ); - }); -}); diff --git a/packages/fdr-sdk/src/navigation/utils/__test__/pruneVersionNode.test.ts b/packages/fdr-sdk/src/navigation/utils/__test__/pruneVersionNode.test.ts new file mode 100644 index 0000000000..9f26a54a59 --- /dev/null +++ b/packages/fdr-sdk/src/navigation/utils/__test__/pruneVersionNode.test.ts @@ -0,0 +1,24 @@ +import { FernNavigation } from "../../generated"; +import { toDefaultSlug } from "../pruneVersionNode"; + +describe("toDefaultSlug", () => { + it("should return the default slug", () => { + expect( + toDefaultSlug( + FernNavigation.Slug("basepath/version/page"), + FernNavigation.Slug("basepath"), + FernNavigation.Slug("basepath/version"), + ), + ).toEqual(FernNavigation.Slug("basepath/page")); + }); + + it("should be noop if the slug doesn't start with the version slug", () => { + expect( + toDefaultSlug( + FernNavigation.Slug("basepath/page"), + FernNavigation.Slug("basepath"), + FernNavigation.Slug("basepath/version"), + ), + ).toEqual(FernNavigation.Slug("basepath/page")); + }); +}); diff --git a/packages/fdr-sdk/src/navigation/utils/getUnversionedSlug.ts b/packages/fdr-sdk/src/navigation/utils/getUnversionedSlug.ts deleted file mode 100644 index 19854e1070..0000000000 --- a/packages/fdr-sdk/src/navigation/utils/getUnversionedSlug.ts +++ /dev/null @@ -1,23 +0,0 @@ -export function getUnversionedSlug( - slug: string, - currentVersionSlug: string | undefined, - basePathSlug: string | undefined, -): string { - if ( - currentVersionSlug != null && - slug.startsWith(currentVersionSlug) && - (slug.length === currentVersionSlug.length || slug[currentVersionSlug.length] === "/") - ) { - return slug.split("/").slice(currentVersionSlug.split("/").length).join("/"); - } - - if ( - basePathSlug != null && - slug.startsWith(basePathSlug) && - (slug.length === basePathSlug.length || slug[basePathSlug.length] === "/") - ) { - return slug.split("/").slice(basePathSlug.split("/").length).join("/"); - } - - return slug; -} diff --git a/packages/fdr-sdk/src/navigation/utils/index.ts b/packages/fdr-sdk/src/navigation/utils/index.ts index e74f6a5822..d2bbee9c9c 100644 --- a/packages/fdr-sdk/src/navigation/utils/index.ts +++ b/packages/fdr-sdk/src/navigation/utils/index.ts @@ -7,6 +7,6 @@ export * from "./followRedirect"; export * from "./getApiReferenceId"; export * from "./getNoIndexFromFrontmatter"; export * from "./getPageId"; -export * from "./getUnversionedSlug"; +export { toDefaultSlug } from "./pruneVersionNode"; export * from "./slugjoin"; export * from "./traverseNavigation"; diff --git a/packages/fdr-sdk/src/navigation/utils/pruneVersionNode.ts b/packages/fdr-sdk/src/navigation/utils/pruneVersionNode.ts index 501ae4a0ff..b6e5a47c37 100644 --- a/packages/fdr-sdk/src/navigation/utils/pruneVersionNode.ts +++ b/packages/fdr-sdk/src/navigation/utils/pruneVersionNode.ts @@ -39,6 +39,11 @@ export function pruneVersionNode( return node; } +/** + * All versioned slugs are prefixed with the version slug, but the default version may be accessed without the version prefix. + * toDefaultSlug creates the "unversioned" slug for pages under the default version. + * This should be treated as the cannonical slug for that page. + */ export function toDefaultSlug( slug: FernNavigation.Slug, rootSlug: FernNavigation.Slug, diff --git a/packages/ui/app/src/atoms/navigation.ts b/packages/ui/app/src/atoms/navigation.ts index d9cbd14c56..7893332fc3 100644 --- a/packages/ui/app/src/atoms/navigation.ts +++ b/packages/ui/app/src/atoms/navigation.ts @@ -48,8 +48,14 @@ CURRENT_VERSION_ATOM.debugLabel = "CURRENT_VERSION_ATOM"; export const UNVERSIONED_SLUG_ATOM = atom((get) => { const slug = get(SLUG_ATOM); const currentVersion = get(CURRENT_VERSION_ATOM); - const basePath = get(BASEPATH_ATOM); - return FernNavigation.utils.getUnversionedSlug(slug, currentVersion?.slug, basePath); + if (currentVersion == null) { + return slug; + } + + // the root slug is the basepath without the leading slash (defaults to ""): + const rootSlug = FernNavigation.utils.slugjoin(get(BASEPATH_ATOM) ?? ""); + + return FernNavigation.utils.toDefaultSlug(slug, rootSlug, currentVersion.slug); }); UNVERSIONED_SLUG_ATOM.debugLabel = "UNVERSIONED_SLUG_ATOM"; diff --git a/packages/ui/app/src/next-app/utils/__test__/getSeoProps.ts b/packages/ui/app/src/next-app/utils/__test__/getSeoProps.ts index 4308545c74..7aafdb5890 100644 --- a/packages/ui/app/src/next-app/utils/__test__/getSeoProps.ts +++ b/packages/ui/app/src/next-app/utils/__test__/getSeoProps.ts @@ -21,6 +21,8 @@ describe("getSeoProps", () => { noindex: undefined, }, parents: [], + currentVersion: undefined, + root: { slug: FernNavigation.Slug("") } as FernNavigation.RootNode, }, true, ); diff --git a/packages/ui/app/src/next-app/utils/getSeoProp.ts b/packages/ui/app/src/next-app/utils/getSeoProp.ts index 36b179452e..cbb60a1a46 100644 --- a/packages/ui/app/src/next-app/utils/getSeoProp.ts +++ b/packages/ui/app/src/next-app/utils/getSeoProp.ts @@ -23,7 +23,12 @@ export function getSeoProps( pages: Record, files: Record, apis: Record, - { node, parents }: Pick, + { + root, + node, + parents, + currentVersion, + }: Pick, isSeoDisabled: boolean, ): NextSeoProps { const additionalMetaTags: MetaTag[] = []; @@ -38,6 +43,13 @@ export function getSeoProps( breadcrumbList: getBreadcrumbList(domain, pages, parents, node), }; + // if the current version is the default version, the page is duplicated (/v1/page and /page). + // the canonical link should point to `/page`. + if (currentVersion != null && currentVersion.default) { + const canonicalSlug = FernNavigation.utils.toDefaultSlug(node.slug, root.slug, currentVersion.slug); + seo.canonical = `https://${domain}/${canonicalSlug}`; + } + const pageId = FernNavigation.utils.getPageId(node); let ogMetadata: DocsV1Read.MetadataConfig = metadata ?? {};