Skip to content

Commit

Permalink
Merge pull request #1237 from vivid-planet/site-preview
Browse files Browse the repository at this point in the history
Use Next.JS Site-Preview mode
  • Loading branch information
fraxachun authored Nov 22, 2023
2 parents b6de17b + 82325bc commit 1d0bf6c
Show file tree
Hide file tree
Showing 34 changed files with 239 additions and 442 deletions.
23 changes: 23 additions & 0 deletions .changeset/yellow-seahorses-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@comet/cms-admin": major
"@comet/cms-api": major
"@comet/eslint-config": minor
"@comet/cms-site": minor
---

Migrate site preview to Next.js Preview Mode

Requires following changes to site:

- Import `useRouter` from `next/router` (not exported from `@comet/cms-site` anymore)
- Import `Link` from `next/link` (not exported from `@comet/cms-site` anymore)
- Remove preview pages (pages in `src/pages/preview/` directory which call `createGetUniversalProps` with preview parameters)
- Remove `createGetUniversalProps`
- Just implement `getStaticProps`/`getServerSideProps` (Preview Mode will SSR automatically)
- Get `previewData` from `context` and use it to configure the GraphQL Client
- Add `SitePreviewProvider` to `App` (typically in `src/pages/_app.tsx`)
- Add `/api/preview` Next API route (see demo)

Requires following changes to API:

- Set `sitePreviewSecret` in `PageTreeModule`-options (make sure it's the same across multiple API-instances)
1 change: 0 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ SITE_PORT=3000
SITE_URL=http://localhost:$SITE_PORT
SITE_PRELOGIN_ENABLED=false
SITE_PRELOGIN_PASSWORD=password
PREVIEW_URL=$SITE_URL/preview
# no gtm in dev mode
NEXT_PUBLIC_GTM_ID=
NEXT_PUBLIC_SITE_DOMAIN=main
Expand Down
7 changes: 7 additions & 0 deletions demo/api/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ type FilenameResponse {
isOccupied: Boolean!
}

type SitePreviewHash {
timestamp: Float!
hash: String!
}

type CurrentUserPermission {
permission: String!
}
Expand Down Expand Up @@ -602,6 +607,8 @@ type Query {
pageTreeNodeList(scope: PageTreeNodeScopeInput!, category: String): [PageTreeNode!]!
paginatedPageTreeNodes(scope: PageTreeNodeScopeInput!, category: String, sort: [PageTreeNodeSort!], offset: Int! = 0, limit: Int! = 25): PaginatedPageTreeNodes!
pageTreeNodeSlugAvailable(scope: PageTreeNodeScopeInput!, parentId: ID, slug: String!): SlugAvailability!
getSitePreviewHash: SitePreviewHash!
validateSitePreviewHash(timestamp: Float!, hash: String!): Boolean!
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!
redirect(id: ID!): Redirect!
Expand Down
2 changes: 2 additions & 0 deletions demo/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { DbModule } from "@src/db/db.module";
import { LinksModule } from "@src/links/links.module";
import { PagesModule } from "@src/pages/pages.module";
import { PredefinedPage } from "@src/predefined-page/entities/predefined-page.entity";
import { randomBytes } from "crypto";
import { Request } from "express";

import { AuthModule } from "./auth/auth.module";
Expand Down Expand Up @@ -123,6 +124,7 @@ export class AppModule {
Documents: [Page, Link, PredefinedPage],
Scope: PageTreeNodeScope,
reservedPaths: ["/events"],
sitePreviewSecret: randomBytes(32).toString("hex"),
}),
RedirectsModule.register({ customTargets: { news: NewsLinkBlock }, Scope: RedirectScope }),
BlobStorageModule.register({
Expand Down
2 changes: 1 addition & 1 deletion demo/site/src/components/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Link } from "@comet/cms-site";
import { GridRoot } from "@src/components/common/GridRoot";
import { gql } from "graphql-request";
import Link from "next/link";
import * as React from "react";

import { GQLBreadcrumbsFragment } from "./Breadcrumbs.generated";
Expand Down
3 changes: 2 additions & 1 deletion demo/site/src/header/PageLink.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Link, useRouter } from "@comet/cms-site";
import { LinkBlock } from "@src/blocks/LinkBlock";
import { GQLPredefinedPage } from "@src/graphql.generated";
import { predefinedPagePaths } from "@src/predefinedPages/predefinedPagePaths";
import { gql } from "graphql-request";
import Link from "next/link";
import { useRouter } from "next/router";
import * as React from "react";

import { GQLPageLinkFragment } from "./PageLink.generated";
Expand Down
3 changes: 2 additions & 1 deletion demo/site/src/news/blocks/NewsLinkBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Link, PropsWithData } from "@comet/cms-site";
import { PropsWithData } from "@comet/cms-site";
import { NewsLinkBlockData } from "@src/blocks.generated";
import Link from "next/link";
import * as React from "react";

