Skip to content

Commit

Permalink
DS-916: Split paths and Slug Prefix. (#350)
Browse files Browse the repository at this point in the history
* DS-916: Split paths and Slug Prefix.
  • Loading branch information
sherakama authored Oct 9, 2024
1 parent 320d23f commit ab1e18e
Show file tree
Hide file tree
Showing 22 changed files with 259 additions and 125 deletions.
1 change: 1 addition & 0 deletions .adr-dir
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
docs/decisions
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ Description

Netlify hosted, Next.js built, Storyblok headless CMS site for the Stanford Momentum website.

Documentation and Decision Records
---

You can find Architectural Decision Records and more documentation in the [docs](docs/) & [docs/decisions](docs/decisions/) directories.


Environment variable set up and installation
---

Expand Down
30 changes: 14 additions & 16 deletions app/(storyblok)/[[...slug]]/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React from 'react';
import StoryblokProvider from '@/components/StoryblokProvider';
import {
ISbStoriesParams, getStoryblokApi, storyblokInit, apiPlugin, StoryblokStory, StoryblokClient,
} from '@storyblok/react/rsc';
import { components as Components } from '@/components/StoryblokProvider';
import { resolveRelations } from '@/utilities/resolveRelations';
import ComponentNotFound from '@/components/Storyblok/ComponentNotFound';
import { isProduction } from '@/utilities/getActiveEnv';

// Storyblok bridge options.
const bridgeOptions = {
Expand Down Expand Up @@ -34,30 +36,24 @@ storyblokInit({
* Make sure to not export the below functions otherwise there will be a typescript error
* https://github.com/vercel/next.js/discussions/48724
*/
async function getStoryData(slug = 'page-not-found') {
const activeEnv = process.env.NODE_ENV || 'development';
async function getStoryData(slug = 'momentum/page-not-found') {
const isProd = isProduction();
const storyblokApi: StoryblokClient = getStoryblokApi();
const sbParams: ISbStoriesParams = {
version: activeEnv === 'development' ? 'draft' : 'published',
cv: activeEnv === 'development' ? Date.now() : undefined,
version: isProd ? 'published' : 'draft',
cv: Date.now(),
resolve_relations: resolveRelations,
};

try {
const story = await storyblokApi.get(`cdn/stories/${slug}`, sbParams);
return story;
} catch (error) {
if (typeof error === 'string') {
try {
const parsedError = JSON.parse(error);
if (parsedError.status === 404) {
return { data: 404 };
}
}
catch (e) {
throw error;
}
} catch (error: any) {

if (error && error.status && error.status === 404) {
return { data: 404 };
}

throw error;
}
};
Expand All @@ -75,6 +71,8 @@ export default async function PageNotFound() {
}

return (
<StoryblokStory story={data.story} bridgeOptions={bridgeOptions} />
<StoryblokProvider>
<StoryblokStory story={data.story} bridgeOptions={bridgeOptions} />
</StoryblokProvider>
);
}
60 changes: 46 additions & 14 deletions app/(storyblok)/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import ComponentNotFound from '@/components/Storyblok/ComponentNotFound';
import { notFound } from 'next/navigation';
import getStoryData from '@/utilities/data/getStoryData';
import getStoryList from '@/utilities/data/getStoryList';
import { isProduction } from '@/utilities/getActiveEnv';
import { getSlugPrefix } from '@/utilities/getSlugPrefix';

type PathsType = {
slug: string[];
Expand Down Expand Up @@ -54,15 +56,16 @@ storyblokInit({
* Generate the list of stories to statically render.
*/
export async function generateStaticParams() {
const activeEnv = process.env.NODE_ENV || 'development';
const isProd = isProduction();
// Fetch new content from storyblok.
const storyblokApi: StoryblokClient = getStoryblokApi();
let sbParams: ISbStoriesParams = {
version: activeEnv === 'development' ? 'draft' : 'published',
cv: activeEnv === 'development' ? Date.now() : undefined,
version: isProd ? 'published' : 'draft',
cv: Date.now(),
resolve_links: '0',
resolve_assets: 0,
per_page: 100,
starts_with: getSlugPrefix() + '/',
};

// Use the `cdn/links` endpoint to get a list of all stories without all the extra data.
Expand All @@ -71,13 +74,27 @@ export async function generateStaticParams() {
let paths: PathsType[] = [];

stories.forEach((story) => {

const slug = story.slug;
const splitSlug = slug.split('/');
paths.push({ slug: splitSlug });

// Remove any empty strings.
const cleanSlug = splitSlug.filter((s:string) => s.length);

// Remove the first element which is the prefix.
cleanSlug.shift();

// Ensure there is at least one element
if (cleanSlug.length === 0) {
cleanSlug.push('');
}

paths.push({ slug: cleanSlug });

});

// Add home page as index.
paths.push({ slug: [] });
// Add the home page.
paths.push({ slug: [''] });

return paths;
};
Expand All @@ -86,18 +103,26 @@ export async function generateStaticParams() {
* Generate the SEO metadata for the page.
*/
export async function generateMetadata({ params }: ParamsType): Promise<Metadata> {
const { slug } = params;
try {
const slug = params.slug ? params.slug.join('/') : 'home';
const { data } = await getStoryData({ path: slug });

// Convert the slug to a path.
const slugPath = slug ? slug.join('/') : '';

// Construct the slug for Storyblok.
const prefixedSlug = getSlugPrefix() + '/' + slugPath;

// Get the story data.
const { data } = await getStoryData({ path: prefixedSlug });
if (!data.story || !data.story.content) {
notFound();
}
const blok = data.story.content;
const meta = getPageMetadata({ blok, slug });
const meta = getPageMetadata({ blok, slug: slugPath });
return meta;
}
catch (error) {
console.log('Metadata error:', error, params.slug);
console.log('Metadata error:', error, slug);
}

notFound();
Expand All @@ -107,17 +132,24 @@ export async function generateMetadata({ params }: ParamsType): Promise<Metadata
* Fetch the path data for the page and render it.
*/
export default async function Page({ params }: ParamsType) {
const slug = params.slug ? params.slug.join('/') : 'home';
const { slug } = params;

// Convert the slug to a path.
const slugPath = slug ? slug.join('/') : '';

// Construct the slug for Storyblok.
const prefixedSlug = getSlugPrefix() + '/' + slugPath;

// Get data out of the API.
const { data } = await getStoryData({ path: slug });
const { data } = await getStoryData({ path: prefixedSlug });

// Define an additional data container to pass through server data fetch to client components.
// as everything below the `StoryblokStory` is a client side component.
let extra = {};

// Get additional data for those stories that need it.
if (data?.story?.content?.component === 'sbStoryFilterPage') {
extra = await getStoryList({ path: slug });
extra = await getStoryList({ path: prefixedSlug });
}

// Failed to fetch from API because story slug was not found.
Expand All @@ -131,7 +163,7 @@ export default async function Page({ params }: ParamsType) {
story={data.story}
extra={extra}
bridgeOptions={bridgeOptions}
slug={slug}
slug={slugPath}
name={data.story.name}
/>
);
Expand Down
26 changes: 11 additions & 15 deletions app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React from 'react';
import StoryblokProvider from '@/components/StoryblokProvider';
import {
ISbStoriesParams, getStoryblokApi, storyblokInit, apiPlugin, StoryblokStory, StoryblokClient,
} from '@storyblok/react/rsc';
import { components as Components } from '@/components/StoryblokProvider';
import { resolveRelations } from '@/utilities/resolveRelations';
import ComponentNotFound from '@/components/Storyblok/ComponentNotFound';
import { isProduction } from '@/utilities/getActiveEnv';

// Storyblok bridge options.
const bridgeOptions = {
Expand Down Expand Up @@ -34,30 +36,24 @@ storyblokInit({
* Make sure to not export the below functions otherwise there will be a typescript error
* https://github.com/vercel/next.js/discussions/48724
*/
async function getStoryData(slug = 'page-not-found') {
const activeEnv = process.env.NODE_ENV || 'development';
async function getStoryData(slug = 'momentum/page-not-found') {
const isProd = isProduction();
const storyblokApi: StoryblokClient = getStoryblokApi();
const sbParams: ISbStoriesParams = {
version: activeEnv === 'development' ? 'draft' : 'published',
cv: activeEnv === 'development' ? Date.now() : undefined,
version: isProd ? 'published' : 'draft',
cv: Date.now(),
resolve_relations: resolveRelations,
};

try {
const story = await storyblokApi.get(`cdn/stories/${slug}`, sbParams);
return story;
} catch (error) {
if (typeof error === 'string') {
try {
const parsedError = JSON.parse(error);
if (parsedError.status === 404) {
return { data: 404 };
}
}
catch (e) {
throw error;
}
} catch (error: any) {

if (error && error.status && error.status === 404) {
return { data: 404 };
}

throw error;
}
};
Expand Down
40 changes: 32 additions & 8 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { MetadataRoute } from 'next';
import StoryblokClient from 'storyblok-js-client';
import { ISbStoriesParams } from '@storyblok/react/rsc';
import { isProduction } from '@/utilities/getActiveEnv';
import { getSlugPrefix } from '@/utilities/getSlugPrefix';
import { sbStripSlugURL } from '@/utilities/sbStripSlugUrl';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {

Expand All @@ -12,24 +16,44 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
region: 'us',
});

const isProd = isProduction();
// Fetch new content from storyblok.
let sbParams: ISbStoriesParams = {
version: isProd ? 'published' : 'draft',
cv: Date.now(),
resolve_links: '0',
resolve_assets: 0,
per_page: 100,
starts_with: getSlugPrefix() + '/',
};

// Fetch all the stories from SB.
// We use the `cdn/stories` endpoint because it has the last published time which `cdn/links` does not.
const response = await storyblokClient.getAll('cdn/stories', {
version: 'published',
cv: Date.now(),
});
const response = await storyblokClient.getAll('cdn/stories', sbParams);

// Exclude any stories with noindex set to true and those inside the Global Components or Test folders in Storyblok
const indexStories = response.filter((story) => (!story.content?.noindex) && !story.full_slug?.startsWith('global-components/') && !story.full_slug?.startsWith('test/'));
const indexStories = response.filter(
(story) => {
if (story.content?.noindex) {
return false;
}

if (story.full_slug.includes('/global-components/') || story.full_slug.includes('/test/')) {
return false;
}

return true;
},
);
const currentURL = process.env.URL || process.env.DEPLOY_PRIME_URL || 'https://momentum.stanford.edu';

const ret = indexStories.map((story) => {
const url = story.path ? `${currentURL}/${story.path}` : `${currentURL}/${story.full_slug}`;
const url = `${currentURL}/${sbStripSlugURL(story.full_slug)}`;
return {
url: url.replace(/\/+$/, ''),
lastModified: new Date(story.published_at),
changeFrequency: 'daily' as const, // Added in 13.4.5
priority: 0.5, // Added in 13.4.5
changeFrequency: 'daily' as const,
priority: 0.5,
};
});

Expand Down
8 changes: 0 additions & 8 deletions components/Cta/CtaLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,6 @@ export const CtaLink = React.forwardRef<HTMLAnchorElement, CtaLinkProps>(
if (isInternal) {
myLink = cachedUrl || href;

if (myLink === 'home') {
myLink = '';
}

if (!myLink?.startsWith('/')) {
myLink = `/${myLink}`;
}

if (anchor) {
myLink = `${myLink}#${anchor}`;
}
Expand Down
18 changes: 17 additions & 1 deletion components/Cta/CtaNextLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { CtaContent } from './CtaContent';
import { type CtaCommonProps } from './Cta.types';
import { marginTops, marginBottoms } from '@/utilities/datasource';
import * as styles from './Cta.styles';
import { getSlugPrefix } from '@/utilities/getSlugPrefix';

export type CtaNextLinkProps = CtaCommonProps & LinkProps & {
target?: React.HTMLAttributeAnchorTarget;
Expand All @@ -32,11 +33,26 @@ export const CtaNextLink = React.forwardRef<HTMLAnchorElement, CtaNextLinkProps>
...rest
} = props;

// Normalize the href and strip the slug prefix.
const prefix = getSlugPrefix();
const path = href.toString();
const hrefParts = path.split('/');

// Remove empty strings from the array.
const cleanParts = hrefParts.filter((s:string) => s.length);

// If the first part of the URL is the slug prefix, remove it.
if (cleanParts[0] === prefix) {
cleanParts.shift();
}

const strippedHref = `/${cleanParts.join('/')}`;

return (
<Link
{...rest}
ref={ref}
href={href}
href={strippedHref}
target={target}
className={cnb(
styles.cta,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRef, useState } from 'react';
import React, { useRef, useState } from 'react';
import { storyblokEditable, type SbBlokData, type ISbStoryData } from '@storyblok/react/rsc';
import { type StoryblokRichtext } from 'storyblok-rich-text-react-renderer-ts';
import { Container } from '@/components/Container';
Expand Down
4 changes: 3 additions & 1 deletion components/Storyblok/SbStoryListNav/SbStoryListNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { FlexBox } from '@/components/FlexBox';
import { Heading } from '@/components/Typography';
import { HeroIcon } from '@/components/HeroIcon';
import * as styles from './SbStoryListNav.styles';
import { sbStripSlugURL } from '@/utilities/sbStripSlugUrl';

type SbStoryListNavType = {
blok: {
Expand All @@ -26,7 +27,8 @@ const StoryListContent = ({ blok: { links }, fullSlug }: SbStoryListNavType) =>
return (
<ul className={styles.list}>
{links.map((link) => {
const isCurrentPage = fullSlug === link.link?.cached_url || (fullSlug === 'stories' && link.link?.cached_url === 'stories/');
const cached_url = link.link?.cached_url ? sbStripSlugURL(link.link.cached_url) : '';
const isCurrentPage = fullSlug === cached_url || (fullSlug === 'stories' && cached_url === 'stories/');
return (
<li key={link._uid} className={styles.listItem}>
<CtaLink
Expand Down
Loading

0 comments on commit ab1e18e

Please sign in to comment.