Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pcc 1752 article url from content structure #332

Merged
merged 38 commits into from
Feb 7, 2025
Merged
Changes from 1 commit
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3461eb8
Renjith | Add content structure to article path
renjithvwarrier Jan 15, 2025
ebb868b
Renjith
renjithvwarrier Jan 15, 2025
d25c69e
Renjith | Fix tests and add another test to test the ability to map c…
renjithvwarrier Jan 15, 2025
6d753f7
Renjith
renjithvwarrier Jan 17, 2025
728fa37
Renjith | Add content strucutre path to nextjs-starter-approuter-ts s…
renjithvwarrier Jan 20, 2025
b776b42
Renjith | Update comments on articles url
renjithvwarrier Jan 20, 2025
f36000b
Renjith | Update tests in nextjs-starter-ts
renjithvwarrier Jan 20, 2025
ce7ae8d
Refactor site fetching in Next.js starter to remove environment varia…
renjithvwarrier Jan 20, 2025
54c4120
Renjith | Simplify return statement
renjithvwarrier Jan 20, 2025
96d9989
Renjith | Reformat utils and command.js
renjithvwarrier Jan 20, 2025
9dced14
Renjith | Export getPanthonAPIOptions using a seperate statement
renjithvwarrier Jan 20, 2025
b39c49c
Renjith | Disable codacy unnecessary blocks
renjithvwarrier Jan 20, 2025
ad577a2
Renjith | Add the codacy disable line before return
renjithvwarrier Jan 20, 2025
29ab0a1
Renjith | Add the codacy disable line before return
renjithvwarrier Jan 20, 2025
41ca572
Renjith | Add the codacy disable line before return
renjithvwarrier Jan 20, 2025
8d74ecf
Merge branch 'main' into PCC-1752-article-url-from-contentStructure
renjithvwarrier Jan 20, 2025
f2a6005
Refactor API options in Pantheon Cloud handler to remove unused type …
renjithvwarrier Jan 20, 2025
96b2b7d
Renjith | Update pnpm-lock.yal
renjithvwarrier Jan 20, 2025
e9e4c5d
Renjith | Override next to 14.2.23
renjithvwarrier Jan 20, 2025
34c6d99
Renjith | Unify babael core
renjithvwarrier Jan 20, 2025
54cd966
Renjith | Update pnpm-lock.yaml
renjithvwarrier Jan 20, 2025
d087a9c
Renjith | Update path-to-regexp
renjithvwarrier Jan 20, 2025
296165c
Renjith | Change revalidate to revalidateSeconds as per PR suggestion
renjithvwarrier Jan 21, 2025
748088c
Fix function name typo from 'doesCHildContainArticle' to 'doesChildCo…
renjithvwarrier Jan 21, 2025
fec742e
Renjith | Update the core-sdk to have the function to get article URLs
renjithvwarrier Jan 21, 2025
2c58906
Renjith | Expose the new article URL functionality functions from cor…
renjithvwarrier Jan 21, 2025
23d2759
Renjith | Change the nextjs-starter-approuter to use the new article …
renjithvwarrier Jan 21, 2025
a649bd1
Renjith | Update nextjs-starter-ts to use the new content structure u…
renjithvwarrier Jan 21, 2025
2dcda26
Renjith | Move nextjs-starter to the updated sdk utility functions
renjithvwarrier Jan 21, 2025
fd57e45
Renjith | Change revalidateSeconds to revalidate
renjithvwarrier Jan 21, 2025
a407141
Renjith | Changeset
renjithvwarrier Jan 21, 2025
767028c
Renjith | Add Changeset
renjithvwarrier Jan 21, 2025
b982134
Renjith | Force running Wix
renjithvwarrier Jan 28, 2025
8430e4e
Renjith | Force running Wix
renjithvwarrier Jan 28, 2025
b9d140c
Renjith | Fix typo in getArticlePathComponentsFromContentStructure fu…
renjithvwarrier Feb 6, 2025
44c2d59
Renjith | Improve article path and slug validation in starters
renjithvwarrier Feb 6, 2025
b746436
Renjith | Update comments for article URL generation with maxDepth de…
renjithvwarrier Feb 7, 2025
f67c167
Merge branch 'main' into PCC-1752-article-url-from-contentStructure
kevinstubbs Feb 7, 2025
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
Prev Previous commit
Next Next commit
Renjith | Change the nextjs-starter-approuter to use the new article …
…url functions from the sdk
renjithvwarrier committed Jan 21, 2025
commit 23d27598bc6c17dc659a149ee54cc410c6c901b7
Original file line number Diff line number Diff line change
@@ -1,65 +1,40 @@
import { Site } from "@pantheon-systems/pcc-react-sdk/*";
import { getPantheonAPIOptions } from "../app/api/pantheoncloud/[...command]/api-options";
import { pantheonAPIOptions } from "../app/api/pantheoncloud/[...command]/api-options";
import { Site } from "@pantheon-systems/pcc-react-sdk/server";

