Merge pull request #33 from yougotwill/dynamic-performance
Blog routes now use ISR
This commit is contained in:
commit
d135a4ef07
|
@ -0,0 +1,53 @@
|
|||
import Head from 'next/head';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { PageType, setPageType, setPostTitle } from '../state/navigation';
|
||||
import { IPost } from '../types/cms';
|
||||
import { generateTitle } from '../utils/metadata';
|
||||
|
||||
import { Article } from '../components/article/Article';
|
||||
|
||||
// Parallax on bg as mouse moves
|
||||
export default function BlogPost({ post, url }: { post: IPost; url: string }) {
|
||||
const dispatch = useDispatch();
|
||||
const pageTitle = generateTitle(post?.title);
|
||||
const imageURL = post?.featureImage?.imageUrl;
|
||||
|
||||
useEffect(() => {
|
||||
if (post) {
|
||||
dispatch(setPageType(PageType.POST));
|
||||
dispatch(setPostTitle(post.title));
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{pageTitle}</title>
|
||||
<meta name="description" content={post?.description}></meta>
|
||||
<meta property="og:title" content={pageTitle} key="ogtitle" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={post?.description}
|
||||
key="ogdesc"
|
||||
/>
|
||||
<meta property="og:type" content="article" />
|
||||
<meta name="image_src" content={imageURL} />
|
||||
<meta name="image_url" content={imageURL} />
|
||||
<meta name="keywords" content={post?.tags?.join(' ')} />
|
||||
<meta property="og:image" content={imageURL} key="ogimage" />
|
||||
<meta property="og:url" content={url} />
|
||||
<link rel="canonical" href={url}></link>{' '}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={pageTitle} />
|
||||
<meta name="twitter:description" content={post?.description} />
|
||||
<meta name="twitter:image" content={imageURL} />
|
||||
</Head>
|
||||
|
||||
<div className="bg-alt">
|
||||
<Article {...post} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import { useContext, ReactElement } from 'react';
|
||||
import ReactPaginate from 'react-paginate';
|
||||
|
||||
import { ScreenContext } from '../contexts/screen';
|
||||
import { Contained } from '../components/Contained';
|
||||
|
||||
interface Props {
|
||||
currentPage: number;
|
||||
pageCount: number;
|
||||
paginationHandler: (page: any) => void;
|
||||
}
|
||||
|
||||
export default function Pagination(props: Props): ReactElement {
|
||||
const { currentPage, pageCount, paginationHandler } = props;
|
||||
const { isMobile } = useContext(ScreenContext);
|
||||
|
||||
// Mobile Pagination Settings
|
||||
const MAX_PAGINATION_BUTTONS_MOBILE = 5; // On mobile, we want to only have a maximum of 5 pagination buttons
|
||||
const hasTooManyButtons =
|
||||
isMobile && pageCount > MAX_PAGINATION_BUTTONS_MOBILE; // Check if the maximum number of pagination buttons have been reached
|
||||
const EDGE_PAGES = [1, 2, pageCount - 1, pageCount]; // Check if the current page is an edge page, to keep the number of pagination buttons consistent
|
||||
const isEdgePage = isMobile && EDGE_PAGES.includes(currentPage);
|
||||
|
||||
return (
|
||||
<Contained>
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="mt-6 tablet:mt-4">
|
||||
<ReactPaginate
|
||||
previousLabel={'<'}
|
||||
nextLabel={'>'}
|
||||
breakLabel={'...'}
|
||||
breakClassName={'break-me'}
|
||||
activeClassName={'active bg-secondary'}
|
||||
containerClassName={'pagination bg-primary text-white'}
|
||||
initialPage={currentPage - 1}
|
||||
pageCount={pageCount}
|
||||
marginPagesDisplayed={hasTooManyButtons && !isEdgePage ? 1 : 2}
|
||||
pageRangeDisplayed={isMobile ? 1 : 2}
|
||||
onPageChange={paginationHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Contained>
|
||||
);
|
||||
}
|
|
@ -76,16 +76,33 @@ export function RichBody(props: Props): ReactElement {
|
|||
// const children;
|
||||
const plaintext = documentToPlainTextString(node);
|
||||
const isShortcode = CMS.SHORTCODE_REGEX.test(plaintext);
|
||||
|
||||
return isShortcode ? (
|
||||
renderShortcode(plaintext)
|
||||
) : (
|
||||
<p
|
||||
className={classNames('mb-3 font-sans tracking-wide text-justify')}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
if (isShortcode) {
|
||||
return renderShortcode(plaintext);
|
||||
} else {
|
||||
let hasImage = false;
|
||||
Children.map(children, (child: any) => {
|
||||
if (child.type === 'figure') {
|
||||
hasImage = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
if (hasImage) {
|
||||
return (
|
||||
<span className={classNames('leading-relaxed pb-6')}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<p
|
||||
className={classNames(
|
||||
'mb-3 font-sans tracking-wide text-justify',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
},
|
||||
[BLOCKS.HEADING_1]: (node, children) => (
|
||||
<h1
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { PageType, setPageType } from '../state/navigation';
|
||||
import { ISplitPage } from '../types/cms';
|
||||
import { generateTitle, generateURL } from '../utils/metadata';
|
||||
|
||||
import { Contained } from '../components/Contained';
|
||||
import { RichBody } from '../components/RichBody';
|
||||
|
||||
export default function RichPage({
|
||||
page,
|
||||
href,
|
||||
}: {
|
||||
page: ISplitPage;
|
||||
href: string;
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const pageTitle = generateTitle(page?.label);
|
||||
const pageDescription = page?.title;
|
||||
const pageURL = generateURL(href);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setPageType(PageType.NORMAL));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{pageTitle}</title>
|
||||
<meta name="description" content={pageDescription}></meta>
|
||||
<meta property="og:title" content={pageTitle} key="ogtitle" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={pageDescription}
|
||||
key="ogdesc"
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:image"
|
||||
content={page?.hero?.imageUrl}
|
||||
key="ogimage"
|
||||
/>
|
||||
<meta property="og:url" content={pageURL} />
|
||||
|
||||
<link rel="canonical" href={pageURL}></link>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={pageTitle} />
|
||||
<meta name="twitter:description" content={pageDescription} />
|
||||
<meta name="twitter:image" content={page?.hero?.imageUrl} />
|
||||
</Head>
|
||||
|
||||
<div className="bg-alt">
|
||||
<div className="relative flex items-center justify-center w-full h-full pt-3 bg-gradient-to-bl from-hyper to-blush">
|
||||
<img
|
||||
style={{ maxHeight: '33vh' }}
|
||||
src={page?.hero?.imageUrl}
|
||||
className="object-contain w-full"
|
||||
alt={page?.hero?.description ?? pageTitle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Contained>
|
||||
<h1 className="mt-12 mb-4 text-4xl font-bold leading-none text-primary font-prompt">
|
||||
{page?.title}
|
||||
</h1>
|
||||
|
||||
<div className="mb-10">
|
||||
<RichBody body={page?.body} />
|
||||
</div>
|
||||
</Contained>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -4,11 +4,12 @@ import React from 'react';
|
|||
interface Props {
|
||||
tag: string;
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
classes?: string;
|
||||
}
|
||||
|
||||
export function TagBlock(props: Props) {
|
||||
const { tag, size = 'small' } = props;
|
||||
const href = `/blog?tag=${tag.toLowerCase()}`;
|
||||
const { tag, size = 'small', classes } = props;
|
||||
const href = `/tag/${tag.toLowerCase()}`;
|
||||
|
||||
return (
|
||||
<a
|
||||
|
@ -19,6 +20,7 @@ export function TagBlock(props: Props) {
|
|||
size === 'small' && 'h-4 text-xs',
|
||||
size === 'medium' && 'h-5 text-sm',
|
||||
size === 'medium' && 'h-6 text-base',
|
||||
classes,
|
||||
)}
|
||||
>
|
||||
<span className="px-2">{tag.toLowerCase()}</span>
|
||||
|
|
|
@ -11,6 +11,7 @@ export interface ISideMenuItem {
|
|||
label: string;
|
||||
href: string;
|
||||
isExternal?: boolean;
|
||||
hasOwnRoute?: boolean;
|
||||
shouldHide?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -61,11 +61,13 @@ const SIDE_MENU_ITEMS = {
|
|||
id: 8,
|
||||
label: "Oxen's 2021 roadmap",
|
||||
href: '/roadmap',
|
||||
hasOwnRoute: true,
|
||||
},
|
||||
[SideMenuItem.FAQ]: {
|
||||
id: 9,
|
||||
label: 'FAQ',
|
||||
href: '/faq',
|
||||
hasOwnRoute: true,
|
||||
},
|
||||
} as { [name: string]: ISideMenuItem };
|
||||
|
||||
|
@ -79,7 +81,7 @@ const MENU_ITEMS: IMenuItem[] = [
|
|||
},
|
||||
{
|
||||
label: 'Dev Updates',
|
||||
href: '/blog?tag=dev-update',
|
||||
href: '/tag/dev-update',
|
||||
newTab: false,
|
||||
subtle: true,
|
||||
external: false,
|
||||
|
@ -125,7 +127,7 @@ const MENU_ITEMS: IMenuItem[] = [
|
|||
const NAVIGATION = {
|
||||
MENU_ITEMS,
|
||||
SIDE_MENU_ITEMS,
|
||||
BLOG_REGEX: /^\/(blog)([?tag=[\w-]*)?([?&]page=[0-9]{1,3})?/,
|
||||
BLOG_REGEX: /^\/(blog|tag)/,
|
||||
POST_REGEX: /^\/(blog\/)(([\w-]{1,100})|(\[slug\]))$/,
|
||||
};
|
||||
|
||||
|
|
|
@ -41,10 +41,15 @@ const nextConfig = {
|
|||
destination: '/api/feed/rss',
|
||||
},
|
||||
{
|
||||
// The /:slug part is a generic parameter handler to catch all other cases
|
||||
source: '/feed/:slug',
|
||||
destination: '/api/feed/:slug',
|
||||
},
|
||||
// Redirects blog posts i.e. https://oxen.io/blog/hello or https://oxen.io/blog/hello-world
|
||||
// Ignores page results i.e. https://oxen.io/blog/1
|
||||
{
|
||||
source: '/blog/:slug((?:[\\w]{1,}[\\-]{1,}).*|[\\D]{1,})',
|
||||
destination: '/:slug',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
|
|
@ -14,9 +14,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.2.2",
|
||||
"@contentful/rich-text-html-renderer": "^15.0.0",
|
||||
"@contentful/rich-text-plain-text-renderer": "^15.0.0",
|
||||
"@contentful/rich-text-react-renderer": "^15.0.0",
|
||||
"@contentful/rich-text-html-renderer": "^15.3.5",
|
||||
"@contentful/rich-text-plain-text-renderer": "^15.3.5",
|
||||
"@contentful/rich-text-react-renderer": "^15.3.5",
|
||||
"@tailwindcss/aspect-ratio": "^0.2.0",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/lodash": "^4.14.161",
|
||||
|
@ -64,7 +64,7 @@
|
|||
"@babel/core": "^7.12.17",
|
||||
"@babel/preset-env": "^7.12.17",
|
||||
"@babel/preset-react": "^7.12.13",
|
||||
"@contentful/rich-text-types": "^15.0.0",
|
||||
"@contentful/rich-text-types": "^15.3.5",
|
||||
"@tailwindcss/forms": "^0.3.3",
|
||||
"@types/base-64": "^1.0.0",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
|
|
177
pages/[page].tsx
177
pages/[page].tsx
|
@ -1,111 +1,94 @@
|
|||
// [slug].js
|
||||
import Head from 'next/head';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Contained } from '../components/Contained';
|
||||
import { RichBody } from '../components/RichBody';
|
||||
import { GetStaticPaths } from 'next';
|
||||
|
||||
import { CMS, NAVIGATION } from '../constants';
|
||||
import { CmsApi, unslugify } from '../services/cms';
|
||||
import { PageType, setPageType, SideMenuItem } from '../state/navigation';
|
||||
import { ISplitPage } from '../types/cms';
|
||||
import { generateTitle, generateURL } from '../utils/metadata';
|
||||
import { CmsApi, generateLinkMeta, unslugify } from '../services/cms';
|
||||
import { SideMenuItem } from '../state/navigation';
|
||||
import { IPath } from '../types';
|
||||
import { IPost, ISplitPage, isPost } from '../types/cms';
|
||||
import { isLocal } from '../utils/links';
|
||||
|
||||
interface IPath {
|
||||
params: { page: string };
|
||||
}
|
||||
import BlogPost from '../components/BlogPost';
|
||||
import RichPage from '../components/RichPage';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
// Get paths to all pages
|
||||
// Hardcoded in navigation constants.
|
||||
// Contentful can edit entries but cannot add/remove
|
||||
// without touching code.
|
||||
const paths: IPath[] = Object.values(NAVIGATION.SIDE_MENU_ITEMS).map(
|
||||
item => ({
|
||||
params: { page: item.href },
|
||||
}),
|
||||
);
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
// Get paths to all pages stored in navigation constants.
|
||||
// Contentful can edit entries but cannot add/remove without touching code.
|
||||
const navigationPaths: IPath[] = Object.values(NAVIGATION.SIDE_MENU_ITEMS)
|
||||
.filter(item => {
|
||||
return item.hasOwnRoute === undefined && isLocal(item.href);
|
||||
})
|
||||
.map(item => ({
|
||||
params: { page: item.href.slice(1) },
|
||||
}));
|
||||
|
||||
return { paths, fallback: true };
|
||||
}
|
||||
const cms = new CmsApi();
|
||||
const posts: IPost[] = [];
|
||||
let currentPage = 1;
|
||||
let foundAllPosts = false;
|
||||
|
||||
// Contentful only allows 100 at a time
|
||||
while (!foundAllPosts) {
|
||||
const { entries: _posts } = await cms.fetchBlogEntries(100, currentPage);
|
||||
|
||||
if (_posts.length === 0) {
|
||||
foundAllPosts = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
posts.push(..._posts);
|
||||
currentPage++;
|
||||
}
|
||||
|
||||
const postPaths: IPath[] = posts.map(post => ({
|
||||
params: { page: post.slug },
|
||||
}));
|
||||
|
||||
return { paths: [...navigationPaths, ...postPaths], fallback: 'blocking' };
|
||||
};
|
||||
|
||||
export async function getStaticProps({ params }) {
|
||||
console.log(`Building Page %c${params.page}`, 'color: purple;');
|
||||
const href = params?.page ?? '';
|
||||
const id = unslugify(String(href));
|
||||
|
||||
const cms = new CmsApi();
|
||||
const page = await cms.fetchPageById(SideMenuItem[id] ?? '');
|
||||
try {
|
||||
const cms = new CmsApi();
|
||||
let page: ISplitPage | IPost;
|
||||
|
||||
if (!page) {
|
||||
return { notFound: true };
|
||||
if (SideMenuItem[id]) {
|
||||
page = await cms.fetchPageById(SideMenuItem[id]);
|
||||
} else {
|
||||
page = await cms.fetchEntryBySlug(href, 'post');
|
||||
// embedded links in post body need metadata for preview
|
||||
page.body = await generateLinkMeta(page.body);
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
page,
|
||||
href: `/${href}`,
|
||||
},
|
||||
revalidate: CMS.CONTENT_REVALIDATE_RATE,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return {
|
||||
notFound: true,
|
||||
revalidate: CMS.CONTENT_REVALIDATE_RATE,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
page,
|
||||
href: `/${href}`,
|
||||
},
|
||||
revalidate: CMS.CONTENT_REVALIDATE_RATE,
|
||||
};
|
||||
}
|
||||
|
||||
function Page({ page, href }: { page: ISplitPage | null; href: string }) {
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
dispatch(setPageType(PageType.NORMAL));
|
||||
}, []);
|
||||
|
||||
const pageTitle = generateTitle(page?.label);
|
||||
const pageDescription = page?.title;
|
||||
const pageURL = generateURL(href);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{pageTitle}</title>
|
||||
<meta name="description" content={pageDescription}></meta>
|
||||
<meta property="og:title" content={pageTitle} key="ogtitle" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={pageDescription}
|
||||
key="ogdesc"
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:image"
|
||||
content={page?.hero?.imageUrl}
|
||||
key="ogimage"
|
||||
/>
|
||||
<meta property="og:url" content={pageURL} />
|
||||
|
||||
<link rel="canonical" href={pageURL}></link>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={pageTitle} />
|
||||
<meta name="twitter:description" content={pageDescription} />
|
||||
<meta name="twitter:image" content={page?.hero?.imageUrl} />
|
||||
</Head>
|
||||
|
||||
<div className="bg-alt">
|
||||
<div className="relative flex items-center justify-center w-full h-full pt-3 bg-gradient-to-bl from-hyper to-blush">
|
||||
<img
|
||||
style={{ maxHeight: '33vh' }}
|
||||
src={page?.hero?.imageUrl}
|
||||
className="object-contain w-full"
|
||||
alt={page?.hero?.description ?? pageTitle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Contained>
|
||||
<h1 className="mt-12 mb-4 text-4xl font-bold leading-none text-primary font-prompt">
|
||||
{page?.title}
|
||||
</h1>
|
||||
|
||||
<div className="mb-10">
|
||||
<RichBody body={page?.body} />
|
||||
</div>
|
||||
</Contained>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
export default function Page({
|
||||
page,
|
||||
href,
|
||||
}: {
|
||||
page: ISplitPage | IPost;
|
||||
href: string;
|
||||
}) {
|
||||
if (isPost(page)) {
|
||||
return <BlogPost post={page} url={href} />;
|
||||
} else {
|
||||
return <RichPage page={page} href={href} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default Page;
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
import { useEffect, ReactElement } from 'react';
|
||||
import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { CMS, METADATA } from '../../constants';
|
||||
import { CmsApi } from '../../services/cms';
|
||||
import { PageType, setPageType } from '../../state/navigation';
|
||||
import { IPath } from '../../types';
|
||||
import { IPost } from '../../types/cms';
|
||||
import { generateTitle } from '../../utils/metadata';
|
||||
|
||||
import { ArticleCard } from '../../components/cards/ArticleCard';
|
||||
import { ArticleCardFeature } from '../../components/cards/ArticleCardFeature';
|
||||
import { CardGrid } from '../../components/cards/CardGrid';
|
||||
import { Contained } from '../../components/Contained';
|
||||
import Pagination from '../../components/Pagination';
|
||||
|
||||
interface Props {
|
||||
posts: IPost[];
|
||||
currentPage: number;
|
||||
pageCount: number;
|
||||
}
|
||||
|
||||
export default function Blog(props: Props): ReactElement {
|
||||
const {
|
||||
posts: [featuredPost, ...otherPosts],
|
||||
currentPage,
|
||||
pageCount,
|
||||
} = props;
|
||||
const router = useRouter();
|
||||
const dispatch = useDispatch();
|
||||
const pageTitle = generateTitle('Blog');
|
||||
const featuredImageURL = featuredPost?.featureImage?.imageUrl;
|
||||
|
||||
const paginationHandler = page => {
|
||||
const newRoute = `/blog/${page.selected + 1}`;
|
||||
router.push(newRoute);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setPageType(PageType.BLOG));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{pageTitle}</title>
|
||||
<meta name="description" content={METADATA.BLOG.DESCRIPTION}></meta>
|
||||
<meta property="og:title" content={pageTitle} key="ogtitle" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={METADATA.BLOG.DESCRIPTION}
|
||||
key="ogdesc"
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content={featuredImageURL} key="ogimage" />
|
||||
<meta property="og:url" content={METADATA.BLOG.URL} />
|
||||
<link rel="canonical" href={METADATA.BLOG.URL}></link>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={pageTitle} />
|
||||
<meta name="twitter:description" content={METADATA.BLOG.DESCRIPTION} />
|
||||
<meta name="twitter:image" content={featuredImageURL} />
|
||||
</Head>
|
||||
|
||||
<div className="flex flex-col w-full mt-12 mb-6 space-y-6 bg-alt">
|
||||
<Contained classes={'mb-6'}>
|
||||
<h1 className="mb-2 text-4xl font-medium uppercase font-prompt">
|
||||
Oxen Blogs
|
||||
</h1>
|
||||
{featuredPost && <ArticleCardFeature {...featuredPost} />}
|
||||
</Contained>
|
||||
|
||||
<CardGrid>
|
||||
{otherPosts?.map(post => (
|
||||
<ArticleCard key={post.id} {...post} />
|
||||
))}
|
||||
</CardGrid>
|
||||
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
paginationHandler={paginationHandler}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async (
|
||||
context: GetStaticPropsContext,
|
||||
) => {
|
||||
console.log(
|
||||
`Building Blog posts page %c${
|
||||
context.params.page ? context.params.page[0] : ''
|
||||
}`,
|
||||
'color: purple;',
|
||||
);
|
||||
|
||||
const cms = new CmsApi();
|
||||
const page = context.params.page ? Number(context.params.page[0]) : 1;
|
||||
|
||||
try {
|
||||
const {
|
||||
entries: posts,
|
||||
total,
|
||||
} = await cms.fetchBlogEntriesWithoutDevUpdates(
|
||||
CMS.BLOG_RESULTS_PER_PAGE,
|
||||
page,
|
||||
);
|
||||
|
||||
const pageCount = Math.ceil(total / CMS.BLOG_RESULTS_PER_PAGE);
|
||||
|
||||
if (page > pageCount && page > 1) {
|
||||
throw 'Page results exceeded!';
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
posts,
|
||||
pageCount,
|
||||
currentPage: page,
|
||||
},
|
||||
revalidate: CMS.CONTENT_REVALIDATE_RATE,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return {
|
||||
notFound: true,
|
||||
revalidate: CMS.CONTENT_REVALIDATE_RATE,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const cms = new CmsApi();
|
||||
|
||||
const { entries, total } = await cms.fetchBlogEntriesWithoutDevUpdates();
|
||||
const pageCount = Math.ceil(total / CMS.BLOG_RESULTS_PER_PAGE);
|
||||
const paths: IPath[] = [];
|
||||
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
paths.push({ params: { page: [String(i)] } });
|
||||
}
|
||||
|
||||
return { paths, fallback: 'blocking' };
|
||||
};
|
|
@ -1,108 +0,0 @@
|
|||
// [slug].js
|
||||
import Head from 'next/head';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Article } from '../../components/article/Article';
|
||||
import { CMS } from '../../constants';
|
||||
import { CmsApi, generateLinkMeta } from '../../services/cms';
|
||||
import { PageType, setPageType, setPostTitle } from '../../state/navigation';
|
||||
import { IPost } from '../../types/cms';
|
||||
import { generateTitle, generateURL } from '../../utils/metadata';
|
||||
|
||||
interface IPath {
|
||||
params: { slug: string };
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const cms = new CmsApi();
|
||||
let posts: IPost[] = [];
|
||||
let page = 1;
|
||||
let foundAllPosts = false;
|
||||
|
||||
// Contentful only allows 100 at a time
|
||||
while (!foundAllPosts) {
|
||||
const { entries: _posts } = await cms.fetchBlogEntries(100, page);
|
||||
|
||||
if (_posts.length === 0) {
|
||||
foundAllPosts = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
posts = [...posts, ..._posts];
|
||||
page++;
|
||||
}
|
||||
|
||||
const paths: IPath[] = posts.map(item => ({
|
||||
params: { slug: item.slug },
|
||||
}));
|
||||
|
||||
return { paths, fallback: true };
|
||||
}
|
||||
|
||||
export async function getStaticProps({ params }) {
|
||||
console.log(`Building page: %c${params.slug}`, 'color: purple;');
|
||||
|
||||
const cms = new CmsApi();
|
||||
const post = await cms.fetchEntryBySlug(String(params?.slug) ?? '', 'post');
|
||||
// embedded links in post body need metadata for preview
|
||||
post.body = await generateLinkMeta(post.body);
|
||||
const url = generateURL(params?.slug ? `/blog/${params?.slug}` : '/blog');
|
||||
if (!post) {
|
||||
return { notFound: true };
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
post,
|
||||
url,
|
||||
},
|
||||
revalidate: CMS.CONTENT_REVALIDATE_RATE,
|
||||
};
|
||||
}
|
||||
|
||||
// Parallax on bg as mouse moves
|
||||
function Post({ post, url }: { post: IPost; url: string }) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (post) {
|
||||
dispatch(setPageType(PageType.POST));
|
||||
dispatch(setPostTitle(post.title));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const pageTitle = generateTitle(post?.title);
|
||||
const imageURL = post?.featureImage?.imageUrl;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{pageTitle}</title>
|
||||
<meta name="description" content={post?.description}></meta>
|
||||
<meta property="og:title" content={pageTitle} key="ogtitle" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={post?.description}
|
||||
key="ogdesc"
|
||||
/>
|
||||
<meta property="og:type" content="article" />
|
||||
<meta name="image_src" content={imageURL} />
|
||||
<meta name="image_url" content={imageURL} />
|
||||
<meta name="keywords" content={post?.tags?.join(' ')} />
|
||||
<meta property="og:image" content={imageURL} key="ogimage" />
|
||||
<meta property="og:url" content={url} />
|
||||
<link rel="canonical" href={url}></link>{' '}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={pageTitle} />
|
||||
<meta name="twitter:description" content={post?.description} />
|
||||
<meta name="twitter:image" content={imageURL} />
|
||||
</Head>
|
||||
|
||||
<div className="bg-alt">
|
||||
<Article {...post} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Post;
|
|
@ -1,222 +0,0 @@
|
|||
import { GetServerSideProps } from 'next';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import ReactPaginate from 'react-paginate';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { ArticleCard } from '../../components/cards/ArticleCard';
|
||||
import { ArticleCardFeature } from '../../components/cards/ArticleCardFeature';
|
||||
import { CardGrid } from '../../components/cards/CardGrid';
|
||||
import { Contained } from '../../components/Contained';
|
||||
import { TagBlock } from '../../components/TagBlock';
|
||||
import { CMS, METADATA } from '../../constants';
|
||||
import { CmsApi } from '../../services/cms';
|
||||
import { PageType, setPageType } from '../../state/navigation';
|
||||
import { IPost } from '../../types/cms';
|
||||
import { generateTitle } from '../../utils/metadata';
|
||||
import { ScreenContext } from '../../contexts/screen';
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async context => {
|
||||
const cms = new CmsApi();
|
||||
|
||||
// Get tag query
|
||||
const tag = String(context.query.tag ?? '') ?? null;
|
||||
const page = Math.ceil(Number(context.query.page ?? 1));
|
||||
|
||||
const RESULTS_PER_PAGE = tag
|
||||
? CMS.BLOG_RESULTS_PER_PAGE_TAGGED
|
||||
: CMS.BLOG_RESULTS_PER_PAGE;
|
||||
|
||||
// Fetch posts even when tag, for related etc
|
||||
// Pagination only occurs when tag isnt defined.
|
||||
// If tag is defined, pagination is for tag results
|
||||
const { entries: posts, total: totalPosts } = await cms.fetchBlogEntries(
|
||||
RESULTS_PER_PAGE,
|
||||
tag ? 1 : page,
|
||||
);
|
||||
|
||||
// Get tags for pagination
|
||||
let tagPosts = [];
|
||||
let tagTotalPosts;
|
||||
let filteredPosts = posts;
|
||||
let filteredTotalPosts = totalPosts;
|
||||
if (tag) {
|
||||
const {
|
||||
entries: _tagPosts = [],
|
||||
total: _tagTotalPosts,
|
||||
} = await cms.fetchBlogEntriesByTag(tag ?? '', RESULTS_PER_PAGE, page);
|
||||
tagPosts = _tagPosts;
|
||||
tagTotalPosts = _tagTotalPosts;
|
||||
} else {
|
||||
// Retrieve all blog posts without the `dev-update` tag when not searching by tag
|
||||
const {
|
||||
entries: _tagPosts = [],
|
||||
total: _tagTotalPosts,
|
||||
} = await cms.fetchBlogEntriesWithoutDevUpdates(RESULTS_PER_PAGE, page);
|
||||
|
||||
filteredPosts = _tagPosts;
|
||||
filteredTotalPosts = _tagTotalPosts;
|
||||
}
|
||||
|
||||
const total = tagTotalPosts ?? filteredTotalPosts;
|
||||
const pageCount = Math.ceil(total / RESULTS_PER_PAGE);
|
||||
|
||||
return {
|
||||
props: {
|
||||
posts: filteredPosts,
|
||||
pageCount,
|
||||
currentPage: page,
|
||||
tag,
|
||||
tagPosts,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
interface Props {
|
||||
posts: IPost[];
|
||||
tagPosts: IPost[];
|
||||
tag: string | null;
|
||||
currentPage: number;
|
||||
pageCount: number;
|
||||
}
|
||||
|
||||
const Blog = (props: Props) => {
|
||||
const { posts, tagPosts, tag, currentPage, pageCount } = props;
|
||||
const { isMobile } = useContext(ScreenContext);
|
||||
|
||||
const router = useRouter();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setPageType(PageType.BLOG));
|
||||
}, []);
|
||||
|
||||
const tagHasPosts = tagPosts && tagPosts?.length > 0;
|
||||
const [featuredPost, ...otherPosts] = posts;
|
||||
const showPagination = posts.length > 0 && pageCount > 1;
|
||||
|
||||
console.log('index ➡️ tag:', tag);
|
||||
console.log('index ➡️ tagHasPosts:', tagHasPosts);
|
||||
|
||||
const paginationHandler = page => {
|
||||
const currentPath = router.pathname;
|
||||
|
||||
// Copy current query to avoid its removing
|
||||
const currentQuery = { ...router.query };
|
||||
currentQuery.page = page.selected + 1;
|
||||
|
||||
router.push({
|
||||
pathname: currentPath,
|
||||
query: currentQuery,
|
||||
});
|
||||
};
|
||||
|
||||
// Mobile Pagination Settings
|
||||
const MAX_PAGINATION_BUTTONS_MOBILE = 5; // On mobile, we want to only have a maximum of 5 pagination buttons
|
||||
const hasTooManyButtons =
|
||||
isMobile && pageCount > MAX_PAGINATION_BUTTONS_MOBILE; // Check if the maximum number of pagination buttons have been reached
|
||||
const EDGE_PAGES = [1, 2, pageCount - 1, pageCount]; // Check if the current page is an edge page, to keep the number of pagination buttons consistent
|
||||
const isEdgePage = isMobile && EDGE_PAGES.includes(currentPage);
|
||||
|
||||
const pagination = (
|
||||
<Contained>
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="mt-6 tablet:mt-4">
|
||||
<ReactPaginate
|
||||
previousLabel={'<'}
|
||||
nextLabel={'>'}
|
||||
breakLabel={'...'}
|
||||
breakClassName={'break-me'}
|
||||
activeClassName={'active bg-secondary'}
|
||||
containerClassName={'pagination bg-primary text-white'}
|
||||
initialPage={currentPage - 1}
|
||||
pageCount={pageCount}
|
||||
marginPagesDisplayed={hasTooManyButtons && !isEdgePage ? 1 : 2}
|
||||
pageRangeDisplayed={isMobile ? 1 : 2}
|
||||
onPageChange={paginationHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Contained>
|
||||
);
|
||||
|
||||
const pageTitle = generateTitle('Blog');
|
||||
const featuredImageURL = featuredPost?.featureImage?.imageUrl;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>{pageTitle}</title>
|
||||
<meta name="description" content={METADATA.BLOG.DESCRIPTION}></meta>
|
||||
<meta property="og:title" content={pageTitle} key="ogtitle" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={METADATA.BLOG.DESCRIPTION}
|
||||
key="ogdesc"
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content={featuredImageURL} key="ogimage" />
|
||||
<meta property="og:url" content={METADATA.BLOG.URL} />
|
||||
<link rel="canonical" href={METADATA.BLOG.URL}></link>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={pageTitle} />
|
||||
<meta name="twitter:description" content={METADATA.BLOG.DESCRIPTION} />
|
||||
<meta name="twitter:image" content={featuredImageURL} />
|
||||
</Head>
|
||||
|
||||
<div className="flex flex-col w-full mt-12 mb-6 space-y-6 bg-alt">
|
||||
<Contained>
|
||||
<h1 className="mb-2 text-4xl font-medium uppercase font-prompt">
|
||||
Oxen Blogs
|
||||
</h1>
|
||||
{!tag && posts.length && <ArticleCardFeature {...featuredPost} />}
|
||||
|
||||
{tag && (
|
||||
<>
|
||||
<div className="flex items-center w-full mt-4 space-x-2 font-sans">
|
||||
<p className={tagHasPosts ? 'mb-0' : 'mb-10'}>
|
||||
{tagHasPosts
|
||||
? 'Tag Results:'
|
||||
: 'There are no posts with the tag'}
|
||||
</p>
|
||||
<TagBlock size="large" tag={tag} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Contained>
|
||||
|
||||
{/* Tag has posts */}
|
||||
{tag && tagHasPosts && (
|
||||
<>
|
||||
<CardGrid>
|
||||
{tagPosts?.map(post => (
|
||||
<ArticleCard key={post.id} {...post} />
|
||||
))}
|
||||
</CardGrid>
|
||||
|
||||
{pagination}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Contained>
|
||||
{tag && (
|
||||
<h3 className="-mb-2 text-3xl font-prompt text-primary">
|
||||
Recent Posts
|
||||
</h3>
|
||||
)}
|
||||
</Contained>
|
||||
|
||||
{/* Posts, or recent posts if tag */}
|
||||
<CardGrid>
|
||||
{(tag ? posts : otherPosts)?.map(post => (
|
||||
<ArticleCard key={post.id} {...post} />
|
||||
))}
|
||||
</CardGrid>
|
||||
|
||||
{!tagHasPosts && pagination}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Blog;
|
|
@ -0,0 +1,206 @@
|
|||
import { useEffect, ReactElement } from 'react';
|
||||
import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { CMS, METADATA } from '../../constants';
|
||||
import { CmsApi } from '../../services/cms';
|
||||
import { PageType, setPageType } from '../../state/navigation';
|
||||
import { IPath } from '../../types';
|
||||
import { IPost } from '../../types/cms';
|
||||
import { generateTitle } from '../../utils/metadata';
|
||||
|
||||
import { ArticleCard } from '../../components/cards/ArticleCard';
|
||||
import { CardGrid } from '../../components/cards/CardGrid';
|
||||
import { Contained } from '../../components/Contained';
|
||||
import { TagBlock } from '../../components/TagBlock';
|
||||
import Pagination from '../../components/Pagination';
|
||||
|
||||
interface Props {
|
||||
posts: IPost[];
|
||||
tagPosts: IPost[];
|
||||
tag: string;
|
||||
currentPage: number;
|
||||
pageCount: number;
|
||||
}
|
||||
|
||||
export default function Tag(props: Props): ReactElement {
|
||||
const { posts, tagPosts, tag, currentPage, pageCount } = props;
|
||||
const router = useRouter();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const tagHasPosts = tagPosts && tagPosts?.length > 0;
|
||||
const pageTitle = generateTitle(`${tag} Archives`);
|
||||
const featuredPost = posts[0];
|
||||
const featuredImageURL = featuredPost?.featureImage?.imageUrl;
|
||||
|
||||
const paginationHandler = page => {
|
||||
const newRoute = `/tag/${tag}/${page.selected + 1}`;
|
||||
router.push(newRoute);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setPageType(PageType.BLOG));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{pageTitle}</title>
|
||||
<meta name="description" content={METADATA.BLOG.DESCRIPTION}></meta>
|
||||
<meta property="og:title" content={pageTitle} key="ogtitle" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={METADATA.BLOG.DESCRIPTION}
|
||||
key="ogdesc"
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content={featuredImageURL} key="ogimage" />
|
||||
<meta property="og:url" content={METADATA.BLOG.URL} />
|
||||
<link rel="canonical" href={METADATA.BLOG.URL}></link>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={pageTitle} />
|
||||
<meta name="twitter:description" content={METADATA.BLOG.DESCRIPTION} />
|
||||
<meta name="twitter:image" content={featuredImageURL} />
|
||||
</Head>
|
||||
|
||||
<div className="flex flex-col w-full mt-12 mb-6 space-y-6 bg-alt">
|
||||
<Contained>
|
||||
<h1 className="mb-2 text-4xl font-medium uppercase font-prompt">
|
||||
Oxen Blogs
|
||||
</h1>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex w-full mt-4 space-x-2 font-sans',
|
||||
tagHasPosts
|
||||
? 'items-center'
|
||||
: 'flex-col tablet:flex-row tablet:items-center',
|
||||
)}
|
||||
>
|
||||
<p className={'mb-0'}>
|
||||
{tagHasPosts
|
||||
? 'Tag Results:'
|
||||
: 'There are no posts with the tag:'}
|
||||
</p>
|
||||
<TagBlock
|
||||
size="large"
|
||||
tag={tag}
|
||||
classes={classNames(!tagHasPosts && 'mt-3 tablet:mt-0')}
|
||||
/>
|
||||
</div>
|
||||
</Contained>
|
||||
|
||||
{tagHasPosts && (
|
||||
<>
|
||||
<CardGrid>
|
||||
{tagPosts?.map(post => (
|
||||
<ArticleCard key={post.id} {...post} />
|
||||
))}
|
||||
</CardGrid>
|
||||
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
paginationHandler={paginationHandler}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Contained>
|
||||
<h3 className="-mb-2 text-3xl font-prompt text-primary">
|
||||
Recent Posts
|
||||
</h3>
|
||||
</Contained>
|
||||
|
||||
<CardGrid>
|
||||
{posts?.map(post => (
|
||||
<ArticleCard key={post.id} {...post} />
|
||||
))}
|
||||
</CardGrid>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async (
|
||||
context: GetStaticPropsContext,
|
||||
) => {
|
||||
console.log(
|
||||
`Building Results for tag "%c${context.params.slug[0]}" page ${
|
||||
context.params.slug && context.params.slug[1]
|
||||
? context.params.slug[1]
|
||||
: ''
|
||||
}`,
|
||||
'color: purple;',
|
||||
);
|
||||
|
||||
const cms = new CmsApi();
|
||||
const tag = String(context.params.slug[0] ?? '') ?? null;
|
||||
const page = Number(context.params.slug[1] ?? 1);
|
||||
|
||||
try {
|
||||
const {
|
||||
entries: tagPosts = [],
|
||||
total: tagTotalPosts,
|
||||
} = await cms.fetchBlogEntriesByTag(
|
||||
tag,
|
||||
CMS.BLOG_RESULTS_PER_PAGE_TAGGED,
|
||||
page,
|
||||
);
|
||||
|
||||
const { entries: posts, total: totalPosts } = await cms.fetchBlogEntries(
|
||||
CMS.BLOG_RESULTS_PER_PAGE_TAGGED,
|
||||
);
|
||||
|
||||
const pageCount = Math.ceil(
|
||||
tagTotalPosts / CMS.BLOG_RESULTS_PER_PAGE_TAGGED,
|
||||
);
|
||||
|
||||
if (page > pageCount && page > 1) {
|
||||
throw 'Page results exceeded!';
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
posts,
|
||||
pageCount,
|
||||
currentPage: page,
|
||||
tag,
|
||||
tagPosts,
|
||||
},
|
||||
revalidate: CMS.CONTENT_REVALIDATE_RATE,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return {
|
||||
notFound: true,
|
||||
revalidate: CMS.CONTENT_REVALIDATE_RATE,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const cms = new CmsApi();
|
||||
|
||||
const tags = Object.values(await cms.fetchTagList());
|
||||
const paths: IPath[] = [];
|
||||
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
const { entries, total } = await cms.fetchBlogEntriesByTag(tags[i]);
|
||||
const pageCount = Math.ceil(total / CMS.BLOG_RESULTS_PER_PAGE);
|
||||
const _paths = [];
|
||||
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
_paths.push({ params: { slug: [tags[i], String(i)] } });
|
||||
}
|
||||
|
||||
paths.push(..._paths);
|
||||
}
|
||||
|
||||
return {
|
||||
paths,
|
||||
fallback: 'blocking',
|
||||
};
|
||||
};
|
|
@ -20,6 +20,7 @@ import {
|
|||
IFetchBlogEntriesReturn,
|
||||
IFetchEntriesReturn,
|
||||
IFetchFAQItemsReturn,
|
||||
ITagList,
|
||||
} from '../types/cms';
|
||||
import isLive from '../utils/environment';
|
||||
import { generateURL } from '../utils/metadata';
|
||||
|
@ -45,6 +46,20 @@ export class CmsApi {
|
|||
});
|
||||
}
|
||||
|
||||
public async fetchTagList(): Promise<ITagList> {
|
||||
// TODO Migrate to Contentful Tag System
|
||||
const { entries, total } = await this.fetchBlogEntries();
|
||||
const tags: ITagList = {};
|
||||
entries.forEach(entry => {
|
||||
entry.tags.forEach(tag => {
|
||||
if (!tags[tag]) {
|
||||
tags[tag] = tag;
|
||||
}
|
||||
});
|
||||
});
|
||||
return tags;
|
||||
}
|
||||
|
||||
public async fetchBlogEntries(
|
||||
quantity = CMS.BLOG_RESULTS_PER_PAGE,
|
||||
page = 1,
|
||||
|
|
|
@ -7,7 +7,7 @@ import sanitize from '../utils/sanitize';
|
|||
|
||||
import EmbedContent from '../components/EmbedContent';
|
||||
import { ScreenContext } from '../contexts/screen';
|
||||
import { ReactElement } from 'react';
|
||||
import { ReactElement, CSSProperties } from 'react';
|
||||
|
||||
function Markup(node: any): ReactElement {
|
||||
const frontTags: string[] = [];
|
||||
|
@ -72,7 +72,7 @@ function EmbeddedMedia(node: any, isInline = false): ReactElement {
|
|||
const imageWidth = node.width ?? media.file.details.image.width;
|
||||
const imageHeight = node.height ?? media.file.details.image.height;
|
||||
const figureClasses = [
|
||||
isInline && node.position && 'text-center mx-auto mb-5',
|
||||
isInline && node.position && 'text-center mx-auto mt-4 mb-5',
|
||||
isInline && !node.position && 'inline-block align-middle mx-1',
|
||||
isInline && node.position === 'left' && 'tablet:float-left tablet:mr-4',
|
||||
isInline && node.position === 'right' && 'tablet:float-right tablet:ml-4',
|
||||
|
@ -84,14 +84,15 @@ function EmbeddedMedia(node: any, isInline = false): ReactElement {
|
|||
!node.position &&
|
||||
'text-center tablet:inline-block tablet:align-middle tablet:mx-1',
|
||||
];
|
||||
const figureStyles: CSSProperties = {};
|
||||
if (!isMobile && node.position) {
|
||||
figureStyles.width = imageWidth;
|
||||
}
|
||||
if (isDesktop) {
|
||||
figureStyles.maxWidth = '800px';
|
||||
}
|
||||
return (
|
||||
<figure
|
||||
className={classNames(figureClasses)}
|
||||
style={{
|
||||
width: !isMobile && node.position ? imageWidth : '',
|
||||
maxWidth: isDesktop ? '800px' : '',
|
||||
}}
|
||||
>
|
||||
<figure className={classNames(figureClasses)} style={figureStyles}>
|
||||
<Image
|
||||
src={`${url}${isMobile ? '?w=300' : isTablet ? '?w=600' : ''}`}
|
||||
alt={node.title}
|
||||
|
|
|
@ -34,6 +34,10 @@ export interface IPost {
|
|||
slug: string;
|
||||
}
|
||||
|
||||
export function isPost(object: unknown): object is IPost {
|
||||
return Object.prototype.hasOwnProperty.call(object, 'publishedDate');
|
||||
}
|
||||
|
||||
export type BodyDocument = {
|
||||
nodeType: 'document';
|
||||
content: any;
|
||||
|
@ -58,6 +62,10 @@ export interface IFetchEntriesReturn {
|
|||
total: number;
|
||||
}
|
||||
|
||||
export type ITagList = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
export interface IFetchBlogEntriesReturn extends IFetchEntriesReturn {
|
||||
entries: Array<IPost>;
|
||||
}
|
||||
|
|
|
@ -1,2 +1,6 @@
|
|||
// Common types
|
||||
export type SVG = React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
|
||||
|
||||
export interface IPath {
|
||||
params: { page: string | string[] };
|
||||
}
|
||||
|
|
38
yarn.lock
38
yarn.lock
|
@ -1414,32 +1414,32 @@
|
|||
exec-sh "^0.3.2"
|
||||
minimist "^1.2.0"
|
||||
|
||||
"@contentful/rich-text-html-renderer@^15.0.0":
|
||||
version "15.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@contentful/rich-text-html-renderer/-/rich-text-html-renderer-15.2.0.tgz#112bb9f748b5bc4df9d2e2065cb9f6d6e9ca085a"
|
||||
integrity sha512-htcPzKiKsfINv6wkHtx38Vw0QLeGwzRTLe8dNHU2lvGZ+tgmqsgmt6C0XcpBbdon98Afl8xdToOpZdfQImI8NA==
|
||||
"@contentful/rich-text-html-renderer@^15.3.5":
|
||||
version "15.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@contentful/rich-text-html-renderer/-/rich-text-html-renderer-15.3.5.tgz#b8e9d5226f9e75f2ac7cf2c5761d2b4ea195b7bd"
|
||||
integrity sha512-xB9mL8Vb1Ebh0To5jxDb1mYuf1fy4j/pfcl8ZxQppB9nIc7jlHfYUePMl3P3t1kPtED7GjHmsV7kLAxEoWqtgg==
|
||||
dependencies:
|
||||
"@contentful/rich-text-types" "^15.1.0"
|
||||
"@contentful/rich-text-types" "^15.3.5"
|
||||
escape-html "^1.0.3"
|
||||
|
||||
"@contentful/rich-text-plain-text-renderer@^15.0.0":
|
||||
version "15.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@contentful/rich-text-plain-text-renderer/-/rich-text-plain-text-renderer-15.1.0.tgz#d6deb9f78de24b606d96dffd00abe8e2f957801e"
|
||||
integrity sha512-aPyDMQpb5rXfTJFRDur10qNZm1roKtXcXi6nIUP582NSLEzkp5GRydJsbp0Zcu+8qLcNMfvc7nwn4725P+Ma8w==
|
||||
"@contentful/rich-text-plain-text-renderer@^15.3.5":
|
||||
version "15.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@contentful/rich-text-plain-text-renderer/-/rich-text-plain-text-renderer-15.3.5.tgz#d92761ca118c5f0b16b2c55c9a190a8c02e75ffd"
|
||||
integrity sha512-AFNP1gAopmFiFK8E4qzMcgemYzSUHfmgVZeZAzypPDKQhhS3x/L4u9kihZcK55YkLpSJgUisc8XcrJlUTYMl4g==
|
||||
dependencies:
|
||||
"@contentful/rich-text-types" "^15.1.0"
|
||||
"@contentful/rich-text-types" "^15.3.5"
|
||||
|
||||
"@contentful/rich-text-react-renderer@^15.0.0":
|
||||
version "15.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@contentful/rich-text-react-renderer/-/rich-text-react-renderer-15.2.0.tgz#b5ff25b4746f063f15d83eccaff555597d46683e"
|
||||
integrity sha512-mMvs1tvd8Uab1Rqu1oNNqrVwaiQ3+EiLbjZQkBuQ0vTpVCmFI5w60X/mUg5aLMQzJMyEW/kzzcGEjo5oG7im5Q==
|
||||
"@contentful/rich-text-react-renderer@^15.3.5":
|
||||
version "15.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@contentful/rich-text-react-renderer/-/rich-text-react-renderer-15.3.5.tgz#94a94ea937c7a2f331942307f9998c460a5dd221"
|
||||
integrity sha512-BDZQ0Z1koj5C9VzOt9V5vaEIKRfbakHBqU2DpjmWCx0h+wRdmmVQdTX6VHousH7TOHjWLUi8w7xHH3Kjnzh0EQ==
|
||||
dependencies:
|
||||
"@contentful/rich-text-types" "^15.1.0"
|
||||
"@contentful/rich-text-types" "^15.3.5"
|
||||
|
||||
"@contentful/rich-text-types@^15.0.0", "@contentful/rich-text-types@^15.1.0":
|
||||
version "15.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@contentful/rich-text-types/-/rich-text-types-15.1.0.tgz#5637452257e6c7c6fc406ec1ec6ab7fcc1158105"
|
||||
integrity sha512-3PgQi/B5yKC3xXDar1Ry4i4l9jgt56PFZ40cOkE+oCNLmNlFtZolo6cTrf4VunuufSsCfjTs4iWovy7fbM1I8Q==
|
||||
"@contentful/rich-text-types@^15.3.5":
|
||||
version "15.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@contentful/rich-text-types/-/rich-text-types-15.3.5.tgz#414e7b4e671d9c3a4f75e5c762e4e109ccd4ba20"
|
||||
integrity sha512-NOAdIfWUadOxhnv4IR+vBtgPtAtV0hv1uoZJogm2mniXZHRIpmKNbEBncjfr/SSHhOG98tQgHCUw6b/nvDTVaQ==
|
||||
|
||||
"@csstools/convert-colors@^1.4.0":
|
||||
version "1.4.0"
|
||||
|
|
Loading…
Reference in New Issue