function NewsLinkBlock({ data: { id }, children }: React.PropsWithChildren<PropsWithData<NewsLinkBlockData>>): JSX.Element | null {
Expand Down
104 changes: 34 additions & 70 deletions demo/site/src/pages/[[...path]].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,17 @@ import NotFound404 from "@src/pages/404";
import PageTypePage, { pageQuery as PageTypePageQuery } from "@src/pageTypes/Page";
import createGraphQLClient from "@src/util/createGraphQLClient";
import { gql } from "graphql-request";
import {
GetServerSidePropsContext,
GetServerSidePropsResult,
GetStaticPaths,
GetStaticProps,
GetStaticPropsContext,
GetStaticPropsResult,
InferGetStaticPropsType,
} from "next";
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next";
import { ParsedUrlQuery } from "querystring";
import * as React from "react";

import { GQLPagesQuery, GQLPagesQueryVariables, GQLPageTypeQuery, GQLPageTypeQueryVariables } from "./[[...path]].generated";
import { PreviewData } from "./api/preview";

interface PageProps {
type PageProps = GQLPage & {
documentType: string;
id: string;
}
export type PageUniversalProps = PageProps & GQLPage;
};

export default function Page(props: InferGetStaticPropsType<typeof getStaticProps>): JSX.Element {
if (!pageTypes[props.documentType]) {
Expand All @@ -34,6 +27,7 @@ export default function Page(props: InferGetStaticPropsType<typeof getStaticProp
);
}
const { component: Component } = pageTypes[props.documentType];

return <Component {...props} />;
}

Expand All @@ -53,67 +47,37 @@ const pageTypes = {
},
};

export const getStaticProps: GetStaticProps<PageUniversalProps> = async (context) => {
const getUniversalProps = createGetUniversalProps();
return getUniversalProps(context);
};

interface CreateGetUniversalPropsOptions {
includeInvisiblePages?: boolean;
includeInvisibleBlocks?: boolean;
previewDamUrls?: boolean;
}