describe("hasConfiguredPantheonClient", () => {
// Define a dummy site
const site: Site = {
id: "123",
name: "test",
url: "https://test.com",
domain: "test.com",
tags: [],
metadataFields: {},
contentStructure: {
active: [],
},
};

it("Pantheon API options have been filled out", () => {
expect(
getPantheonAPIOptions(site).smartComponentMap.MEDIA_PREVIEW,
).toBeDefined();
expect(pantheonAPIOptions.smartComponentMap?.MEDIA_PREVIEW).toBeDefined();
});

it("Resolve document by article id", () => {
expect(getPantheonAPIOptions(site).resolvePath({ id: "123" })).toBe(
"/articles/123",
);
const site: Site = {
id: "123",
name: "test",
url: "https://test.com",
domain: "test.com",
tags: [],
metadataFields: {},
contentStructure: {
active: [],
},
};
expect(pantheonAPIOptions.resolvePath!({ id: "123" }, site)).toBe("/articles/123");
});

it("Resolve document by article slug", () => {
expect(
getPantheonAPIOptions(site).resolvePath({
id: "123",
slug: "foo-bar-slug",
}),
).toBe("/articles/foo-bar-slug");
});

it("Resolve document to the right content structure path", () => {
// Add content structure to the site
site.contentStructure = {
active: [
{
id: "category-1",
name: "foo",
type: "category",
children: [
{
id: "nested-category-1",
name: "bar",
type: "category",
children: [{ id: "123", name: "baz", type: "article" }],
},
],
},
],
const site: Site = {
id: "123",
name: "test",
url: "https://test.com",
domain: "test.com",
tags: [],
metadataFields: {},
contentStructure: {
active: [],
},
};
expect(
getPantheonAPIOptions(site).resolvePath({
id: "123",
slug: "foo-bar-slug",
}),
).toBe("/articles/foo/bar/foo-bar-slug");
pantheonAPIOptions.resolvePath!({ id: "123", slug: "foo-bar-slug" }, site),
).toBe("/articles/foo-bar-slug");
});
});
});
Original file line number Diff line number Diff line change
@@ -1,41 +1,36 @@
import { Article, PantheonAPI, PCCConvenienceFunctions, Site } from "@pantheon-systems/pcc-react-sdk/server";
import {
PantheonAPIOptions,
getArticleURLFromSiteWithOptions,
} from "@pantheon-systems/pcc-react-sdk/server";
import { serverSmartComponentMap } from "../../../../components/smart-components/server-components";
import {
getAuthorById,
listAuthors,
} from "../../../../lib/pcc-metadata-groups";
import { getArticleURLFromSite } from "../../../../lib/utils";
import { NextRequest } from "next/server";

export function getPantheonAPIOptions(site: Site) {
return {
resolvePath: (article: Partial<Article> & Pick<Article, "id">) =>
getArticleURLFromSite(article, site),
smartComponentMap: serverSmartComponentMap,
componentPreviewPath: (componentName: string) =>
`/component-preview/${componentName}`,
metadataGroups: [
{
label: "Author",
groupIdentifier: "AUTHOR",
schema: {
name: "string" as const,
image: "file" as const,
},
get: getAuthorById,
list: listAuthors,
},
],
};
}

