Skip to content

Commit

Permalink
Replace next Draft Mode with custom cookie and preview route
Browse files Browse the repository at this point in the history
  • Loading branch information
pookmish committed Mar 29, 2024
1 parent 7393df5 commit 820af18
Show file tree
Hide file tree
Showing 23 changed files with 140 additions and 121 deletions.
32 changes: 3 additions & 29 deletions app/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,28 @@
import NodePage from "@components/nodes/pages/node-page";
import UnpublishedBanner from "@components/elements/unpublished-banner";
import {Metadata} from "next";
import {NodeUnion} from "@lib/gql/__generated__/drupal.d";
import {getAllNodePaths, getEntityFromPath} from "@lib/gql/gql-queries";
import {getNodeMetadata} from "./metadata";
import {isDraftMode} from "@lib/drupal/utils";
import {notFound, redirect} from "next/navigation";
import {getPathFromContext, PageProps} from "@lib/drupal/utils";

// https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config
export const revalidate = false;
export const dynamic = 'force-static';

const Page = async ({params}: PageProps) => {
const path = getPathFromContext({params})
const inDraft = isDraftMode();

const {redirect: redirectPath, entity, error} = await getEntityFromPath<NodeUnion>(path, inDraft)
const {redirect: redirectPath, entity, error} = await getEntityFromPath<NodeUnion>(path)

if (error) throw new Error(error);
if (redirectPath?.url) redirect(redirectPath.url)
if (!entity) notFound();

return (
<>
<UnpublishedBanner status={entity.status}>
Unpublished Page
</UnpublishedBanner>
<NodePage node={entity}/>
</>
)
return <NodePage node={entity}/>
}

export const generateMetadata = async ({params}: PageProps): Promise<Metadata> => {
// If the user is in draft mode, there's no need to emit any customized metadata.
if (isDraftMode()) return {};

const path = getPathFromContext({params})
const {entity} = await getEntityFromPath<NodeUnion>(path)
return entity ? getNodeMetadata(entity) : {};
Expand All @@ -46,18 +34,4 @@ export const generateStaticParams = async (): Promise<PageProps["params"][]> =>
return nodePaths.map(path => ({slug: path.split('/').filter(part => !!part)}));
}

