Skip to content

Commit

Permalink
Use nextJS draft mode instead of preview (#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
pookmish authored Nov 14, 2023
1 parent 4e0f68f commit 93b359d
Show file tree
Hide file tree
Showing 25 changed files with 252 additions and 230 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ NEXT_PUBLIC_DRUPAL_BASE_URL=${NEXT_PUBLIC_DRUPAL_BASE_URL}
NEXT_IMAGE_DOMAIN=${NEXT_IMAGE_DOMAIN}
NEXT_PUBLIC_SITE_NAME=${NEXT_PUBLIC_SITE_NAME}

#DRUPAL_DRAFT_CLIENT=${DRUPAL_DRAFT_CLIENT}
#DRUPAL_DRAFT_SECRET=${DRUPAL_DRAFT_SECRET}
DRUPAL_PREVIEW_SECRET=${DRUPAL_PREVIEW_SECRET}
DRUPAL_REVALIDATE_SECRET=${DRUPAL_REVALIDATE_SECRET}

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build_lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ jobs:
build-lint:
runs-on: ubuntu-latest
container:
image: node:18.10
image: node:18
env:
NODE_ENV: development
NEXT_PUBLIC_DRUPAL_BASE_URL: ${{ secrets.NEXT_PUBLIC_DRUPAL_BASE_URL }}
Expand Down
18 changes: 13 additions & 5 deletions app/(public)/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import {Suspense} from "react";
import SecondaryMenu from "@/components/menu/secondary-menu";
import {getMenu} from "@/lib/drupal/get-menu";
import {DrupalJsonApiParams} from "drupal-jsonapi-params";
import {isDraftMode} from "@/lib/drupal/is-draft-mode";
import UnpublishedBanner from "@/components/patterns/unpublished-banner";

export const revalidate = 1800;
export const revalidate = 86400;

class RedirectError extends Error {
constructor(public message: string) {
Expand All @@ -24,7 +26,8 @@ class RedirectError extends Error {
}

const fetchNodeData = async (context) => {
const path = await translatePathFromContext(context);
const draftMode = isDraftMode();
const path = await translatePathFromContext(context, {draftMode});

// Check for redirect.
if (path?.redirect?.[0].to) {
Expand All @@ -44,16 +47,18 @@ const fetchNodeData = async (context) => {
throw new RedirectError(path.entity.path);
}

const node = await getResourceFromContext<DrupalNode>(path.jsonapi.resourceName, context)
const fullWidth: boolean = (node.type === 'node--stanford_page' && node.layout_selection?.resourceIdObjMeta?.drupal_internal__target_id === 'stanford_basic_page_full') ||
(node.type === 'node--sul_library' && node.layout_selection?.resourceIdObjMeta?.drupal_internal__target_id === 'sul_library_full_width');
const node = await getResourceFromContext<DrupalNode>(path.jsonapi.resourceName, context,{draftMode})
const fullWidth: boolean = (node?.type === 'node--stanford_page' && node.layout_selection?.resourceIdObjMeta?.drupal_internal__target_id === 'stanford_basic_page_full') ||
(node?.type === 'node--sul_library' && node.layout_selection?.resourceIdObjMeta?.drupal_internal__target_id === 'sul_library_full_width');

return {node, fullWidth}
}

export const generateMetadata = async (context): Promise<Metadata> => {
try {
const {node} = await fetchNodeData(context);
if (!node) return {};

return getNodeMetadata(node);
} catch (e) {
// Probably a 404 or redirect page.
Expand Down Expand Up @@ -81,6 +86,9 @@ const NodePage = async (context) => {

return (
<main id="main-content" className="su-mb-50">
{!node.status &&
<UnpublishedBanner/>
}
<Conditional showWhen={node.type === 'node--sul_library'}>
<LibraryHeader node={node as Library}/>
</Conditional>
Expand Down
5 changes: 3 additions & 2 deletions app/(public)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import Header from "@/components/layout/header";
import {ReactNode} from "react";
import Script from "next/script";
import GoogleAnalytics from "@/components/utils/google-analytics";
import {isDraftMode} from "@/lib/drupal/is-draft-mode";

const Layout = ({children}: { children: ReactNode }) => {

const draftMode = isDraftMode()
return (
<>
{process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID &&
{(!draftMode && process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID) &&
<>
<Script async src="//siteimproveanalytics.com/js/siteanalyze_80352.js"/>
<GoogleAnalytics/>
Expand Down
7 changes: 7 additions & 0 deletions app/api/draft/disable/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {draftMode} from 'next/headers'
import {redirect} from "next/navigation";

export async function GET(request: Request) {
draftMode().disable()
redirect('/');
}
36 changes: 36 additions & 0 deletions app/api/draft/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// route handler with secret and slug
import {draftMode} from 'next/headers'
import {redirect} from 'next/navigation'
import {getResourceByPath} from "@/lib/drupal/get-resource";
import {DrupalNode} from "next-drupal";

export async function GET(request: Request) {
// Parse query string parameters
const {searchParams} = new URL(request.url)
const secret = searchParams.get('secret')
const slug = searchParams.get('slug')

// Check the secret and next parameters
// This secret should only be known to this route handler and the CMS
if (secret !== process.env.DRUPAL_PREVIEW_SECRET || !slug) {
return new Response('Invalid token', {status: 401})
}
// Enable Draft Mode by setting the cookie
draftMode().enable()

if (slug.startsWith('/admin/paragraph')) {
redirect(slug);
}
// Fetch the headless CMS t44343o check if the provided `slug` exists
// getPostBySlug would implement the required fetching logic to the headless CMS
const node = await getResourceByPath<DrupalNode>(slug, {draftMode: true})

// If the slug doesn't exist prevent draft mode from being enabled
if (!node) {
return new Response('Invalid slug', {status: 401})
}

// Redirect to the path from the fetched post
// We don't redirect to searchParams.slug as that might lead to open redirect vulnerabilities
redirect(node.path.alias)
}
5 changes: 3 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import "../styles/globals.css"
import Editori11y from "@/components/editori11y";
import {ReactNode} from "react";
import {Icon} from "next/dist/lib/metadata/types/metadata-types";
import {isDraftMode} from "@/lib/drupal/is-draft-mode";

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 @@ -32,10 +33,10 @@ export const metadata = {
}

const RootLayout = ({children, modal}: { children: ReactNode, modal: ReactNode }) => {

const draftMode = isDraftMode();
return (
<html lang="en">
<Editori11y/>
{draftMode && <Editori11y/>}
<body>
<nav aria-label="Skip link">
<a className="su-skiplink" href="#main-content">Skip to main content</a>
Expand Down
17 changes: 4 additions & 13 deletions components/editori11y.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
"use client"
"use client";

import Script from "next/script";
import {useEffect, useState} from "react";

const Editori11y = () => {
const [addTool, setAddTool] = useState(false)

useEffect(() => {
if (document.cookie.indexOf('addEditoria11y') != -1) setAddTool(true)
}, []);

const startEditoria11y = () => {
// @ts-ignore
Expand All @@ -20,12 +14,9 @@ const Editori11y = () => {
}
}

if (addTool) {
return (
<Script src="//cdn.jsdelivr.net/gh/itmaybejj/editoria11y@2/dist/editoria11y.min.js" onReady={startEditoria11y}/>
)
}
return null;
return (
<Script src="//cdn.jsdelivr.net/gh/itmaybejj/editoria11y@2/dist/editoria11y.min.js" onReady={startEditoria11y}/>
)
}

export default Editori11y;
12 changes: 11 additions & 1 deletion components/layout/library-footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import TwitterIcon from "@/components/patterns/icons/TwitterIcon";
import InstagramIcon from "@/components/patterns/icons/InstagramIcon";
import YoutubeIcon from "@/components/patterns/icons/YoutubeIcon";
import {ReactNode} from "react";
import {isDraftMode} from "@/lib/drupal/is-draft-mode";

const LibraryFooter = () => {
return (
Expand Down Expand Up @@ -36,7 +37,16 @@ const LibraryFooter = () => {
href="/all-locations-and-hours">All locations and hours<ArrowRightIcon className="su-inline-block su-ml-10" width={15}/></Link>
</li>
<li><Link className="su-text-m0 su-text-white hocus:su-text-white su-no-underline hocus:su-underline"
href="/contact-us">Contact us<ArrowRightIcon className="su-inline-block su-ml-10" width={15}/></Link></li>
href="/contact-us">Contact us<ArrowRightIcon className="su-inline-block su-ml-10"
width={15}/></Link></li>
{isDraftMode() &&
<li>
<Link className="su-text-m0 su-text-white hocus:su-text-white su-no-underline hocus:su-underline"
href="/api/draft/disable" prefetch={false}>
Disable Draft Mode
</Link>
</li>
}
</ul>
</div>
<div>
Expand Down
4 changes: 3 additions & 1 deletion components/node/stanford-page/page-display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import {ParagraphRows} from "@/components/paragraph/rows/rows";
import {BasicPage} from "@/lib/drupal/drupal";
import fetchComponents from "@/lib/fetch-components";
import {DrupalParagraph} from "next-drupal";
import {isDraftMode} from "@/lib/drupal/is-draft-mode";

const StanfordPage = async ({node}: { node: BasicPage }) => {
node.su_page_components = await fetchComponents(node.su_page_components ?? []) as DrupalParagraph[];
const draftMode = isDraftMode();
node.su_page_components = await fetchComponents(node.su_page_components ?? [], {draftMode}) as DrupalParagraph[];
node.su_page_components = node.su_page_components.filter(item => item?.id?.length > 0);

const fullWidth = node.layout_selection?.resourceIdObjMeta?.drupal_internal__target_id === 'stanford_basic_page_full';
Expand Down
9 changes: 2 additions & 7 deletions components/paragraph/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Conditional from "@/components/utils/conditional";
import {ExclamationCircleIcon} from "@heroicons/react/20/solid";
import StanfordCard from "@/components/paragraph/stanford-card";
import StanfordBanner from "@/components/paragraph/stanford-banner";
import StanfordImageGallery from "@/components/paragraph/stanford-image-gallery";
Expand All @@ -14,6 +13,7 @@ import SulContactCard from "@/components/paragraph/sul-contact-card";
import SulButton from "@/components/paragraph/sul-button";
import {PropsWithoutRef, useId} from "react";
import SulLibguides from "@/components/paragraph/sul-libguides";
import UnpublishedBanner from "@/components/patterns/unpublished-banner";

interface ParagraphProps extends PropsWithoutRef<any> {
paragraph: any;
Expand All @@ -29,12 +29,7 @@ const Paragraph = ({paragraph, singleRow = false, ...props}: ParagraphProps) =>
return (
<>
<Conditional showWhen={paragraph.status != undefined && !paragraph.status}>
<div className="su-bg-illuminating-light su-py-30 su-mb-20">
<div className="su-centered su-text-m2 su-flex su-gap-lg">
<ExclamationCircleIcon width={40}/>
Unpublished Content
</div>
</div>
<UnpublishedBanner/>
</Conditional>

{paragraph.type === 'paragraph--stanford_card' &&
Expand Down
10 changes: 10 additions & 0 deletions components/patterns/unpublished-banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {ExclamationCircleIcon} from "@heroicons/react/20/solid";

const UnpublishedBanner = () => {
return (
<div className="su-bg-illuminating su-py-10 su-text-3xl su-font-bold">
<div className="su-centered-container su-flex su-gap-10"><ExclamationCircleIcon width={20}/>Unpublished</div>
</div>
)
}
export default UnpublishedBanner
3 changes: 2 additions & 1 deletion lib/drupal/deserialize.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import Jsona from "jsona";
import {TDeserializeOptions, TJsonApiBody} from "jsona/src/JsonaTypes";

const dataFormatter = new Jsona()

export const deserialize = (body, options?) => {
export const deserialize = (body:TJsonApiBody | string, options?: TDeserializeOptions) => {
if (!body) return null
return dataFormatter.deserialize(body, options)
}
13 changes: 8 additions & 5 deletions lib/drupal/get-access-token.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ interface AccessToken {

const CACHE_KEY = "NEXT_DRUPAL_ACCESS_TOKEN"

export async function getAccessToken(): Promise<AccessToken | null> {
if (!process.env.DRUPAL_CLIENT_ID || !process.env.DRUPAL_CLIENT_SECRET) {
return null
export async function getAccessToken(draftMode: boolean = false): Promise<AccessToken | null> {

if (!process.env.DRUPAL_DRAFT_CLIENT || !process.env.DRUPAL_DRAFT_SECRET || !draftMode) {
return null;
}

const cached = cache.get<AccessToken>(CACHE_KEY)
Expand All @@ -20,7 +21,7 @@ export async function getAccessToken(): Promise<AccessToken | null> {
}

const basic = Buffer.from(
`${process.env.DRUPAL_CLIENT_ID}:${process.env.DRUPAL_CLIENT_SECRET}`
`${process.env.DRUPAL_DRAFT_CLIENT}:${process.env.DRUPAL_DRAFT_SECRET}`
).toString("base64")

const response = await fetch(
Expand All @@ -36,7 +37,9 @@ export async function getAccessToken(): Promise<AccessToken | null> {
)

if (!response.ok) {
throw new Error(response.statusText)
console.log('unable to fetch oauth token: '+await response.text());
cache.set(CACHE_KEY, null, 30)
return null;
}

const result: AccessToken = await response.json()
Expand Down
43 changes: 18 additions & 25 deletions lib/drupal/get-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
import {AccessToken, DrupalMenuLinkContent, JsonApiWithLocaleOptions} from "next-drupal/src/types";
import {AccessToken, JsonApiWithLocaleOptions, DrupalMenuLinkContent} from "next-drupal";
import {buildUrl, buildHeaders} from "./utils";
import {deserialize} from "@/lib/drupal/deserialize";

export async function getMenu<T extends DrupalMenuLinkContent>(
export async function getMenu(
name: string,
options?: {
deserialize?: boolean
accessToken?: AccessToken
} & JsonApiWithLocaleOptions
): Promise<{
items: T[]
tree: T[]
}> {
draftMode: boolean
} & JsonApiWithLocaleOptions,
): Promise<{ items: DrupalMenuLinkContent[], tree: DrupalMenuLinkContent[] }> {

options = {
deserialize: true,
draftMode: false,
...options,
}

const localePrefix =
options?.locale && options.locale !== options.defaultLocale
? `/${options.locale}`
: ""

const url = buildUrl(`${localePrefix}/jsonapi/menu_items/${name}`)
const url = buildUrl(`/jsonapi/menu_items/${name}`)

const response = await fetch(url.toString(), {
headers: await buildHeaders(options),
Expand All @@ -34,14 +29,14 @@ export async function getMenu<T extends DrupalMenuLinkContent>(

const data = await response.json()

let items = options.deserialize ? deserialize(data) : data
let items: DrupalMenuLinkContent[] = options.deserialize ? deserialize(data) : data;
items = items.map(item => ({
id: item.id,
title: item.title,
url: item.url,
parent: item.parent,
expanded: item.expanded
}));
} as DrupalMenuLinkContent));
const {items: tree} = buildMenuTree(items)

return {
Expand All @@ -51,10 +46,10 @@ export async function getMenu<T extends DrupalMenuLinkContent>(
}


function buildMenuTree(
function buildMenuTree<T extends DrupalMenuLinkContent>(
links: DrupalMenuLinkContent[],
parent: DrupalMenuLinkContent["id"] = ""
) {
): { items: DrupalMenuLinkContent[] } {
if (!links?.length) {
return {
items: [],
Expand All @@ -63,12 +58,10 @@ function buildMenuTree(

const children = links.filter((link) => link.parent === parent)

return children.length
? {
items: children.map((link) => ({
...link,
...buildMenuTree(links, link.id),
})),
}
: {}
return children.length ? {
items: children.map((link) => ({
...link,
...buildMenuTree(links, link.id),
})),
} : {items: []}
}
Loading

1 comment on commit 93b359d

@vercel
Copy link

@vercel vercel bot commented on 93b359d Nov 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.