export default async function apiHandler(req: NextRequest, res: any) {
// Get the site
const site = await PCCConvenienceFunctions.getSite();
// Create the options for the PantheonAPI
const options = getPantheonAPIOptions(site);
// Create the handler for the PantheonAPI
const handler = PantheonAPI(options);
// Handle the request
return handler(req, res);
}

export const pantheonAPIOptions: PantheonAPIOptions = {
resolvePath: getArticleURLFromSiteWithOptions({
// The base path to use for the URL.
basePath: "/articles",
// The maximum depth to include in the URL. We need it to include everything
a11rew marked this conversation as resolved.
Show resolved Hide resolved
maxDepth: -1,
}),
getSiteId: () => process.env.PCC_SITE_ID as string,
smartComponentMap: serverSmartComponentMap,
componentPreviewPath: (componentName) =>
`/component-preview/${componentName}`,
metadataGroups: [
{
label: "Author",
groupIdentifier: "AUTHOR",
schema: {
name: "string",
image: "file",
},
get: getAuthorById,
list: listAuthors,
},
],
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import apiHandler from "./api-options";
import { PantheonAPI } from "@pantheon-systems/pcc-react-sdk/server";
import { pantheonAPIOptions } from "./api-options";

export { apiHandler as GET, apiHandler as POST };
const handler = PantheonAPI(pantheonAPIOptions);
export { handler as GET, handler as POST };
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { PCCConvenienceFunctions, Site } from "@pantheon-systems/pcc-react-sdk/server";
import {
PCCConvenienceFunctions,
getArticlePathComponentsFromContentStrucuture
} from "@pantheon-systems/pcc-react-sdk/server";
import { cookies } from "next/headers";
import { notFound, redirect, RedirectType } from "next/navigation";
import queryString from "query-string";
import { getPantheonAPIOptions } from "../../api/pantheoncloud/[...command]/api-options";
import { pantheonAPIOptions } from "../../api/pantheoncloud/[...command]/api-options";
import { ClientsideArticleView } from "./clientside-articleview";
import {
getArticlePathFromContentStrucuture,
getSeoMetadata,
} from "../../../lib/utils";


export interface ArticleViewProps {
@@ -63,10 +62,10 @@ export async function getServersideArticle({
}

// Get the article path from the content structure
const articlePath = getArticlePathFromContentStrucuture(article, site);

// Get the pantheonAPIOptions
const pantheonAPIOptions = getPantheonAPIOptions(site);
const articlePath = getArticlePathComponentsFromContentStrucuture(
article,
site,
);

if (
((article.slug?.trim().length &&
@@ -81,7 +80,7 @@ export async function getServersideArticle({
// then redirect to the canonical link.
redirect(
queryString.stringifyUrl({
url: pantheonAPIOptions.resolvePath(article),
url: pantheonAPIOptions.resolvePath(article, site),
query: { publishingLevel, ...query },
}),
RedirectType.replace,
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { getArticleBySlugOrId, PCCConvenienceFunctions } from "@pantheon-systems/pcc-react-sdk/server";
import {
PCCConvenienceFunctions,
getArticlePathComponentsFromContentStrucuture
} from "@pantheon-systems/pcc-react-sdk/server";
import { Metadata } from "next";
import { notFound } from "next/navigation";
import { StaticArticleView } from "../../../../components/article-view";
import Layout from "../../../../components/layout";
import { getArticlePathFromContentStrucuture, getSeoMetadata } from "../../../../lib/utils";
import { getSeoMetadata } from "../../../../lib/utils";

interface ArticlePageProps {
params: { uri: string[] };
@@ -59,7 +62,10 @@ export async function generateStaticParams() {

return publishedArticles.flatMap((article) => {
// Generate the article path from the contnet structure
const articlePath = getArticlePathFromContentStrucuture(article, site)
const articlePath = getArticlePathComponentsFromContentStrucuture(
article,
site,
);

const id = article.id

8 changes: 6 additions & 2 deletions starters/nextjs-starter-approuter-ts/components/grid.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { ArticleWithoutContent, Site } from "@pantheon-systems/pcc-react-sdk";
import type {
ArticleWithoutContent,
Site,
} from "@pantheon-systems/pcc-react-sdk";
import { getArticleURLFromSite } from "@pantheon-systems/pcc-react-sdk/server";
import Link from "next/link";
import { cn, getArticleURLFromSite } from "../lib/utils";
import { cn } from "../lib/utils";
import { Button } from "./ui/button";

export function HomepageArticleGrid({
117 changes: 1 addition & 116 deletions starters/nextjs-starter-approuter-ts/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Article, ArticleWithoutContent, Site } from "@pantheon-systems/pcc-react-sdk";
import { Article, ArticleWithoutContent } from "@pantheon-systems/pcc-react-sdk";
import { clsx, type ClassValue } from "clsx";
import { Metadata } from "next";
import { twMerge } from "tailwind-merge";
@@ -80,118 +80,3 @@ export function getSeoMetadata(
},
};
}

function doesChildContainArticle(
child: any,
article: Partial<Article> & Pick<Article, "id">,
) {
let categoryTree: string[] = [];
let contains = false;

// If the child is an article, check if it matches the article id
if (child.type === "article") {
if (child.id === article.id) {
contains = true;
}

return { contains, categoryTree };
}

// Iterate over the category and its children
for (const childOfChild of child.children) {
// If the child is another category, we need to iterate over its children
if (childOfChild.type === "category") {
const result = doesChildContainArticle(childOfChild, article);
if (result.contains) {
// Add the current category name to the category tree
categoryTree.push(child.name);
// Append the result of the recursive call
categoryTree.push(...result.categoryTree);
contains = true;
// Break out of the loop
break;
}
} else {
// If the child is an article, check if it matches the article id
if (childOfChild.id === article.id) {
contains = true;
// If it does, append the result's category tree to the current category tree
categoryTree.push(child.name);
// Break out of the loop
break;
}
}
}

return { contains, categoryTree };
}

export function getArticlePathFromContentStrucuture(
article: Partial<Article> & Pick<Article, "id">,
site: Site,
) {
const defaultPath: string[] = [];
// If the site is not defined or the content structure is not defined or if the active key is not defined, return the default path
if (!site || !site.contentStructure || !site.contentStructure.active) {
return defaultPath;
}
// If the active key is present, it will be an array of objects. Its structure is as follows:
// {
// "id": "string",
// "name": "string",
// "type": "string"
// "children": [
// {
// "id": "string",
// "name": "string",
// "type": "string"
// }
// ]
// }
// type will be one of the following: "category" or "article"
// We need to find the article object that contains the articleId
const active = site.contentStructure.active;
if (typeof active !== "object" || !Array.isArray(active) || !active.length) {
return defaultPath;
}
// Iterate over the active array
for (const category of active) {
// The categories can be nested, so we need to find the relevant list of categories that contain the articleId
// We need to iterate over all the categories, do the same for all its children. Keep doing this until we find the articleId
const { contains, categoryTree } = doesChildContainArticle(
category,
article,
);
if (!contains) {
continue;
}
// If the item is found, return the path as a normalized path
if (categoryTree && categoryTree.length > 0) {
// normalise the name of each category in the categoryTree
const normalisedCategoryTree = categoryTree.map((category) =>
category
.replace(/ /g, "-")
.replace(/[^a-zA-Z0-9-]/g, "")
.toLowerCase(),
);
// Return it
return normalisedCategoryTree;
}
}

return defaultPath;
}

export function getArticleURLFromSite(
article: Partial<Article> & Pick<Article, "id">,
site: Site,
basePath = "/articles",
) {
// Get the article path
const articlePath = getArticlePathFromContentStrucuture(article, site);
// Add the basePath before the articlePath and the article slug or id at the end
if (articlePath.length > 0) {
return `${basePath}/${articlePath.join("/")}/${article.slug || article.id}`;
}
return `${basePath}/${article.slug || article.id}`;
}