diff --git a/packages/core/src/core/pantheon-api.ts b/packages/core/src/core/pantheon-api.ts index 184b3f47..46bcb6ac 100644 --- a/packages/core/src/core/pantheon-api.ts +++ b/packages/core/src/core/pantheon-api.ts @@ -6,7 +6,13 @@ import { PCCConvenienceFunctions, } from "../helpers"; import { parseJwt } from "../lib/jwt"; -import { Article, MetadataGroup, Site, SmartComponentMap } from "../types"; +import { + Article, + MetadataGroup, + Site, + SmartComponentMap, + type PublishingLevel, +} from "../types"; import { PantheonClient, PantheonClientConfig } from "./pantheon-client"; export interface ApiRequest { @@ -111,7 +117,7 @@ const defaultOptions = { notFoundPath: "/404", } satisfies PantheonAPIOptions; -type AllowablePublishingLevels = "PRODUCTION" | "REALTIME" | undefined; +type AllowablePublishingLevels = keyof typeof PublishingLevel | undefined; export const PantheonAPI = (givenOptions?: PantheonAPIOptions) => { const options = { @@ -124,7 +130,7 @@ export const PantheonAPI = (givenOptions?: PantheonAPIOptions) => { await res.setHeader("Access-Control-Allow-Origin", "*"); const { command: commandInput, pccGrant, ...restOfQuery } = req.query; - const { publishingLevel } = restOfQuery; + const { publishingLevel, versionId } = restOfQuery; if (!commandInput) { return await res.redirect(302, options?.notFoundPath || "/404"); @@ -197,6 +203,7 @@ export const PantheonAPI = (givenOptions?: PantheonAPIOptions) => { publishingLevel: publishingLevel ?.toString() .toUpperCase() as AllowablePublishingLevels, + versionId: versionId?.toString(), }, ), client && !client.apiKey?.startsWith("pcc_grant") @@ -213,6 +220,7 @@ export const PantheonAPI = (givenOptions?: PantheonAPIOptions) => { const queryParams = { pccGrant: pccGrant?.toString(), publishingLevel: publishingLevel?.toString().toUpperCase(), + versionId: versionId?.toString(), }; return await res.redirect( diff --git a/packages/core/src/helpers/articles.ts b/packages/core/src/helpers/articles.ts index 13a17c75..feb3918d 100644 --- a/packages/core/src/helpers/articles.ts +++ b/packages/core/src/helpers/articles.ts @@ -32,6 +32,7 @@ export interface ArticleQueryArgs { sortOrder?: keyof typeof SortOrder; metadataFilters?: { [key: string]: unknown }; preamble?: string; + versionId?: string; } export interface ArticlePaginatedQueryArgs { diff --git a/packages/core/src/helpers/convenience.ts b/packages/core/src/helpers/convenience.ts index 8cbb7023..3e598aa3 100644 --- a/packages/core/src/helpers/convenience.ts +++ b/packages/core/src/helpers/convenience.ts @@ -113,14 +113,15 @@ async function getAllArticlesWithSummary( async function getArticleBySlugOrId( id: number | string, - publishingLevel: "PRODUCTION" | "REALTIME" = "PRODUCTION", + args?: Parameters[2], ) { const post = await _getArticleBySlugOrId( buildPantheonClient({ isClientSide: false }), id, { - publishingLevel, + publishingLevel: "PRODUCTION", contentType: "TREE_PANTHEON_V2", + ...args, }, ); diff --git a/packages/core/src/lib/gql.ts b/packages/core/src/lib/gql.ts index ded53425..0aece860 100644 --- a/packages/core/src/lib/gql.ts +++ b/packages/core/src/lib/gql.ts @@ -6,12 +6,14 @@ export const GET_ARTICLE_QUERY = gql` $slug: String $contentType: ContentType $publishingLevel: PublishingLevel + $versionId: String ) { article( id: $id slug: $slug contentType: $contentType publishingLevel: $publishingLevel + versionId: $versionId ) { id title @@ -34,11 +36,13 @@ export const ARTICLE_UPDATE_SUBSCRIPTION = gql` $id: String! $contentType: ContentType $publishingLevel: PublishingLevel + $versionId: String ) { article: articleUpdate( id: $id contentType: $contentType publishingLevel: $publishingLevel + versionId: $versionId ) { id title diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 4a272b6a..6a92f737 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -34,6 +34,7 @@ export type PaginatedArticle = { export enum PublishingLevel { PRODUCTION = "PRODUCTION", REALTIME = "REALTIME", + DRAFT = "DRAFT", } export enum ContentType { diff --git a/starters/nextjs-starter-approuter-ts/app/api/articles/tilenavigation/route.ts b/starters/nextjs-starter-approuter-ts/app/api/articles/tilenavigation/route.ts index bd1ba3f6..1e426b7f 100644 --- a/starters/nextjs-starter-approuter-ts/app/api/articles/tilenavigation/route.ts +++ b/starters/nextjs-starter-approuter-ts/app/api/articles/tilenavigation/route.ts @@ -53,10 +53,7 @@ export async function GET(request: NextRequest) { articles = await Promise.all( docIds.map(async (id) => { try { - return await PCCConvenienceFunctions.getArticleBySlugOrId( - id, - "PRODUCTION", - ); + return await PCCConvenienceFunctions.getArticleBySlugOrId(id); } catch (error) { const sanitizedId = id.replace(/\n|\r/g, ""); console.error( diff --git a/starters/nextjs-starter-approuter-ts/app/articles/[...uri]/article-view.tsx b/starters/nextjs-starter-approuter-ts/app/articles/[...uri]/article-view.tsx index c9f0de6f..44321bc4 100644 --- a/starters/nextjs-starter-approuter-ts/app/articles/[...uri]/article-view.tsx +++ b/starters/nextjs-starter-approuter-ts/app/articles/[...uri]/article-view.tsx @@ -1,19 +1,20 @@ -import { +import { + getArticlePathComponentsFromContentStructure, PCCConvenienceFunctions, - getArticlePathComponentsFromContentStructure - } from "@pantheon-systems/pcc-react-sdk/server"; + type PublishingLevel, +} 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 { pantheonAPIOptions } from "../../api/pantheoncloud/[...command]/api-options"; import { ClientsideArticleView } from "./clientside-articleview"; - export interface ArticleViewProps { params: { uri: string[] }; searchParams: { - publishingLevel: "PRODUCTION" | "REALTIME"; + publishingLevel: keyof typeof PublishingLevel; pccGrant: string | undefined; + versionId: string | undefined; }; } @@ -26,14 +27,22 @@ export const ArticleView = async ({ searchParams, }); - return ; + return ( + + ); }; interface GetServersideArticleProps { params: { uri: string[] }; searchParams: { - publishingLevel: "PRODUCTION" | "REALTIME"; + publishingLevel: keyof typeof PublishingLevel; pccGrant: string | undefined; + versionId: string | undefined; }; } @@ -42,18 +51,17 @@ export async function getServersideArticle({ searchParams, }: GetServersideArticleProps) { const { uri } = params; - const { publishingLevel, pccGrant, ...query } = searchParams; + const { publishingLevel, pccGrant, versionId, ...query } = searchParams; const slugOrId = uri[uri.length - 1]; const grant = pccGrant || cookies().get("PCC-GRANT")?.value || null; // Fetch the article and site in parallel const [article, site] = await Promise.all([ - PCCConvenienceFunctions.getArticleBySlugOrId( - slugOrId, - (publishingLevel?.toString().toUpperCase() as "PRODUCTION" | "REALTIME") || - "PRODUCTION", - ), + PCCConvenienceFunctions.getArticleBySlugOrId(slugOrId, { + publishingLevel, + versionId, + }), PCCConvenienceFunctions.getSite(), ]); @@ -61,7 +69,7 @@ export async function getServersideArticle({ return notFound(); } - // Get the article path from the content structure + // Get the article path from the content structure const articlePath = getArticlePathComponentsFromContentStructure( article, site, @@ -70,12 +78,12 @@ export async function getServersideArticle({ if ( // Check if the article has a slug ((article.slug?.trim().length && - // Check if the slug is not the same as the slugOrId - article.slug.toLowerCase() !== slugOrId?.trim().toLowerCase())|| - // Check if the article path is not the same as the uri - articlePath.length !== uri.length - 1 || - // Check if the article path (with all the components together) is not the same as the uri - articlePath.join("/") !== uri.slice(0, -1).join("/")) && + // Check if the slug is not the same as the slugOrId + article.slug.toLowerCase() !== slugOrId?.trim().toLowerCase()) || + // Check if the article path is not the same as the uri + articlePath.length !== uri.length - 1 || + // Check if the article path (with all the components together) is not the same as the uri + articlePath.join("/") !== uri.slice(0, -1).join("/")) && // Check if resolvePath in pantheon API options is not null pantheonAPIOptions.resolvePath != null ) { @@ -95,6 +103,8 @@ export async function getServersideArticle({ return { article, grant, + publishingLevel, + versionId, site, }; } diff --git a/starters/nextjs-starter-approuter-ts/app/articles/[...uri]/clientside-articleview.tsx b/starters/nextjs-starter-approuter-ts/app/articles/[...uri]/clientside-articleview.tsx index 301e7afd..dded4c69 100644 --- a/starters/nextjs-starter-approuter-ts/app/articles/[...uri]/clientside-articleview.tsx +++ b/starters/nextjs-starter-approuter-ts/app/articles/[...uri]/clientside-articleview.tsx @@ -5,6 +5,7 @@ import { PantheonProvider, PCCConvenienceFunctions, updateConfig, + type PublishingLevel, } from "@pantheon-systems/pcc-react-sdk"; import ArticleView from "../../../components/article-view"; @@ -17,9 +18,13 @@ updateConfig({ export const ClientsideArticleView = ({ article, grant, + publishingLevel, + versionId, }: { article: Article; grant?: string | undefined; + publishingLevel: keyof typeof PublishingLevel; + versionId: string | null; }) => { return ( - + ); }; diff --git a/starters/nextjs-starter-approuter-ts/app/examples/ssg-isr/[...uri]/page.tsx b/starters/nextjs-starter-approuter-ts/app/examples/ssg-isr/[...uri]/page.tsx index 9e2a80b6..5bb785cd 100644 --- a/starters/nextjs-starter-approuter-ts/app/examples/ssg-isr/[...uri]/page.tsx +++ b/starters/nextjs-starter-approuter-ts/app/examples/ssg-isr/[...uri]/page.tsx @@ -1,7 +1,7 @@ -import { +import { + getArticlePathComponentsFromContentStructure, PCCConvenienceFunctions, - getArticlePathComponentsFromContentStructure - } from "@pantheon-systems/pcc-react-sdk/server"; +} from "@pantheon-systems/pcc-react-sdk/server"; import { Metadata } from "next"; import { notFound } from "next/navigation"; import { StaticArticleView } from "../../../../components/article-view"; @@ -17,7 +17,6 @@ export const revalidate = 21600; // revalidate every 6 hours export default async function ArticlePage({ params }: ArticlePageProps) { const article = await PCCConvenienceFunctions.getArticleBySlugOrId( params.uri[params.uri.length - 1], - "PRODUCTION", ); if (!article) { @@ -38,7 +37,6 @@ export async function generateMetadata({ }: ArticlePageProps): Promise { const article = await PCCConvenienceFunctions.getArticleBySlugOrId( params.uri[params.uri.length - 1], - "PRODUCTION", ); return getSeoMetadata(article); @@ -65,16 +63,16 @@ export async function generateStaticParams() { site, ); - const id = article.id + const id = article.id; // Add the ID to the article path - articlePath.push(id) + articlePath.push(id); // Add a copy of the article path with the slug const params = [{ uri: articlePath.slice() }]; if (article.metadata?.slug) { // Change the ID in the article path to the slug - articlePath[articlePath.length - 1] = String(article.metadata.slug) + articlePath[articlePath.length - 1] = String(article.metadata.slug); params.push({ uri: articlePath }); } diff --git a/starters/nextjs-starter-approuter-ts/components/article-view.tsx b/starters/nextjs-starter-approuter-ts/components/article-view.tsx index b1aeb332..a1e6cd10 100644 --- a/starters/nextjs-starter-approuter-ts/components/article-view.tsx +++ b/starters/nextjs-starter-approuter-ts/components/article-view.tsx @@ -1,7 +1,7 @@ "use client"; import { useArticle } from "@pantheon-systems/pcc-react-sdk"; -import type { Article } from "@pantheon-systems/pcc-react-sdk"; +import type { Article, PublishingLevel } from "@pantheon-systems/pcc-react-sdk"; import { ArticleRenderer } from "@pantheon-systems/pcc-react-sdk/components"; import Image from "next/image"; import Link from "next/link"; @@ -52,6 +52,8 @@ const componentOverrideMap = { type ArticleViewProps = { article: Article; onlyContent?: boolean; + publishingLevel: keyof typeof PublishingLevel; + versionId: string | null; }; const ArticleHeader = ({ @@ -105,7 +107,10 @@ const ArticleHeader = ({ ); }; -export function StaticArticleView({ article, onlyContent }: ArticleViewProps) { +export function StaticArticleView({ + article, + onlyContent, +}: Pick) { const seoMetadata = getSeoMetadata(article); return ( @@ -144,15 +149,18 @@ export function StaticArticleView({ article, onlyContent }: ArticleViewProps) { export default function ArticleView({ article, onlyContent, + publishingLevel, + versionId, }: ArticleViewProps) { const { data } = useArticle( article.id, { - publishingLevel: article.publishingLevel, + publishingLevel, + versionId: versionId ?? undefined, contentType: "TREE_PANTHEON_V2", }, { - skip: article.publishingLevel !== "REALTIME", + skip: publishingLevel !== "REALTIME", }, ); diff --git a/starters/nextjs-starter-ts/components/article-view.tsx b/starters/nextjs-starter-ts/components/article-view.tsx index 480ec23d..65471081 100644 --- a/starters/nextjs-starter-ts/components/article-view.tsx +++ b/starters/nextjs-starter-ts/components/article-view.tsx @@ -1,5 +1,5 @@ import { useArticle } from "@pantheon-systems/pcc-react-sdk"; -import type { Article } from "@pantheon-systems/pcc-react-sdk"; +import type { Article, PublishingLevel } from "@pantheon-systems/pcc-react-sdk"; import { ArticleRenderer } from "@pantheon-systems/pcc-react-sdk/components"; import type { Metadata } from "next"; import Image from "next/image"; @@ -36,6 +36,8 @@ const overrideElementStyles = (tag: keyof HTMLElementTagNameMap) => { type ArticleViewProps = { article: Article; onlyContent?: boolean; + publishingLevel: keyof typeof PublishingLevel; + versionId: string | null; }; const ArticleHeader = ({ @@ -92,15 +94,18 @@ const ArticleHeader = ({ export default function ArticleView({ article, onlyContent, + publishingLevel, + versionId, }: ArticleViewProps) { const { data } = useArticle( article.id, { - publishingLevel: article.publishingLevel, + publishingLevel, + versionId: versionId ?? undefined, contentType: "TREE_PANTHEON_V2", }, { - skip: article.publishingLevel !== "REALTIME", + skip: publishingLevel !== "REALTIME", }, ); @@ -114,7 +119,10 @@ export default function ArticleView({ ); } -export function StaticArticleView({ article, onlyContent }: ArticleViewProps) { +export function StaticArticleView({ + article, + onlyContent, +}: Pick) { const seoMetadata = getSeoMetadata(article); return ( diff --git a/starters/nextjs-starter-ts/pages/api/articles/tilenavigation.ts b/starters/nextjs-starter-ts/pages/api/articles/tilenavigation.ts index ab06b65e..ec5a0ba4 100644 --- a/starters/nextjs-starter-ts/pages/api/articles/tilenavigation.ts +++ b/starters/nextjs-starter-ts/pages/api/articles/tilenavigation.ts @@ -60,10 +60,7 @@ export default async function handler( articles = await Promise.all( docIds.map(async (id) => { try { - return await PCCConvenienceFunctions.getArticleBySlugOrId( - id, - "PRODUCTION", - ); + return await PCCConvenienceFunctions.getArticleBySlugOrId(id); } catch (error) { const sanitizedId = id.replace(/\n|\r/g, ""); console.error( diff --git a/starters/nextjs-starter-ts/pages/articles/[...uri].tsx b/starters/nextjs-starter-ts/pages/articles/[...uri].tsx index b8258b41..e538dcba 100644 --- a/starters/nextjs-starter-ts/pages/articles/[...uri].tsx +++ b/starters/nextjs-starter-ts/pages/articles/[...uri].tsx @@ -2,6 +2,7 @@ import { PantheonProvider, PCCConvenienceFunctions, type Article, + type PublishingLevel, } from "@pantheon-systems/pcc-react-sdk"; import { getArticlePathComponentsFromContentStructure } from "@pantheon-systems/pcc-react-sdk/server"; import { NextSeo } from "next-seo"; @@ -14,9 +15,16 @@ import { pantheonAPIOptions } from "../api/pantheoncloud/[...command]"; interface ArticlePageProps { article: Article; grant: string; + publishingLevel: keyof typeof PublishingLevel; + versionId: string | null; } -export default function ArticlePage({ article, grant }: ArticlePageProps) { +export default function ArticlePage({ + article, + grant, + publishingLevel, + versionId, +}: ArticlePageProps) { const seoMetadata = getSeoMetadata(article); return ( @@ -34,7 +42,11 @@ export default function ArticlePage({ article, grant }: ArticlePageProps) { />
- +
@@ -43,15 +55,16 @@ export default function ArticlePage({ article, grant }: ArticlePageProps) { export async function getServerSideProps({ req: { cookies }, - query: { uri, publishingLevel, pccGrant, ...query }, + query: { uri, publishingLevel, pccGrant, versionId, ...query }, }: { req: { cookies: Record; }; query: { uri: string[]; - publishingLevel: "PRODUCTION" | "REALTIME" | undefined; + publishingLevel: keyof typeof PublishingLevel | undefined; pccGrant: string; + versionId: string | undefined; }; }) { const slugOrId = uri[uri.length - 1]; @@ -59,14 +72,10 @@ export async function getServerSideProps({ // Fetch the article and the site in parallel const [article, site] = await Promise.all([ - PCCConvenienceFunctions.getArticleBySlugOrId( - slugOrId, - publishingLevel - ? (publishingLevel.toString().toUpperCase() as - | "PRODUCTION" - | "REALTIME") - : "PRODUCTION", - ), + PCCConvenienceFunctions.getArticleBySlugOrId(slugOrId, { + publishingLevel, + versionId, + }), PCCConvenienceFunctions.getSite(), ]); @@ -114,6 +123,8 @@ export async function getServerSideProps({ props: { article, grant, + publishingLevel, + versionId: versionId || null, recommendedArticles: await PCCConvenienceFunctions.getRecommendedArticles( article.id, ), diff --git a/starters/nextjs-starter/components/article-view.jsx b/starters/nextjs-starter/components/article-view.jsx index 3fe5af93..a77b0b7b 100644 --- a/starters/nextjs-starter/components/article-view.jsx +++ b/starters/nextjs-starter/components/article-view.jsx @@ -122,15 +122,21 @@ export function StaticArticleView({ article, onlyContent }) { ); } -export default function ArticleView({ article, onlyContent }) { +export default function ArticleView({ + article, + onlyContent, + publishingLevel, + versionId, +}) { const { data } = useArticle( article.id, { - publishingLevel: article.publishingLevel, + publishingLevel, + versionId: versionId ?? undefined, contentType: "TREE_PANTHEON_V2", }, { - skip: article.publishingLevel !== "REALTIME", + skip: publishingLevel !== "REALTIME", }, ); diff --git a/starters/nextjs-starter/pages/api/articles/tilenavigation.js b/starters/nextjs-starter/pages/api/articles/tilenavigation.js index d38e57bc..fe428113 100644 --- a/starters/nextjs-starter/pages/api/articles/tilenavigation.js +++ b/starters/nextjs-starter/pages/api/articles/tilenavigation.js @@ -53,10 +53,7 @@ export default async function handler(req, res) { articles = await Promise.all( docIds.map(async (id) => { try { - return await PCCConvenienceFunctions.getArticleBySlugOrId( - id, - "PRODUCTION", - ); + return await PCCConvenienceFunctions.getArticleBySlugOrId(id); } catch (error) { const sanitizedId = id.replace(/\n|\r/g, ""); console.error( diff --git a/starters/nextjs-starter/pages/articles/[...uri].jsx b/starters/nextjs-starter/pages/articles/[...uri].jsx index 19292953..fd998c69 100644 --- a/starters/nextjs-starter/pages/articles/[...uri].jsx +++ b/starters/nextjs-starter/pages/articles/[...uri].jsx @@ -2,15 +2,20 @@ import { PantheonProvider, PCCConvenienceFunctions, } from "@pantheon-systems/pcc-react-sdk"; +import { getArticlePathComponentsFromContentStructure } from "@pantheon-systems/pcc-react-sdk/server"; import { NextSeo } from "next-seo"; import queryString from "query-string"; import ArticleView from "../../components/article-view"; import Layout from "../../components/layout"; import { getSeoMetadata } from "../../lib/utils"; import { pantheonAPIOptions } from "../api/pantheoncloud/[...command]"; -import { getArticlePathComponentsFromContentStructure } from "@pantheon-systems/pcc-react-sdk/server"; -export default function ArticlePage({ article, grant }) { +export default function ArticlePage({ + article, + grant, + publishingLevel, + versionId, +}) { const seoMetadata = getSeoMetadata(article); return ( @@ -28,7 +33,11 @@ export default function ArticlePage({ article, grant }) { />
- +
@@ -37,17 +46,17 @@ export default function ArticlePage({ article, grant }) { export async function getServerSideProps({ req: { cookies }, - query: { uri, publishingLevel, pccGrant, ...query }, + query: { uri, publishingLevel, pccGrant, versionId, ...query }, }) { const slugOrId = uri[uri.length - 1]; const grant = pccGrant || cookies["PCC-GRANT"] || null; // Fetch the article and site in parallel const [article, site] = await Promise.all([ - PCCConvenienceFunctions.getArticleBySlugOrId( - slugOrId, - publishingLevel ? publishingLevel.toString().toUpperCase() : "PRODUCTION", - ), + PCCConvenienceFunctions.getArticleBySlugOrId(slugOrId, { + publishingLevel, + versionId, + }), PCCConvenienceFunctions.getSite(), ]); @@ -67,16 +76,16 @@ export async function getServerSideProps({ if ( // Check if the article has a slug ((article.slug?.trim().length && - // Check if the slug is not the same as the slugOrId - article.slug.toLowerCase() !== slugOrId?.trim().toLowerCase()) || - // Check if the article path is not the same as the uri - articlePath.length !== (uri.length - 1) || - // Check if the article (with all the components together) path is not the same as the uri - articlePath.join("/") !== uri.slice(0, -1).join("/")) && + // Check if the slug is not the same as the slugOrId + article.slug.toLowerCase() !== slugOrId?.trim().toLowerCase()) || + // Check if the article path is not the same as the uri + articlePath.length !== uri.length - 1 || + // Check if the article (with all the components together) path is not the same as the uri + articlePath.join("/") !== uri.slice(0, -1).join("/")) && // Check if resolvePath in pantheon API options is not null pantheonAPIOptions.resolvePath != null ) { - // If the article was accessed by the id rather than the slug + // If the article was accessed by the id rather than the slug // or the article path is not the same as the uri - then redirect to the canonical // link (mostly for SEO purposes than anything else). return { @@ -94,6 +103,8 @@ export async function getServerSideProps({ props: { article, grant, + publishingLevel, + versionId: versionId || null, recommendedArticles: await PCCConvenienceFunctions.getRecommendedArticles( article.id, ),