const getPathFromContext = (context: PageProps, prefix = ""): string => {
let {slug} = context.params

slug = Array.isArray(slug) ? slug.map((s) => encodeURIComponent(s)).join("/") : slug
slug = slug.replace(/^\//, '');
return prefix ? `${prefix}/${slug}` : `/${slug}`
}

type PageProps = {
params: { slug: string | string[] }
searchParams?: Record<string, string | string[] | undefined>
}


export default Page;
9 changes: 0 additions & 9 deletions app/api/draft/disable/route.tsx

This file was deleted.

13 changes: 9 additions & 4 deletions app/api/draft/route.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {NextRequest, NextResponse} from "next/server";
import {draftMode} from 'next/headers'
import {redirect} from 'next/navigation'
import {cookies} from "next/headers";

export const revalidate = 0;

Expand All @@ -18,10 +18,15 @@ export async function GET(request: NextRequest) {
if (!slug) {
return NextResponse.json({message: 'Invalid slug path'}, {status: 401})
}

draftMode().enable()
cookies().set('preview', secret, {
maxAge: 60 * 60,
httpOnly: true,
sameSite: 'none',
secure: true,
partitioned: true,
});

// Redirect to the path from the fetched post
// We don't redirect to searchParams.slug as that might lead to open redirect vulnerabilities
redirect(slug)
redirect(`/preview/${slug}`)
}
33 changes: 9 additions & 24 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import '../src/styles/index.css';
import BackToTop from "@components/elements/back-to-top";
import DrupalWindowSync from "@components/elements/drupal-window-sync";
import Editori11y from "@components/tools/editorially";
import Link from "@components/elements/link";
import PageFooter from "@components/global/page-footer";
import PageHeader from "@components/global/page-header";
import Script from "next/script";
import {GoogleAnalytics} from "@next/third-parties/google";
import {Icon} from "next/dist/lib/metadata/types/metadata-types";
import {StanfordBasicSiteSetting} from "@lib/gql/__generated__/drupal.d";
import {getConfigPage} from "@lib/gql/gql-queries";
import {isDraftMode} from "@lib/drupal/utils";
import {sourceSans3} from "../src/styles/fonts";
import DrupalWindowSync from "@components/elements/drupal-window-sync";
import {isPreviewMode} from "@lib/drupal/utils";
import UserAnalytics from "@components/elements/user-analytics";

const appleIcons: Icon[] = [60, 72, 76, 114, 120, 144, 152, 180].map(size => ({
url: `https://www-media.stanford.edu/assets/favicon/apple-touch-icon-${size}x${size}.png`,
Expand Down Expand Up @@ -47,30 +42,20 @@ export const metadata = {
// https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config
export const revalidate = false;

const RootLayout = async ({children, modal}: { children: React.ReactNode, modal: React.ReactNode }) => {
const draftMode = isDraftMode();
const siteSettingsConfig = await getConfigPage<StanfordBasicSiteSetting>('StanfordBasicSiteSetting')
const RootLayout = ({children, modal}: { children: React.ReactNode, modal: React.ReactNode }) => {
const isPreview = isPreviewMode();
return (
<html lang="en" className={sourceSans3.className}>
{draftMode && <><Editori11y/><DrupalWindowSync/></>}

{/* Add Google Analytics and SiteImprove when not in draft mode. */}
{(!draftMode && siteSettingsConfig?.suGoogleAnalytics) &&
<>
<Script async src="//siteimproveanalytics.com/js/siteanalyze_80352.js"/>
<GoogleAnalytics gaId={siteSettingsConfig?.suGoogleAnalytics}/>
</>
{/* Add Google Analytics and SiteImprove when not in preview mode. */}
{!isPreview &&
<UserAnalytics/>
}
<DrupalWindowSync/>
<body>
<nav aria-label="Skip Links">
<a href="#main-content" className="skiplink">Skip to main content</a>
</nav>

{/* Automatically exit "Draft" mode upon the page loading. This prevents unwanted uncached data fetching. */}
{draftMode &&
<Link href="/api/draft/disable" tabIndex={-1} className="sr-only">Disable Draft Mode</Link>
}

<div className="flex flex-col min-h-screen">
<PageHeader/>
<main id="main-content" className="flex-grow mb-32">
Expand Down
4 changes: 2 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Rows from "@components/paragraphs/rows/rows";
import {notFound} from "next/navigation";
import {getEntityFromPath} from "@lib/gql/gql-queries";
import {NodeStanfordPage, NodeUnion} from "@lib/gql/__generated__/drupal.d";
import {isDraftMode} from "@lib/drupal/utils";
import {isPreviewMode} from "@lib/drupal/utils";
import {Metadata} from "next";
import {getNodeMetadata} from "./[...slug]/metadata";
import BannerParagraph from "@components/paragraphs/stanford-banner/banner-paragraph";
Expand All @@ -12,7 +12,7 @@ export const revalidate = false;
export const dynamic = 'force-static';

const Home = async () => {
const {entity, error} = await getEntityFromPath<NodeStanfordPage>('/', isDraftMode());
const {entity, error} = await getEntityFromPath<NodeStanfordPage>('/', isPreviewMode());

if (error) throw new Error(error);
if (!entity) notFound();
Expand Down
32 changes: 32 additions & 0 deletions app/preview/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import NodePage from "@components/nodes/pages/node-page";
import UnpublishedBanner from "@components/elements/unpublished-banner";
import {NodeUnion} from "@lib/gql/__generated__/drupal.d";
import {getEntityFromPath} from "@lib/gql/gql-queries";
import {notFound} from "next/navigation";
import Editori11y from "@components/tools/editorially";
import {getPathFromContext, isPreviewMode, PageProps} from "@lib/drupal/utils";

const Page = async ({params}: PageProps) => {
const path = getPathFromContext({params})
if (!isPreviewMode()) notFound();

const { entity, error} = await getEntityFromPath<NodeUnion>(path, true)

if (error) throw new Error(error);
if (!entity) notFound();

return (
<>
<Editori11y/>
<UnpublishedBanner status={false}>
Preview Mode
</UnpublishedBanner>
<UnpublishedBanner status={entity.status}>
Unpublished Page
</UnpublishedBanner>
<NodePage node={entity}/>
</>
)
}

export default Page;
27 changes: 14 additions & 13 deletions src/components/elements/drupal-window-sync.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
"use client";

import {usePathname} from "next/navigation";
import {useEffect} from "react";
import {useIsClient} from "usehooks-ts";

const DrupalWindowSync = () => {
const pathname = usePathname();
if (!useIsClient()) return;

useEffect(() => {
if (!pathname) return;

if (
pathname &&
!pathname?.startsWith('/gallery/') &&
window &&
window.top !== window.self
) {
window.parent.postMessage({type: "NEXT_DRUPAL_ROUTE_SYNC", path: pathname}, process.env.NEXT_PUBLIC_DRUPAL_BASE_URL as string)
}
}, [pathname]);
if (
pathname &&
!pathname?.startsWith('/gallery/') &&
!pathname?.startsWith('/preview/') &&
window &&
window.top !== window.self
) {
window.parent.postMessage({
type: "NEXT_DRUPAL_ROUTE_SYNC",
path: pathname
}, process.env.NEXT_PUBLIC_DRUPAL_BASE_URL as string)
}
return null;
}

Expand Down
18 changes: 18 additions & 0 deletions src/components/elements/user-analytics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {getConfigPage} from "@lib/gql/gql-queries";
import {StanfordBasicSiteSetting} from "@lib/gql/__generated__/drupal";
import Script from "next/script";
import {GoogleAnalytics} from "@next/third-parties/google";
import {isPreviewMode} from "@lib/drupal/utils";

const UserAnalytics = async () => {
if (isPreviewMode()) return;
const siteSettingsConfig = await getConfigPage<StanfordBasicSiteSetting>('StanfordBasicSiteSetting')
if (!siteSettingsConfig?.suGoogleAnalytics) return;
return (
<>
<Script async src="//siteimproveanalytics.com/js/siteanalyze_80352.js"/>
<GoogleAnalytics gaId={siteSettingsConfig?.suGoogleAnalytics}/>
</>
)
}
export default UserAnalytics;
4 changes: 2 additions & 2 deletions src/components/global/page-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import {
StanfordBasicSiteSetting,
StanfordGlobalMessage
} from "@lib/gql/__generated__/drupal.d";
import {isDraftMode} from "@lib/drupal/utils";
import {isPreviewMode} from "@lib/drupal/utils";

const PageHeader = async () => {
const menuItems = await getMenu(MenuAvailable.Main, isDraftMode());
const menuItems = await getMenu(MenuAvailable.Main, isPreviewMode());
const globalMessageConfig = await getConfigPage<StanfordGlobalMessage>('StanfordGlobalMessage');
const siteSettingsConfig = await getConfigPage<StanfordBasicSiteSetting>('StanfordBasicSiteSetting')
const lockupSettingsConfig = await getConfigPage<LockupSetting>('LockupSetting')
Expand Down
4 changes: 2 additions & 2 deletions src/components/layouts/interior-page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {getMenu} from "@lib/gql/gql-queries";
import SideNav from "@components/menu/side-nav";
import {HtmlHTMLAttributes} from "react";
import {isDraftMode} from "@lib/drupal/utils";
import {isPreviewMode} from "@lib/drupal/utils";
import {MenuAvailable} from "@lib/gql/__generated__/drupal.d";
import useActiveTrail from "@lib/hooks/useActiveTrail";
import {twMerge} from "tailwind-merge";
Expand All @@ -14,7 +14,7 @@ type Props = HtmlHTMLAttributes<HTMLDivElement> & {
}

const InteriorPage = async ({children, currentPath, ...props}: Props) => {
const menu = await getMenu(MenuAvailable.Main, isDraftMode());
const menu = await getMenu(MenuAvailable.Main, isPreviewMode());
const activeTrail: string[] = useActiveTrail(menu, currentPath);

// Peel off the menu items from the parent.
Expand Down
6 changes: 3 additions & 3 deletions src/components/nodes/cards/node-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import StanfordPageCard from "@components/nodes/cards/stanford-page/stanford-pag
import StanfordPersonCard from "@components/nodes/cards/stanford-person/stanford-person-card";
import StanfordPolicyCard from "@components/nodes/cards/stanford-policy/stanford-policy-card";
import StanfordPublicationCard from "@components/nodes/cards/stanford-publication/stanford-publication-card";
import {isDraftMode} from "@lib/drupal/utils";
import {isPreviewMode} from "@lib/drupal/utils";
import {NodeUnion} from "@lib/gql/__generated__/drupal.d";

type Props = {
Expand All @@ -21,9 +21,9 @@ type Props = {
}

const NodeCard = ({node, headingLevel}: Props) => {
const draftMode = isDraftMode();
const previewMode = isPreviewMode();
const itemProps: { [key: string]: string } = {};
if (draftMode) {
if (previewMode) {
itemProps['data-type'] = node.__typename || 'unknown';
itemProps['data-id'] = node.id;
}
Expand Down
6 changes: 3 additions & 3 deletions src/components/nodes/list-item/node-list-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import StanfordPersonListItem from "@components/nodes/list-item/stanford-person/
import StanfordPolicyListItem from "@components/nodes/list-item/stanford-policy/stanford-policy-list-item";
import StanfordPublicationListItem
from "@components/nodes/list-item/stanford-publication/stanford-publication-list-item";
import {isDraftMode} from "@lib/drupal/utils";
import {isPreviewMode} from "@lib/drupal/utils";
import {NodeUnion} from "@lib/gql/__generated__/drupal.d";

type Props = {
Expand All @@ -23,9 +23,9 @@ type Props = {
}

const NodeListItem = ({node, headingLevel}: Props) => {
const draftMode = isDraftMode();
const previewMode = isPreviewMode();
const itemProps: { [key: string]: string } = {};
if (draftMode) {
if (previewMode) {
itemProps['data-type'] = node.__typename || 'unknown';
itemProps['data-id'] = node.id;
}
Expand Down
6 changes: 3 additions & 3 deletions src/components/nodes/pages/node-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import StanfordPolicyPage from "@components/nodes/pages/stanford-policy/stanford
import StanfordPublicationPage from "@components/nodes/pages/stanford-publication/stanford-publication-page";
import StanfordCoursePage from "@components/nodes/pages/stanford-course/stanford-course-page";
import StanfordEventSeriesPage from "@components/nodes/pages/stanford-event-series/stanford-event-series-page";
import {isDraftMode} from "@lib/drupal/utils";
import {isPreviewMode} from "@lib/drupal/utils";
import {NodeUnion} from "@lib/gql/__generated__/drupal.d";

const NodePage = ({node}: { node: NodeUnion }) => {
const draftMode = isDraftMode();
const previewMode = isPreviewMode();
const itemProps: { [key: string]: string } = {};

if (draftMode) {
if (previewMode) {
itemProps['data-type'] = node.__typename || 'unknown';
itemProps['data-id'] = node.id;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import Wysiwyg from "@components/elements/wysiwyg";
import {H1} from "@components/elements/headers";
import {HtmlHTMLAttributes} from "react";
import {NodeStanfordCourse} from "@lib/gql/__generated__/drupal.d";
import {isDraftMode} from "@lib/drupal/utils";
import {isPreviewMode} from "@lib/drupal/utils";

type Props = HtmlHTMLAttributes<HTMLDivElement> & {
node: NodeStanfordCourse
headingLevel?: "h2" | "h3"
}

const StanfordCoursePage = ({node, ...props}: Props) => {
if (node.suCourseLink?.url && !isDraftMode()) redirect(node.suCourseLink?.url);
if (node.suCourseLink?.url && !isPreviewMode()) redirect(node.suCourseLink?.url);
return (
<article className="centered my-32" {...props}>
<H1>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ import {NodeStanfordEvent, ParagraphStanfordSchedule} from "@lib/gql/__generated
import Email from "@components/elements/email";
import Telephone from "@components/elements/telephone";
import Link from "@components/elements/link";
import {isDraftMode} from "@lib/drupal/utils";
import {isPreviewMode} from "@lib/drupal/utils";

type Props = HtmlHTMLAttributes<HTMLDivElement> & {
node: NodeStanfordEvent
headingLevel?: "h2" | "h3"
}

const StanfordEventPage = ({node, ...props}: Props) => {
if (node.suEventSource?.url && !isDraftMode()) redirect(node.suEventSource.url)
if (node.suEventSource?.url && !isPreviewMode()) redirect(node.suEventSource.url)

const startTime = new Date(node.suEventDateTime.value * 1000);
const endTime = new Date(node.suEventDateTime.end_value * 1000);
Expand Down
Loading

0 comments on commit 820af18

Please sign in to comment.