From f1d9e449b38535c4cd671a4dd5effead87e7aa0f Mon Sep 17 00:00:00 2001
From: Johannes Obermair <48853629+johnnyomair@users.noreply.github.com>
Date: Wed, 18 Sep 2024 08:04:14 +0200
Subject: [PATCH] App Router: Support user-defined paths for predefined pages
(#2293)
This is achieved by using a combination of rewrites and redirects in the
middleware:
- Rewrite from the page tree node path (e.g., `/aktuelles`) to the code
path (e.g., `/news`)
- Redirect from the code path (e.g., `/news`) to the page tree node path
(e.g., `/aktuelles`)
---
.changeset/dirty-clocks-wonder.md | 17 ++++
demo/api/schema.gql | 2 +-
demo/site/src/header/PageLink.tsx | 10 +--
demo/site/src/middleware.ts | 24 +++---
.../src/{redirects => middleware}/cache.ts | 0
demo/site/src/middleware/predefinedPages.ts | 77 +++++++++++++++++++
.../{redirects => middleware}/redirects.ts | 0
packages/api/cms-api/schema.gql | 2 +-
.../src/page-tree/createPageTreeResolver.ts | 8 +-
.../paginated-page-tree-nodes-args.factory.ts | 8 +-
10 files changed, 120 insertions(+), 28 deletions(-)
create mode 100644 .changeset/dirty-clocks-wonder.md
rename demo/site/src/{redirects => middleware}/cache.ts (100%)
create mode 100644 demo/site/src/middleware/predefinedPages.ts
rename demo/site/src/{redirects => middleware}/redirects.ts (100%)
diff --git a/.changeset/dirty-clocks-wonder.md b/.changeset/dirty-clocks-wonder.md
new file mode 100644
index 0000000000..72e548cf01
--- /dev/null
+++ b/.changeset/dirty-clocks-wonder.md
@@ -0,0 +1,17 @@
+---
+"@comet/cms-api": minor
+---
+
+Support filtering for document types in the `paginatedPageTreeNodes` query
+
+**Example**
+
+```graphql
+query PredefinedPages($scope: PageTreeNodeScopeInput!) {
+ paginatedPageTreeNodes(scope: $scope, documentType: "PredefinedPage") {
+ nodes {
+ id
+ }
+ }
+}
+```
diff --git a/demo/api/schema.gql b/demo/api/schema.gql
index 9c3568a7e4..0e6cf3af7c 100644
--- a/demo/api/schema.gql
+++ b/demo/api/schema.gql
@@ -746,7 +746,7 @@ type Query {
pageTreeNode(id: ID!): PageTreeNode
pageTreeNodeByPath(path: String!, scope: PageTreeNodeScopeInput!): PageTreeNode
pageTreeNodeList(scope: PageTreeNodeScopeInput!, category: String): [PageTreeNode!]!
- paginatedPageTreeNodes(scope: PageTreeNodeScopeInput!, category: String, sort: [PageTreeNodeSort!], offset: Int! = 0, limit: Int! = 25): PaginatedPageTreeNodes!
+ paginatedPageTreeNodes(scope: PageTreeNodeScopeInput!, category: String, sort: [PageTreeNodeSort!], documentType: String, offset: Int! = 0, limit: Int! = 25): PaginatedPageTreeNodes!
pageTreeNodeSlugAvailable(scope: PageTreeNodeScopeInput!, parentId: ID, slug: String!): SlugAvailability!
redirects(scope: RedirectScopeInput!, query: String, type: RedirectGenerationType, active: Boolean, sortColumnName: String, sortDirection: SortDirection! = ASC): [Redirect!]! @deprecated(reason: "Use paginatedRedirects instead. Will be removed in the next version.")
paginatedRedirects(scope: RedirectScopeInput!, search: String, filter: RedirectFilter, sort: [RedirectSort!], offset: Int! = 0, limit: Int! = 25): PaginatedRedirects!
diff --git a/demo/site/src/header/PageLink.tsx b/demo/site/src/header/PageLink.tsx
index adfff3e37a..53d0e1b29a 100644
--- a/demo/site/src/header/PageLink.tsx
+++ b/demo/site/src/header/PageLink.tsx
@@ -1,7 +1,5 @@
"use client";
import { LinkBlock } from "@src/blocks/LinkBlock";
-import { GQLPredefinedPage } from "@src/graphql.generated";
-import { predefinedPagePaths } from "@src/predefinedPages/predefinedPagePaths";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { PropsWithChildren } from "react";
@@ -41,14 +39,8 @@ function PageLink({ page, children, className: passedClassName, activeClassName
);
} else if (page.documentType === "PredefinedPage") {
- if (!page.document) {
- return null;
- }
-
- const type = (page.document as GQLPredefinedPage).type;
-
return (
-
+
{children}
);
diff --git a/demo/site/src/middleware.ts b/demo/site/src/middleware.ts
index 1b5a2d84ba..452047ebdb 100644
--- a/demo/site/src/middleware.ts
+++ b/demo/site/src/middleware.ts
@@ -1,10 +1,9 @@
-import { Rewrite } from "next/dist/lib/load-custom-routes";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { domain } from "./config";
-import { GQLRedirectScope } from "./graphql.generated";
-import { createRedirects } from "./redirects/redirects";
+import { getPredefinedPageRedirect, getPredefinedPageRewrite } from "./middleware/predefinedPages";
+import { createRedirects } from "./middleware/redirects";
export async function middleware(request: NextRequest) {
const { pathname } = new URL(request.url);
@@ -19,20 +18,19 @@ export async function middleware(request: NextRequest) {
return NextResponse.redirect(new URL(destination, request.url), redirect.permanent ? 308 : 307);
}
- const rewrites = await createRewrites(scope);
- const rewrite = rewrites.get(pathname);
- if (rewrite) {
- return NextResponse.rewrite(new URL(rewrite.destination, request.url));
+ const predefinedPageRedirect = await getPredefinedPageRedirect(scope, pathname);
+
+ if (predefinedPageRedirect) {
+ return NextResponse.redirect(new URL(predefinedPageRedirect, request.url), 307);
}
- return NextResponse.next();
-}
+ const predefinedPageRewrite = await getPredefinedPageRewrite(scope, pathname);
-type RewritesMap = Map;
+ if (predefinedPageRewrite) {
+ return NextResponse.rewrite(new URL(predefinedPageRewrite, request.url));
+ }
-async function createRewrites(scope: GQLRedirectScope): Promise {
- const rewritesMap = new Map();
- return rewritesMap;
+ return NextResponse.next();
}
export const config = {
diff --git a/demo/site/src/redirects/cache.ts b/demo/site/src/middleware/cache.ts
similarity index 100%
rename from demo/site/src/redirects/cache.ts
rename to demo/site/src/middleware/cache.ts
diff --git a/demo/site/src/middleware/predefinedPages.ts b/demo/site/src/middleware/predefinedPages.ts
new file mode 100644
index 0000000000..d5926c00db
--- /dev/null
+++ b/demo/site/src/middleware/predefinedPages.ts
@@ -0,0 +1,77 @@
+import { gql } from "@comet/cms-site";
+import { languages } from "@src/config";
+import { predefinedPagePaths } from "@src/predefinedPages/predefinedPagePaths";
+import { createGraphQLFetch } from "@src/util/graphQLClient";
+
+import { memoryCache } from "./cache";
+import { GQLPredefinedPagesQuery, GQLPredefinedPagesQueryVariables } from "./predefinedPages.generated";
+
+async function getPredefinedPageRedirect(scope: { domain: string }, pathname: string): Promise {
+ const pages = await fetchPredefinedPages(scope);
+
+ const matchingPredefinedPage = pages.find((page) => pathname.startsWith(page.codePath));
+
+ if (matchingPredefinedPage) {
+ return pathname.replace(matchingPredefinedPage.codePath, matchingPredefinedPage.pageTreeNodePath);
+ }
+
+ return undefined;
+}
+
+async function getPredefinedPageRewrite(scope: { domain: string }, pathname: string): Promise {
+ const pages = await fetchPredefinedPages(scope);
+
+ const matchingPredefinedPage = pages.find((page) => pathname.startsWith(page.pageTreeNodePath));
+
+ if (matchingPredefinedPage) {
+ return pathname.replace(matchingPredefinedPage.pageTreeNodePath, matchingPredefinedPage.codePath);
+ }
+
+ return undefined;
+}
+
+const predefinedPagesQuery = gql`
+ query PredefinedPages($scope: PageTreeNodeScopeInput!) {
+ paginatedPageTreeNodes(scope: $scope, documentType: "PredefinedPage") {
+ nodes {
+ id
+ path
+ document {
+ __typename
+ ... on PredefinedPage {
+ type
+ }
+ }
+ }
+ }
+ }
+`;
+
+const graphQLFetch = createGraphQLFetch();
+
+async function fetchPredefinedPages(scope: { domain: string }) {
+ const key = `predefinedPages-${JSON.stringify(scope)}`;
+
+ return memoryCache.wrap(key, async () => {
+ const pages: Array<{ codePath: string; pageTreeNodePath: string }> = [];
+
+ for (const language of languages) {
+ const { paginatedPageTreeNodes } = await graphQLFetch(predefinedPagesQuery, {
+ scope: { domain: scope.domain, language },
+ });
+
+ for (const node of paginatedPageTreeNodes.nodes) {
+ if (node.document?.__typename === "PredefinedPage" && node.document.type) {
+ pages.push({
+ codePath: `/${language}${predefinedPagePaths[node.document.type]}`,
+ pageTreeNodePath: `/${language}${node.path}`,
+ });
+ }
+ }
+ }
+
+ return pages;
+ });
+}
+
+export { getPredefinedPageRedirect, getPredefinedPageRewrite };
diff --git a/demo/site/src/redirects/redirects.ts b/demo/site/src/middleware/redirects.ts
similarity index 100%
rename from demo/site/src/redirects/redirects.ts
rename to demo/site/src/middleware/redirects.ts
diff --git a/packages/api/cms-api/schema.gql b/packages/api/cms-api/schema.gql
index b83632f4e1..f43e214b9a 100644
--- a/packages/api/cms-api/schema.gql
+++ b/packages/api/cms-api/schema.gql
@@ -361,7 +361,7 @@ type Query {
pageTreeNode(id: ID!): PageTreeNode
pageTreeNodeByPath(path: String!, scope: PageTreeNodeScopeInput!): PageTreeNode
pageTreeNodeList(scope: PageTreeNodeScopeInput!, category: String): [PageTreeNode!]!
- paginatedPageTreeNodes(scope: PageTreeNodeScopeInput! = {}, category: String, sort: [PageTreeNodeSort!], offset: Int! = 0, limit: Int! = 25): PaginatedPageTreeNodes!
+ paginatedPageTreeNodes(scope: PageTreeNodeScopeInput! = {}, category: String, sort: [PageTreeNodeSort!], documentType: String, offset: Int! = 0, limit: Int! = 25): PaginatedPageTreeNodes!
pageTreeNodeSlugAvailable(scope: PageTreeNodeScopeInput!, parentId: ID, slug: String!): SlugAvailability!
kubernetesCronJobs: [KubernetesCronJob!]!
kubernetesCronJob(name: String!): KubernetesCronJob!
diff --git a/packages/api/cms-api/src/page-tree/createPageTreeResolver.ts b/packages/api/cms-api/src/page-tree/createPageTreeResolver.ts
index 7165433b67..43ca6a616c 100644
--- a/packages/api/cms-api/src/page-tree/createPageTreeResolver.ts
+++ b/packages/api/cms-api/src/page-tree/createPageTreeResolver.ts
@@ -100,10 +100,12 @@ export function createPageTreeResolver({
}
@Query(() => PaginatedPageTreeNodes)
- async paginatedPageTreeNodes(@Args() { scope, category, sort, offset, limit }: PaginatedPageTreeNodesArgs): Promise {
+ async paginatedPageTreeNodes(
+ @Args() { scope, category, sort, offset, limit, documentType }: PaginatedPageTreeNodesArgs,
+ ): Promise {
await this.pageTreeReadApi.preloadNodes(scope);
- const nodes = await this.pageTreeReadApi.getNodes({ scope: nonEmptyScopeOrNothing(scope), category, offset, limit, sort });
- const count = await this.pageTreeReadApi.getNodesCount({ scope: nonEmptyScopeOrNothing(scope), category });
+ const nodes = await this.pageTreeReadApi.getNodes({ scope: nonEmptyScopeOrNothing(scope), category, offset, limit, sort, documentType });
+ const count = await this.pageTreeReadApi.getNodesCount({ scope: nonEmptyScopeOrNothing(scope), category, documentType });
return new PaginatedPageTreeNodes(nodes, count);
}
diff --git a/packages/api/cms-api/src/page-tree/dto/paginated-page-tree-nodes-args.factory.ts b/packages/api/cms-api/src/page-tree/dto/paginated-page-tree-nodes-args.factory.ts
index e8a6fb3ae8..a211c04257 100644
--- a/packages/api/cms-api/src/page-tree/dto/paginated-page-tree-nodes-args.factory.ts
+++ b/packages/api/cms-api/src/page-tree/dto/paginated-page-tree-nodes-args.factory.ts
@@ -1,7 +1,7 @@
import { Type } from "@nestjs/common";
import { ArgsType, Field } from "@nestjs/graphql";
import { Type as TransformerType } from "class-transformer";
-import { IsOptional, ValidateNested } from "class-validator";
+import { IsOptional, IsString, ValidateNested } from "class-validator";
import { OffsetBasedPaginationArgs } from "../../common/pagination/offset-based.args";
import { PageTreeNodeCategory, ScopeInterface } from "../types";
@@ -14,6 +14,7 @@ export interface PaginatedPageTreeNodesArgsInterface {
sort?: PageTreeNodeSort[];
offset: number;
limit: number;
+ documentType?: string;
}
export class PaginatedPageTreeNodesArgsFactory {
@@ -33,6 +34,11 @@ export class PaginatedPageTreeNodesArgsFactory {
@TransformerType(() => PageTreeNodeSort)
@ValidateNested({ each: true })
sort?: PageTreeNodeSort[];
+
+ @Field({ nullable: true })
+ @IsOptional()
+ @IsString()
+ documentType?: string;
}
return PageTreeNodesArgs;
}