// a function to create a universal function which can be used as getStaticProps or getServerSideProps (preview)
export function createGetUniversalProps({
includeInvisiblePages = false,
includeInvisibleBlocks = false,
previewDamUrls = false,
}: CreateGetUniversalPropsOptions = {}) {
return async function getUniversalProps<Context extends GetStaticPropsContext | GetServerSidePropsContext>({
params,
locale = defaultLanguage,
}: Context): Promise<
Context extends GetStaticPropsContext ? GetStaticPropsResult<PageUniversalProps> : GetServerSidePropsResult<PageUniversalProps>
> {
const path = params?.path ?? "";
const contentScope = { domain, language: locale };

//fetch pageType
const data = await createGraphQLClient({ includeInvisiblePages, includeInvisibleBlocks, previewDamUrls }).request<
GQLPageTypeQuery,
GQLPageTypeQueryVariables
>(pageTypeQuery, {
path: `/${Array.isArray(path) ? path.join("/") : path}`,
contentScope,
});
if (!data.pageTreeNodeByPath?.documentType) {
// eslint-disable-next-line no-console
console.log("got no data from api", data, path);
return { notFound: true };
}
const pageId = data.pageTreeNodeByPath.id;
export const getStaticProps: GetStaticProps<PageProps, ParsedUrlQuery, PreviewData> = async ({ params, previewData, locale = defaultLanguage }) => {
const path = params?.path ?? "";
const contentScope = { domain, language: locale };
//fetch pageType
const data = await createGraphQLClient(previewData).request<GQLPageTypeQuery, GQLPageTypeQueryVariables>(pageTypeQuery, {
path: `/${Array.isArray(path) ? path.join("/") : path}`,
contentScope,
});
if (!data.pageTreeNodeByPath?.documentType) {
// eslint-disable-next-line no-console
console.log("got no data from api", data, path);
return { notFound: true };
}
const pageId = data.pageTreeNodeByPath.id;

//pageType dependent query
const { query: queryForPageType } = pageTypes[data.pageTreeNodeByPath.documentType];
const pageTypeData = await createGraphQLClient({ includeInvisiblePages, includeInvisibleBlocks, previewDamUrls }).request<GQLPage>(
queryForPageType,
{
pageId,
domain: contentScope.domain,
language: contentScope.language,
},
);
//pageType dependent query
const { query: queryForPageType } = pageTypes[data.pageTreeNodeByPath.documentType];
const pageTypeData = await createGraphQLClient(previewData).request<GQLPage>(queryForPageType, {
pageId,
domain: contentScope.domain,
language: contentScope.language,
});

return {
props: {
...pageTypeData,
documentType: data.pageTreeNodeByPath.documentType,
id: pageId,
},
};
return {
props: {
...pageTypeData,
documentType: data.pageTreeNodeByPath.documentType,
id: pageId,
},
};
}
};

const pagesQuery = gql`
query Pages($contentScope: PageTreeNodeScopeInput!) {
Expand Down
5 changes: 4 additions & 1 deletion demo/site/src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SitePreviewProvider } from "@comet/cms-site";
import theme, { Theme } from "@src/theme";
import { AppProps, NextWebVitalsMetric } from "next/app";
import Head from "next/head";
Expand Down Expand Up @@ -65,7 +66,9 @@ export default function App({ Component, pageProps }: AppProps): JSX.Element {
</Head>
<ThemeProvider theme={theme}>
<GlobalStyle />
<Component {...pageProps} />
<SitePreviewProvider>
<Component {...pageProps} />
</SitePreviewProvider>
</ThemeProvider>
</IntlProvider>
);
Expand Down
31 changes: 31 additions & 0 deletions demo/site/src/pages/api/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import createGraphQLClient, { GraphQLClientOptions } from "@src/util/createGraphQLClient";
import { gql } from "graphql-request";

import { GQLValidateSitePreviewHashQuery, GQLValidateSitePreviewHashQueryVariables } from "./preview.generated";

export default async function handler(req, res) {
const data = await createGraphQLClient().request<GQLValidateSitePreviewHashQuery, GQLValidateSitePreviewHashQueryVariables>(
gql`
query ValidateSitePreviewHash($timestamp: Float!, $hash: String!) {
validateSitePreviewHash(timestamp: $timestamp, hash: $hash)
}
`,
{
timestamp: parseInt(req.query.timestamp),
hash: req.query.hash,
},
);
if (!data.validateSitePreviewHash) {
return res.status(401).json({ message: "Validation failed" });
}

const previewData: PreviewData = {
includeInvisiblePages: true,
includeInvisibleBlocks: req.query.includeInvisibleBlocks === "true",
previewDamUrls: true,
};
res.setPreviewData(previewData);
res.redirect(req.query.path ?? "/");
}

export type PreviewData = GraphQLClientOptions;
22 changes: 0 additions & 22 deletions demo/site/src/pages/preview/[[...path]].tsx

This file was deleted.

5 changes: 3 additions & 2 deletions demo/site/src/util/createGraphQLClient.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { GraphQLClient } from "graphql-request";

interface GraphQLClientOptions {
export type GraphQLClientOptions = {
includeInvisiblePages: boolean;
includeInvisibleBlocks: boolean;
previewDamUrls: boolean;
}
};

const defaultOptions: GraphQLClientOptions = {
includeInvisiblePages: false,
includeInvisibleBlocks: false,
Expand Down
29 changes: 13 additions & 16 deletions packages/admin/cms-admin/src/preview/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,19 @@ IFrameBridgePreviewPage (src/pages/preview/admin/page.tsx)

## Site Preview

Similar to real site but live rendered (SSR) and optionally with invisible blocks shown.
Uses Next.js Preview Mode to live render pages (SSR), optionally with invisible blocks shown.

### iframe messages: admin -> site

- *none*

### URL: admin -> site

- page url (real site url prefixed with /preview)
- __preview parameter: { includeInvisibleBlocks: boolean } (JSON encoded)
Admin opens I-Frame with {previewSiteUrl}/api/preview to enter Next.js Preview Mode and passes the following parameters:

- path: which pathname to be shown
- includeInvisibleBlocks
- timestamp & hash: is validated to activate Preview Mode -

### iframe messages: site -> admin

Expand All @@ -96,7 +99,6 @@ Similar to real site but live rendered (SSR) and optionally with invisible block
SitePreview: state from Url (get params): path, device, showOnlyVisible
- has controls for managing path, device, showOnlyVisible
- handles messages coming from iframe (OpenLink, SitePreviewLocation)
- appends authProvider to iframeUrl
- handles incoming messages (with useSitePreviewIFrameBridge)
IFrameViewer[common] (prop drilling: device)
- does scale the iframe according to device (+the device around the iframe)
Expand All @@ -106,16 +108,11 @@ SitePreview: state from Url (get params): path, device, showOnlyVisible
### Site: States, Contexts and Components

```
AuthenticatedPreviewPage (src/pages/preview/[...path]].tsx)
SitePreviewPage
- checks login and registers serviceworker
SitePreviewProvider
- messages SitePreviewLocation on location change (sends message directly using sendSitePreviewIFrameMessage helper)
- creates PreviewContext containing
- previewType: "SitePreview",
- showPreviewSkeletons: false,
- pathToPreviewPath: implementation that adds baseUrl (/preview) and __preview params
- previewPathToPath: implementation that removes them
Page (src/pages/[...path]].tsx)
- ExternalLinkBlock messages OpenLink (sends message directly using sendSitePreviewIFrameMessage helper)
SitePreviewProvider (only active in Preview Mode)
- messages SitePreviewLocation on location change (sends message directly using sendSitePreviewIFrameMessage helper)
- creates PreviewContext containing
- previewType: "SitePreview",
- showPreviewSkeletons: false,
Page (src/pages/[...path]].tsx)
- ExternalLinkBlock messages OpenLink (sends message directly using sendSitePreviewIFrameMessage helper)
```
Loading

0 comments on commit 1d0bf6c

Please sign in to comment.