Skip to content

Commit

Permalink
App Router: Support user-defined paths for predefined pages (#2293)
Browse files Browse the repository at this point in the history
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`)
  • Loading branch information
johnnyomair authored Sep 18, 2024
1 parent bc1ed88 commit f1d9e44
Show file tree
Hide file tree
Showing 10 changed files with 120 additions and 28 deletions.
17 changes: 17 additions & 0 deletions .changeset/dirty-clocks-wonder.md
Original file line number Diff line number Diff line change
@@ -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
}
}
}
```
2 changes: 1 addition & 1 deletion demo/api/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
10 changes: 1 addition & 9 deletions demo/site/src/header/PageLink.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -41,14 +39,8 @@ function PageLink({ page, children, className: passedClassName, activeClassName
</Link>
);
} else if (page.documentType === "PredefinedPage") {
if (!page.document) {
return null;
}

const type = (page.document as GQLPredefinedPage).type;

return (
<Link href={type && predefinedPagePaths[type] ? `/${page.scope.language}${predefinedPagePaths[type]}` : ""} className={className}>
<Link href={`/${page.scope.language}${page.path}`} className={className}>
{children}
</Link>
);
Expand Down
24 changes: 11 additions & 13 deletions demo/site/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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<string, Rewrite>;
if (predefinedPageRewrite) {
return NextResponse.rewrite(new URL(predefinedPageRewrite, request.url));
}

async function createRewrites(scope: GQLRedirectScope): Promise<RewritesMap> {
const rewritesMap = new Map<string, Rewrite>();
return rewritesMap;
return NextResponse.next();
}

export const config = {
Expand Down
File renamed without changes.
77 changes: 77 additions & 0 deletions demo/site/src/middleware/predefinedPages.ts
Original file line number Diff line number Diff line change
@@ -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<string | undefined> {
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<string | undefined> {
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<GQLPredefinedPagesQuery, GQLPredefinedPagesQueryVariables>(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 };
File renamed without changes.
2 changes: 1 addition & 1 deletion packages/api/cms-api/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
8 changes: 5 additions & 3 deletions packages/api/cms-api/src/page-tree/createPageTreeResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,12 @@ export function createPageTreeResolver({
}

@Query(() => PaginatedPageTreeNodes)
async paginatedPageTreeNodes(@Args() { scope, category, sort, offset, limit }: PaginatedPageTreeNodesArgs): Promise<PaginatedPageTreeNodes> {
async paginatedPageTreeNodes(
@Args() { scope, category, sort, offset, limit, documentType }: PaginatedPageTreeNodesArgs,
): Promise<PaginatedPageTreeNodes> {
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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -14,6 +14,7 @@ export interface PaginatedPageTreeNodesArgsInterface {
sort?: PageTreeNodeSort[];
offset: number;
limit: number;
documentType?: string;
}

export class PaginatedPageTreeNodesArgsFactory {
Expand All @@ -33,6 +34,11 @@ export class PaginatedPageTreeNodesArgsFactory {
@TransformerType(() => PageTreeNodeSort)
@ValidateNested({ each: true })
sort?: PageTreeNodeSort[];

@Field({ nullable: true })
@IsOptional()
@IsString()
documentType?: string;
}
return PageTreeNodesArgs;
}
Expand Down

0 comments on commit f1d9e44

Please sign in to comment.