Merge pull request #35 from yougotwill/seo_improvements
Seo improvements
This commit is contained in:
commit
4ac7335460
|
@ -1,18 +1,21 @@
|
|||
import Head from 'next/head';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { METADATA } from '../constants';
|
||||
import { PageType, setPageType, setPostTitle } from '../state/navigation';
|
||||
import { IPost } from '../types/cms';
|
||||
import { generateTitle } from '../utils/metadata';
|
||||
|
||||
import { Article } from '../components/article/Article';
|
||||
import CustomHead from './CustomHead';
|
||||
|
||||
interface Props {
|
||||
post: IPost;
|
||||
}
|
||||
|
||||
// Parallax on bg as mouse moves
|
||||
export default function BlogPost({ post, url }: { post: IPost; url: string }) {
|
||||
export default function BlogPost(props: Props) {
|
||||
const { post } = props;
|
||||
const dispatch = useDispatch();
|
||||
const pageTitle = generateTitle(post?.title);
|
||||
const imageURL = post?.featureImage?.imageUrl;
|
||||
|
||||
useEffect(() => {
|
||||
if (post) {
|
||||
|
@ -23,28 +26,23 @@ export default function BlogPost({ post, url }: { post: IPost; url: string }) {
|
|||
|
||||
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>
|
||||
|
||||
<CustomHead
|
||||
title={post?.title}
|
||||
metadata={{
|
||||
TYPE: METADATA.BLOG_PAGE.TYPE,
|
||||
DESCRIPTION: post?.description,
|
||||
OG_IMAGE: {
|
||||
URL: post?.featureImage.imageUrl ?? METADATA.OG_IMAGE.URL,
|
||||
WIDTH: Number(post?.featureImage?.width) ?? METADATA.OG_IMAGE.WIDTH,
|
||||
HEIGHT:
|
||||
Number(post?.featureImage?.height) ?? METADATA.OG_IMAGE.HEIGHT,
|
||||
ALT: post?.featureImage?.title ?? METADATA.OG_IMAGE.ALT,
|
||||
},
|
||||
TAGS: post.tags,
|
||||
ARTICLE_SECTION: post.tags[0],
|
||||
PUBLISHED_TIME: post.publishedDateISO,
|
||||
}}
|
||||
/>
|
||||
<div className="bg-alt">
|
||||
<Article {...post} />
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,252 @@
|
|||
import { ReactElement } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import METADATA, { generateTitle, IMetadata } from '../constants/metadata';
|
||||
import { isLocal } from '..//utils/links';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
metadata?: IMetadata;
|
||||
}
|
||||
|
||||
export default function CustomHead(props: Props): ReactElement {
|
||||
const router = useRouter();
|
||||
const { title, metadata } = props;
|
||||
const pageTitle = generateTitle(title);
|
||||
const pageUrl = `${METADATA.HOST_URL}${router.asPath}`;
|
||||
const imageUrl = (() => {
|
||||
if (!metadata?.OG_IMAGE?.URL)
|
||||
return `${METADATA.HOST_URL}${METADATA.OG_IMAGE.URL}`;
|
||||
if (metadata?.OG_IMAGE?.URL && isLocal(metadata.OG_IMAGE.URL)) {
|
||||
return `${METADATA.HOST_URL}${metadata.OG_IMAGE.URL}`;
|
||||
} else {
|
||||
return `${metadata?.OG_IMAGE?.URL}`;
|
||||
}
|
||||
})();
|
||||
const tags = metadata?.TAGS ? metadata?.TAGS : METADATA.TAGS;
|
||||
const renderTags = (() => {
|
||||
const keywords = (
|
||||
<meta key="keywords" name="keywords" content={tags.join(' ')} />
|
||||
);
|
||||
if (metadata?.TYPE !== 'article') return keywords;
|
||||
return (
|
||||
<>
|
||||
{tags.map((tag, index) => {
|
||||
return (
|
||||
<meta
|
||||
key={`article:tag-${pageUrl}-${index}`}
|
||||
property="article:tag"
|
||||
content={tag}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<meta
|
||||
key="article:section"
|
||||
property="article:section"
|
||||
content={metadata?.ARTICLE_SECTION ?? METADATA.TAGS[0]}
|
||||
/>
|
||||
{metadata?.PUBLISHED_TIME && (
|
||||
<meta
|
||||
key="article:published_time"
|
||||
property="article:published_time"
|
||||
content={metadata?.PUBLISHED_TIME}
|
||||
/>
|
||||
)}
|
||||
{keywords}
|
||||
</>
|
||||
);
|
||||
})();
|
||||
const renderLdJSON = (() => {
|
||||
const ldjson = `{
|
||||
"@context": "https://schema.org",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "WebSite",
|
||||
"@id": "${METADATA.HOST_URL}/#website",
|
||||
"url": "${pageUrl}",
|
||||
"name": "${METADATA.SITE_NAME}",
|
||||
"description": "${METADATA.DESCRIPTION}"
|
||||
},
|
||||
{
|
||||
"@type": "ImageObject",
|
||||
"@id": "${pageUrl}#primaryimage",
|
||||
"url": "${imageUrl}",
|
||||
"width": "${String(
|
||||
metadata?.OG_IMAGE?.WIDTH ?? METADATA.OG_IMAGE.WIDTH,
|
||||
)}",
|
||||
"height": "${String(
|
||||
metadata?.OG_IMAGE?.HEIGHT ?? METADATA.OG_IMAGE.HEIGHT,
|
||||
)}"
|
||||
},
|
||||
{
|
||||
"@type": "WebPage",
|
||||
"@id": "${pageUrl}#webpage",
|
||||
"url": "${pageUrl}",
|
||||
"inLanguage": "${METADATA.LOCALE}",
|
||||
"name": "${pageTitle}",
|
||||
"isPartOf": { "@id": "${METADATA.HOST_URL}/#website" },
|
||||
"primaryImageOfPage": {
|
||||
"@id": "${pageUrl}#primaryimage"
|
||||
},
|
||||
"datePublished": "${metadata?.PUBLISHED_TIME ?? ''}",
|
||||
"description": "${METADATA.DESCRIPTION}"
|
||||
}
|
||||
]
|
||||
}`;
|
||||
return (
|
||||
<script
|
||||
key={`ldjson-${pageUrl}`}
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: ldjson }}
|
||||
/>
|
||||
);
|
||||
})();
|
||||
return (
|
||||
<Head>
|
||||
<title key={pageTitle}>{pageTitle}</title>
|
||||
<meta key="utf-8" charSet="utf-8" />
|
||||
<meta
|
||||
key="viewport"
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
||||
/>
|
||||
<meta
|
||||
key="description"
|
||||
name="description"
|
||||
content={metadata?.DESCRIPTION ?? METADATA.DESCRIPTION}
|
||||
/>
|
||||
<meta
|
||||
key="robots"
|
||||
name="robots"
|
||||
content="index,follow,max-snippet:-1,max-image-preview:large,max-video-preview:-1"
|
||||
/>
|
||||
<meta
|
||||
key="googlebot"
|
||||
name="googlebot"
|
||||
content="index,follow,max-snippet:-1,max-image-preview:large,max-video-preview:-1"
|
||||
/>
|
||||
<meta key="og:url" property="og:url" content={pageUrl} />
|
||||
<meta key="og:title" property="og:title" content={pageTitle} />
|
||||
<meta
|
||||
key="og:type"
|
||||
property="og:type"
|
||||
content={metadata?.TYPE ?? METADATA.OG_TYPE}
|
||||
/>
|
||||
<meta
|
||||
key="og:description"
|
||||
property="og:description"
|
||||
content={metadata?.DESCRIPTION ?? METADATA.DESCRIPTION}
|
||||
/>
|
||||
<meta key="og:image" property="og:image" content={imageUrl} />
|
||||
<meta
|
||||
key="og:image:secure_url"
|
||||
property="og:image:secure_url"
|
||||
content={imageUrl}
|
||||
></meta>
|
||||
<meta
|
||||
key="og:image:alt"
|
||||
property="og:image:alt"
|
||||
content={metadata?.OG_IMAGE?.ALT ?? METADATA.OG_IMAGE.ALT}
|
||||
/>
|
||||
<meta
|
||||
key="og:image:width"
|
||||
property="og:image:width"
|
||||
content={String(metadata?.OG_IMAGE?.WIDTH ?? METADATA.OG_IMAGE.WIDTH)}
|
||||
/>
|
||||
<meta
|
||||
key="og:image:height"
|
||||
property="og:image:height"
|
||||
content={String(metadata?.OG_IMAGE?.HEIGHT ?? METADATA.OG_IMAGE.HEIGHT)}
|
||||
/>
|
||||
<meta key="og:locale" property="og:locale" content={METADATA.LOCALE} />
|
||||
<meta
|
||||
key="og:site_name"
|
||||
property="og:site_name"
|
||||
content={METADATA.SITE_NAME}
|
||||
/>
|
||||
<meta
|
||||
key="twitter:card"
|
||||
name="twitter:card"
|
||||
content="summary_large_image"
|
||||
/>
|
||||
<meta key="twitter:title" name="twitter:title" content={pageTitle} />
|
||||
<meta
|
||||
key="twitter:description"
|
||||
name="twitter:description"
|
||||
content={metadata?.DESCRIPTION ?? METADATA.DESCRIPTION}
|
||||
/>
|
||||
<meta key="twitter:image" name="twitter:image" content={imageUrl} />
|
||||
<meta
|
||||
key="twitter:site"
|
||||
name="twitter:site"
|
||||
content={METADATA.HOST_URL}
|
||||
/>
|
||||
<meta key="twitter:creator" name="twitter:creator" content="Oxen_io" />
|
||||
<meta
|
||||
key="apple-itunes-app"
|
||||
name="apple-itunes-app"
|
||||
content="app-id=1547745078"
|
||||
/>
|
||||
<meta
|
||||
key="msapplication-TileColor"
|
||||
name="msapplication-TileColor"
|
||||
content={METADATA.MSAPPLICATION_TILECOLOR}
|
||||
/>
|
||||
<meta
|
||||
key="theme-color"
|
||||
name="theme-color"
|
||||
content={METADATA.THEME_COLOR}
|
||||
/>
|
||||
{renderTags}
|
||||
<link key="canonical" rel="canonical" href={pageUrl} />
|
||||
<link
|
||||
key="image/png32x32"
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href={METADATA.FAVICON.MEDIUM}
|
||||
/>
|
||||
<link
|
||||
key="image/png16x16"
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href={METADATA.FAVICON.SMALL}
|
||||
/>
|
||||
<link
|
||||
key="apple-touch-icon"
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href={METADATA.FAVICON.APPLE_TOUCH_ICON}
|
||||
/>
|
||||
<link key="manifest" rel="manifest" href={METADATA.MANIFEST} />
|
||||
<link
|
||||
key="mask-icon"
|
||||
rel="mask-icon"
|
||||
href={METADATA.MASK_ICON.PATH}
|
||||
color={METADATA.MASK_ICON.COLOR}
|
||||
/>
|
||||
<link key="shortlink" rel="shortlink" href={METADATA.HOST_URL} />
|
||||
<link
|
||||
key="/feed"
|
||||
rel="alternative"
|
||||
type="application/rss+xml"
|
||||
href="/feed"
|
||||
/>
|
||||
<link
|
||||
key="/feed/atom"
|
||||
rel="alternative"
|
||||
type="application/atom+xml"
|
||||
href="/feed/atom"
|
||||
/>
|
||||
<link
|
||||
key="/feed/json"
|
||||
rel="alternative"
|
||||
type="application/feed+json"
|
||||
href="/feed/json"
|
||||
/>
|
||||
{metadata?.TYPE === 'article' && renderLdJSON}
|
||||
</Head>
|
||||
);
|
||||
}
|
|
@ -1,25 +1,21 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { METADATA } from '../constants';
|
||||
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';
|
||||
import CustomHead from './CustomHead';
|
||||
|
||||
export default function RichPage({
|
||||
page,
|
||||
href,
|
||||
}: {
|
||||
interface Props {
|
||||
page: ISplitPage;
|
||||
href: string;
|
||||
}) {
|
||||
}
|
||||
|
||||
export default function RichPage(props: Props) {
|
||||
const { page } = props;
|
||||
const dispatch = useDispatch();
|
||||
const pageTitle = generateTitle(page?.label);
|
||||
const pageDescription = page?.title;
|
||||
const pageURL = generateURL(href);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setPageType(PageType.NORMAL));
|
||||
|
@ -27,37 +23,25 @@ export default function RichPage({
|
|||
|
||||
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>
|
||||
|
||||
<CustomHead
|
||||
title={page?.label}
|
||||
metadata={{
|
||||
DESCRIPTION: page?.label,
|
||||
OG_IMAGE: {
|
||||
URL: page?.hero?.imageUrl ?? METADATA.OG_IMAGE.URL,
|
||||
WIDTH: Number(page?.hero?.width) ?? METADATA.OG_IMAGE.WIDTH,
|
||||
HEIGHT: Number(page?.hero?.height) ?? METADATA.OG_IMAGE.HEIGHT,
|
||||
ALT: page?.hero?.title ?? METADATA.OG_IMAGE.ALT,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<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}
|
||||
alt={page?.hero?.description ?? page?.label}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,20 +1,85 @@
|
|||
import { titleCase } from '../utils/text';
|
||||
|
||||
export interface IMetadata {
|
||||
DESCRIPTION: string;
|
||||
TYPE?: string;
|
||||
OG_IMAGE?: {
|
||||
URL: string;
|
||||
WIDTH: number;
|
||||
HEIGHT: number;
|
||||
ALT: string;
|
||||
};
|
||||
TAGS?: string[];
|
||||
ARTICLE_SECTION?: string;
|
||||
PUBLISHED_TIME?: string;
|
||||
}
|
||||
|
||||
export function generateTitle(prefix: string) {
|
||||
return prefix && prefix.length > 0
|
||||
? `${titleCase(prefix)} - ${METADATA.TITLE}`
|
||||
: METADATA.TITLE;
|
||||
}
|
||||
|
||||
export function generateURL(prefix: string) {
|
||||
return prefix ? `${METADATA.HOST_URL}${prefix}` : METADATA.HOST_URL;
|
||||
}
|
||||
|
||||
const METADATA = {
|
||||
OXEN_HOST_URL: 'https://oxen.io',
|
||||
TITLE_SUFFIX: 'Oxen | Privacy made simple.',
|
||||
SITE_META_DESCRIPTION:
|
||||
HOST_URL: 'https://oxen.io',
|
||||
SITE_NAME: 'Oxen',
|
||||
TITLE: 'Oxen | Privacy made simple.',
|
||||
DESCRIPTION:
|
||||
'Oxen is built by the OPTF, a passionate team of advocates, creatives, and engineers building a world where the internet is open, software is free and accessible, and your privacy is protected. The OPTF also builds other platforms using Oxen technology, and supports other developers in building on Oxen.',
|
||||
ROADMAP: {
|
||||
DESCRIPTION: "View Oxen's plan for the future here.",
|
||||
TAGS: [
|
||||
'Privacy',
|
||||
'decentralisation',
|
||||
'decentralised',
|
||||
'Open Source',
|
||||
'Private messaging',
|
||||
'Onion routing',
|
||||
'Cryptocurrency',
|
||||
'Digital finance',
|
||||
'Privacy Tools',
|
||||
],
|
||||
OG_TYPE: 'website',
|
||||
OG_IMAGE: {
|
||||
URL: '/site-banner.png',
|
||||
WIDTH: 800,
|
||||
HEIGHT: 450,
|
||||
ALT: 'Oxen Logo Blue Background',
|
||||
},
|
||||
LOCALE: 'en_US',
|
||||
FAVICON: {
|
||||
MEDIUM: '/favicon-32x32.png',
|
||||
SMALL: '/favicon-16x16.png',
|
||||
APPLE_TOUCH_ICON: '/apple-touch-icon.png',
|
||||
},
|
||||
MANIFEST: '/site.webmanifest',
|
||||
MASK_ICON: { PATH: '/safari-pinned-tab.svg', COLOR: '#5bbad5' },
|
||||
MSAPPLICATION_TILECOLOR: '#343132',
|
||||
THEME_COLOR: '#ffffff',
|
||||
404: {
|
||||
DESCRIPTION: "Oopsy, here's our 404 page.",
|
||||
},
|
||||
BLOG: {
|
||||
BLOG_PAGE: {
|
||||
TYPE: 'article',
|
||||
DESCRIPTION: "View Oxen's Blog Updates Here",
|
||||
URL: 'https://oxen.io/blog',
|
||||
},
|
||||
FAQ: {
|
||||
TAG_PAGE: {
|
||||
TYPE: 'article',
|
||||
DESCRIPTION: "View Oxen's Blog Updates Sorted By Tag Here",
|
||||
},
|
||||
ROADMAP_PAGE: {
|
||||
DESCRIPTION: "View Oxen's plan for the future here.",
|
||||
},
|
||||
FAQ_PAGE: {
|
||||
DESCRIPTION: 'View Some Frequently Asked Questions here',
|
||||
OG_IMAGE: {
|
||||
URL: '/img/faq.png',
|
||||
WIDTH: 1920,
|
||||
HEIGHT: 1080,
|
||||
ALT: 'Question mark with server boxes surrounding it',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -25,14 +25,17 @@ const nextConfig = {
|
|||
images: {
|
||||
domains: ['downloads.ctfassets.net', 'images.ctfassets.net'],
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
serverRuntimeConfig: {
|
||||
redirects: [
|
||||
{
|
||||
source: '/blog/session-the-road-to-monetisation-and-oxen-value-capture',
|
||||
destination: '/blog/session-the-road-to-monetisation',
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
],
|
||||
},
|
||||
async redirects() {
|
||||
return this.serverRuntimeConfig.redirects;
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
|
@ -50,6 +53,10 @@ const nextConfig = {
|
|||
source: '/blog/:slug((?:[\\w]{1,}[\\-]{1,}).*|[\\D]{1,})',
|
||||
destination: '/:slug',
|
||||
},
|
||||
{
|
||||
source: '/sitemap.xml',
|
||||
destination: '/api/sitemap',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { useContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Head from 'next/head';
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
// import _404 from '../assets/svgs/404.svg';
|
||||
import { UI, METADATA } from '../constants';
|
||||
import { ScreenContext } from '../contexts/screen';
|
||||
import { generateTitle, generateURL } from '../utils/metadata';
|
||||
|
||||
import CustomHead from '../components/CustomHead';
|
||||
|
||||
function oxen404() {
|
||||
const { isMobile, isTablet, isDesktop, isHuge } = useContext(ScreenContext);
|
||||
|
@ -43,30 +44,9 @@ function oxen404() {
|
|||
minHeight: isTablet ? '330px' : '450px',
|
||||
};
|
||||
|
||||
const goBackHomeStyles = {
|
||||
width: '9rem',
|
||||
};
|
||||
|
||||
const pageTitle = generateTitle('404');
|
||||
const pageURL = generateURL('/404');
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center flex-grow h-full">
|
||||
<Head>
|
||||
<title>{pageTitle}</title>
|
||||
<meta name="description" content={METADATA['404'].DESCRIPTION}></meta>
|
||||
<meta property="og:title" content={pageTitle} key="ogtitle" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={METADATA['404'].DESCRIPTION}
|
||||
key="ogdesc"
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={pageURL} />
|
||||
|
||||
<link rel="canonical" href={pageURL}></link>
|
||||
</Head>
|
||||
|
||||
<CustomHead title={'404'} metadata={METADATA[404]} />
|
||||
<div style={wrapperStyles} className="flex items-center flex-grow">
|
||||
<div
|
||||
className={classNames(
|
||||
|
|
|
@ -48,8 +48,8 @@ export const getStaticPaths: GetStaticPaths = async () => {
|
|||
|
||||
export async function getStaticProps({ params }) {
|
||||
console.log(`Building Page %c${params.page}`, 'color: purple;');
|
||||
const href = params?.page ?? '';
|
||||
const id = unslugify(String(href));
|
||||
const url = params?.page ?? '';
|
||||
const id = unslugify(String(url));
|
||||
|
||||
try {
|
||||
const cms = new CmsApi();
|
||||
|
@ -58,7 +58,7 @@ export async function getStaticProps({ params }) {
|
|||
if (SideMenuItem[id]) {
|
||||
page = await cms.fetchPageById(SideMenuItem[id]);
|
||||
} else {
|
||||
page = await cms.fetchEntryBySlug(href, 'post');
|
||||
page = await cms.fetchEntryBySlug(url, 'post');
|
||||
// embedded links in post body need metadata for preview
|
||||
page.body = await generateLinkMeta(page.body);
|
||||
}
|
||||
|
@ -66,7 +66,6 @@ export async function getStaticProps({ params }) {
|
|||
return {
|
||||
props: {
|
||||
page,
|
||||
href: `/${href}`,
|
||||
},
|
||||
revalidate: CMS.CONTENT_REVALIDATE_RATE,
|
||||
};
|
||||
|
@ -79,16 +78,10 @@ export async function getStaticProps({ params }) {
|
|||
}
|
||||
}
|
||||
|
||||
export default function Page({
|
||||
page,
|
||||
href,
|
||||
}: {
|
||||
page: ISplitPage | IPost;
|
||||
href: string;
|
||||
}) {
|
||||
export default function Page({ page }: { page: ISplitPage | IPost }) {
|
||||
if (isPost(page)) {
|
||||
return <BlogPost post={page} url={href} />;
|
||||
return <BlogPost post={page} />;
|
||||
} else {
|
||||
return <RichPage page={page} href={href} />;
|
||||
return <RichPage page={page} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import type { AppProps } from 'next/app';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Provider as StoreProvider } from 'react-redux';
|
||||
import { createStore } from 'redux';
|
||||
|
||||
import '../assets/style.css';
|
||||
import Layout from '../components/layout';
|
||||
import { METADATA, NAVIGATION } from '../constants';
|
||||
import { NAVIGATION } from '../constants';
|
||||
import ScreenProvider from '../contexts/screen';
|
||||
import {
|
||||
collapseMobileHeader,
|
||||
|
@ -16,6 +15,9 @@ import {
|
|||
} from '../state/navigation';
|
||||
import { rootReducer } from '../state/reducers';
|
||||
|
||||
import CustomHead from '../components/CustomHead';
|
||||
import Layout from '../components/layout';
|
||||
|
||||
const store = createStore(rootReducer);
|
||||
|
||||
function App({ Component, pageProps }: AppProps) {
|
||||
|
@ -57,17 +59,7 @@ function App({ Component, pageProps }: AppProps) {
|
|||
<>
|
||||
<StoreProvider store={store}>
|
||||
<ScreenProvider>
|
||||
<Head>
|
||||
<title>{METADATA.TITLE_SUFFIX}</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1"
|
||||
></meta>
|
||||
<meta property="og:site_name" content="Oxen" key="ogsitename" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta name="apple-itunes-app" content="app-id=1547745078" />
|
||||
</Head>
|
||||
|
||||
<CustomHead />
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
|
|
|
@ -1,56 +1,21 @@
|
|||
import Document, { Head, Html, Main, NextScript } from 'next/document';
|
||||
import React from 'react';
|
||||
import Document, {
|
||||
DocumentContext,
|
||||
Head,
|
||||
Html,
|
||||
Main,
|
||||
NextScript,
|
||||
} from 'next/document';
|
||||
|
||||
class MyDocument extends Document {
|
||||
static async getInitialProps(ctx: DocumentContext) {
|
||||
const initialProps = await Document.getInitialProps(ctx);
|
||||
return { ...initialProps };
|
||||
}
|
||||
|
||||
export default class CustomDocument extends Document<any> {
|
||||
render() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
<link rel="shortcut icon" href="/favicon.ico"></link>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/favicon-16x16.png"
|
||||
/>
|
||||
<link
|
||||
key="rss-feed"
|
||||
rel="alternative"
|
||||
type="application/rss+xml"
|
||||
title="RSS feed for just-be.dev"
|
||||
href="/feed"
|
||||
/>
|
||||
<link
|
||||
key="atom-feed"
|
||||
rel="alternative"
|
||||
type="application/atom+xml"
|
||||
title="Atom feed for just-be.dev"
|
||||
href="/feed/atom"
|
||||
/>
|
||||
<link
|
||||
key="json-feed"
|
||||
rel="alternative"
|
||||
type="application/feed+json"
|
||||
title="JSON feed for just-be.dev"
|
||||
href="/feed/json"
|
||||
/>
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
|
||||
{this.props?.styleTags}
|
||||
</Head>
|
||||
<Head />
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
|
@ -59,3 +24,5 @@ export default class CustomDocument extends Document<any> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MyDocument;
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import getConfig from 'next/config';
|
||||
import { readdirSync } from 'fs';
|
||||
|
||||
import { CMS, METADATA, NAVIGATION } from '../../constants';
|
||||
import { CmsApi } from '../../services/cms';
|
||||
import { isLocal } from '../../utils/links';
|
||||
import { SideMenuItem } from '../../state/navigation';
|
||||
|
||||
interface IRedirection {
|
||||
source: string;
|
||||
destination: string;
|
||||
permanent: boolean;
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
const cms = new CmsApi();
|
||||
|
||||
const baseUrl = {
|
||||
development: 'http://localhost:3000',
|
||||
test: 'http://localhost:3000',
|
||||
production: METADATA.HOST_URL,
|
||||
}[process.env.NODE_ENV];
|
||||
|
||||
const staticPages = readdirSync('pages')
|
||||
.filter(page => {
|
||||
return ![
|
||||
'.DS_Store',
|
||||
'_app.tsx',
|
||||
'_document.tsx',
|
||||
'_error.tsx',
|
||||
'404.tsx',
|
||||
'[page].tsx',
|
||||
'sitemap.xml.tsx',
|
||||
'roadmap.tsx',
|
||||
'faq.tsx',
|
||||
'api',
|
||||
'tag',
|
||||
].includes(page);
|
||||
})
|
||||
.map(pagePath => {
|
||||
if (pagePath.includes('index')) {
|
||||
pagePath = '';
|
||||
} else {
|
||||
pagePath = pagePath.split('.tsx')[0];
|
||||
}
|
||||
return `${baseUrl}/${pagePath}`;
|
||||
});
|
||||
|
||||
const navigationPages = Object.keys(NAVIGATION.SIDE_MENU_ITEMS)
|
||||
.filter(url => {
|
||||
return isLocal(NAVIGATION.SIDE_MENU_ITEMS[SideMenuItem[url]].href);
|
||||
})
|
||||
.map(key => {
|
||||
return `${baseUrl}${NAVIGATION.SIDE_MENU_ITEMS[SideMenuItem[key]].href}`;
|
||||
});
|
||||
|
||||
const redirectPages = getConfig().serverRuntimeConfig.redirects.map(
|
||||
(redirect: IRedirection) => {
|
||||
if (redirect.source.includes(':slug')) {
|
||||
return '';
|
||||
} else {
|
||||
return `${baseUrl}${redirect.source}`;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
entries: _blogPages,
|
||||
total: totalBlogPages,
|
||||
} = await cms.fetchBlogEntries();
|
||||
const blogPages = _blogPages.map(page => {
|
||||
return {
|
||||
url: `${baseUrl}/blog/${page.slug}`,
|
||||
published: page.publishedDateISO,
|
||||
};
|
||||
});
|
||||
|
||||
const bloglistPages = [];
|
||||
for (let i = 1; i <= totalBlogPages; i++) {
|
||||
bloglistPages.push(`${baseUrl}/blog/${i}`);
|
||||
}
|
||||
|
||||
const tags = await cms.fetchTagList();
|
||||
const taglistPages = [];
|
||||
for (const tag of Object.keys(tags)) {
|
||||
const { entries, total } = await cms.fetchBlogEntriesByTag(tag);
|
||||
const pageCount = Math.ceil(total / CMS.BLOG_RESULTS_PER_PAGE);
|
||||
const _pages = [];
|
||||
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
_pages.push(`${baseUrl}/tag/${tag}/${i}`);
|
||||
}
|
||||
|
||||
taglistPages.push(..._pages);
|
||||
}
|
||||
|
||||
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${[
|
||||
...staticPages,
|
||||
...navigationPages,
|
||||
...redirectPages,
|
||||
...bloglistPages,
|
||||
...taglistPages,
|
||||
]
|
||||
.map(url => {
|
||||
return `
|
||||
<url>
|
||||
<loc>${url}</loc>
|
||||
<lastmod>${new Date().toISOString()}</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
`;
|
||||
})
|
||||
.join('')}
|
||||
${blogPages
|
||||
.map(post => {
|
||||
return `
|
||||
<url>
|
||||
<loc>${post.url}</loc>
|
||||
<lastmod>${post.published}</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
`;
|
||||
})
|
||||
.join('')}
|
||||
</urlset>
|
||||
`;
|
||||
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/xml');
|
||||
res.write(sitemap);
|
||||
res.end();
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
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';
|
||||
|
||||
|
@ -9,8 +8,8 @@ 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 CustomHead from '../../components/CustomHead';
|
||||
import { ArticleCard } from '../../components/cards/ArticleCard';
|
||||
import { ArticleCardFeature } from '../../components/cards/ArticleCardFeature';
|
||||
import { CardGrid } from '../../components/cards/CardGrid';
|
||||
|
@ -31,8 +30,6 @@ export default function Blog(props: Props): ReactElement {
|
|||
} = 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}`;
|
||||
|
@ -45,25 +42,23 @@ export default function Blog(props: Props): ReactElement {
|
|||
|
||||
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>
|
||||
|
||||
<CustomHead
|
||||
title={'Blog'}
|
||||
metadata={{
|
||||
TYPE: METADATA.BLOG_PAGE.TYPE,
|
||||
DESCRIPTION: METADATA.BLOG_PAGE.DESCRIPTION,
|
||||
OG_IMAGE: {
|
||||
URL: featuredPost?.featureImage.imageUrl ?? METADATA.OG_IMAGE.URL,
|
||||
WIDTH:
|
||||
Number(featuredPost?.featureImage?.width) ??
|
||||
METADATA.OG_IMAGE.WIDTH,
|
||||
HEIGHT:
|
||||
Number(featuredPost?.featureImage?.height) ??
|
||||
METADATA.OG_IMAGE.HEIGHT,
|
||||
ALT: featuredPost?.featureImage?.title ?? METADATA.OG_IMAGE.ALT,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<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">
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import Head from 'next/head';
|
||||
import React from 'react';
|
||||
import { GetStaticProps, GetStaticPropsContext } from 'next';
|
||||
|
||||
import { NAVIGATION, METADATA, CMS } from '../constants';
|
||||
import { SideMenuItem } from '../state/navigation';
|
||||
import { generateTitle, generateURL } from '../utils/metadata';
|
||||
import { CmsApi } from '../services/cms';
|
||||
import { IFAQItem } from '../types/cms';
|
||||
|
||||
import { Accordion } from '../components/Accordion';
|
||||
import { Contained } from '../components/Contained';
|
||||
import CustomHead from '../components/CustomHead';
|
||||
|
||||
export const getStaticProps: GetStaticProps = async (
|
||||
context: GetStaticPropsContext,
|
||||
|
@ -32,42 +32,17 @@ interface Props {
|
|||
function FAQ(props: Props) {
|
||||
const { faqItems } = props;
|
||||
|
||||
const pageTitle = generateTitle(
|
||||
NAVIGATION.SIDE_MENU_ITEMS[SideMenuItem.FAQ].label,
|
||||
);
|
||||
const pageURL = generateURL(
|
||||
NAVIGATION.SIDE_MENU_ITEMS[SideMenuItem.FAQ].href,
|
||||
);
|
||||
const imagePathLocal = 'img/faq.png';
|
||||
const imageURL = `${METADATA.OXEN_HOST_URL}/${imagePathLocal}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{pageTitle}</title>
|
||||
<meta name="description" content={METADATA.FAQ.DESCRIPTION}></meta>
|
||||
<meta property="og:title" content={pageTitle} key="ogtitle" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={METADATA.FAQ.DESCRIPTION}
|
||||
key="ogdesc"
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content={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={METADATA.FAQ.DESCRIPTION} />
|
||||
<meta name="twitter:image" content={imageURL} />
|
||||
</Head>
|
||||
|
||||
<CustomHead
|
||||
title={NAVIGATION.SIDE_MENU_ITEMS[SideMenuItem.FAQ].label}
|
||||
metadata={METADATA.FAQ_PAGE}
|
||||
/>
|
||||
<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={imagePathLocal}
|
||||
src={METADATA.FAQ_PAGE.OG_IMAGE.URL}
|
||||
className="object-contain w-full"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { GetStaticProps, GetStaticPropsContext } from 'next';
|
||||
import Head from 'next/head';
|
||||
|
||||
import { IPost } from '../types/cms';
|
||||
import { CMS, METADATA } from '../constants';
|
||||
import { CMS } from '../constants';
|
||||
import { CmsApi } from '../services/cms';
|
||||
import generateRSSFeed from '../utils/rss';
|
||||
|
||||
|
@ -10,39 +9,8 @@ import { HomeHero } from '../components/HomeHero';
|
|||
import { HomeHeroBubble } from '../components/HomeHeroBubble';
|
||||
|
||||
export default function Index() {
|
||||
const imageURL = `${METADATA.OXEN_HOST_URL}/site-banner.png`;
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{METADATA.TITLE_SUFFIX}</title>
|
||||
<meta
|
||||
name="description"
|
||||
content={METADATA.SITE_META_DESCRIPTION}
|
||||
></meta>
|
||||
<meta
|
||||
property="og:title"
|
||||
content={METADATA.TITLE_SUFFIX}
|
||||
key="ogtitle"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content={METADATA.SITE_META_DESCRIPTION}
|
||||
key="ogdesc"
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content={imageURL} key="ogimage" />
|
||||
<meta property="og:url" content={METADATA.OXEN_HOST_URL} />
|
||||
|
||||
<link rel="canonical" href={METADATA.OXEN_HOST_URL}></link>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={METADATA.TITLE_SUFFIX} />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content={METADATA.SITE_META_DESCRIPTION}
|
||||
/>
|
||||
<meta name="twitter:image" content={imageURL} />
|
||||
</Head>
|
||||
|
||||
{/* Only visible when no pages are open */}
|
||||
<HomeHero />
|
||||
<HomeHeroBubble />
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import Head from 'next/head';
|
||||
import React from 'react';
|
||||
import { useMeasure } from 'react-use';
|
||||
|
||||
import { NAVIGATION, METADATA } from '../constants';
|
||||
import { SideMenuItem } from '../state/navigation';
|
||||
import { generateTitle, generateURL } from '../utils/metadata';
|
||||
|
||||
import CustomHead from '../components/CustomHead';
|
||||
|
||||
function Roadmap() {
|
||||
const [ref, { width, height }] = useMeasure();
|
||||
|
@ -18,38 +18,12 @@ function Roadmap() {
|
|||
console.log('roadmap ➡️ width:', width);
|
||||
console.log('roadmap ➡️ ratio:', aspectRatio);
|
||||
|
||||
const pageTitle = generateTitle(
|
||||
NAVIGATION.SIDE_MENU_ITEMS[SideMenuItem.ROADMAP].label,
|
||||
);
|
||||
const pageURL = generateURL(
|
||||
NAVIGATION.SIDE_MENU_ITEMS[SideMenuItem.ROADMAP].href,
|
||||
);
|
||||
const imageURL = `${METADATA.OXEN_HOST_URL}/site-banner.png`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{pageTitle}</title>
|
||||
<meta name="description" content={METADATA.ROADMAP.DESCRIPTION}></meta>
|
||||
<meta property="og:title" content={pageTitle} key="ogtitle" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={METADATA.ROADMAP.DESCRIPTION}
|
||||
key="ogdesc"
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={pageURL} />
|
||||
<meta property="og:image" content={imageURL} key="ogimage" />
|
||||
<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={METADATA.ROADMAP.DESCRIPTION}
|
||||
/>
|
||||
<meta name="twitter:image" content={imageURL} />
|
||||
</Head>
|
||||
|
||||
<CustomHead
|
||||
title={NAVIGATION.SIDE_MENU_ITEMS[SideMenuItem.ROADMAP].label}
|
||||
metadata={METADATA.ROADMAP_PAGE}
|
||||
/>
|
||||
<div className="mx-4">
|
||||
<div className="flex items-center justify-center mt-8">
|
||||
<img
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
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';
|
||||
|
@ -10,8 +9,8 @@ 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 CustomHead from '../../components/CustomHead';
|
||||
import { ArticleCard } from '../../components/cards/ArticleCard';
|
||||
import { CardGrid } from '../../components/cards/CardGrid';
|
||||
import { Contained } from '../../components/Contained';
|
||||
|
@ -32,9 +31,6 @@ export default function Tag(props: Props): ReactElement {
|
|||
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}`;
|
||||
|
@ -47,25 +43,22 @@ export default function Tag(props: Props): ReactElement {
|
|||
|
||||
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>
|
||||
|
||||
<CustomHead
|
||||
title={tag === 'dev-update' ? 'Dev Updates' : `${tag} Archives`}
|
||||
metadata={{
|
||||
TYPE: METADATA.TAG_PAGE.TYPE,
|
||||
DESCRIPTION: METADATA.TAG_PAGE.DESCRIPTION,
|
||||
OG_IMAGE: {
|
||||
URL: posts[0]?.featureImage.imageUrl ?? METADATA.OG_IMAGE.URL,
|
||||
WIDTH:
|
||||
Number(posts[0]?.featureImage?.width) ?? METADATA.OG_IMAGE.WIDTH,
|
||||
HEIGHT:
|
||||
Number(posts[0]?.featureImage?.height) ??
|
||||
METADATA.OG_IMAGE.HEIGHT,
|
||||
ALT: posts[0]?.featureImage?.title ?? METADATA.OG_IMAGE.ALT,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<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">
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://oxen.io/sitemap.xml
|
|
@ -23,7 +23,7 @@ import {
|
|||
ITagList,
|
||||
} from '../types/cms';
|
||||
import isLive from '../utils/environment';
|
||||
import { generateURL } from '../utils/metadata';
|
||||
import { generateURL } from '../constants/metadata';
|
||||
import { fetchContent } from './embed';
|
||||
|
||||
function loadOptions(options: any) {
|
||||
|
@ -279,6 +279,7 @@ export class CmsApi {
|
|||
body: rawPost.body ?? null,
|
||||
subtitle: rawPost.subtitle ?? null,
|
||||
description: rawPost.description ?? null,
|
||||
publishedDateISO: rawPost.date,
|
||||
publishedDate: format(parseISO(rawPost.date), 'dd MMMM yyyy'),
|
||||
slug: rawPost.slug,
|
||||
tags: rawPost?.tags, //?.map(t => t?.fields?.label) ?? [],
|
||||
|
|
|
@ -28,6 +28,7 @@ export interface IPost {
|
|||
description: string;
|
||||
body: Document;
|
||||
author?: IAuthor;
|
||||
publishedDateISO: string;
|
||||
publishedDate: string;
|
||||
featureImage?: IFigureImage;
|
||||
tags: Array<string>;
|
||||
|
|
|
@ -7,7 +7,7 @@ const protocols = ['https://', 'http://', 'ftp://', 'file://', 'mailto:'];
|
|||
|
||||
export function isLocal(url: string) {
|
||||
let result = true;
|
||||
if (url[0] === '#') {
|
||||
if (url[0] === '#' || url.indexOf('localhost:') > 0) {
|
||||
return result;
|
||||
}
|
||||
protocols.forEach(protocol => {
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
import { METADATA } from '../constants';
|
||||
import { titleCase } from './text';
|
||||
|
||||
export function generateTitle(prefix: string) {
|
||||
return prefix
|
||||
? `${titleCase(prefix)} - ${METADATA.TITLE_SUFFIX}`
|
||||
: METADATA.TITLE_SUFFIX;
|
||||
}
|
||||
|
||||
export function generateURL(prefix: string) {
|
||||
return prefix ? `${METADATA.OXEN_HOST_URL}${prefix}` : METADATA.OXEN_HOST_URL;
|
||||
}
|
22
utils/rss.ts
22
utils/rss.ts
|
@ -4,22 +4,11 @@ import { documentToHtmlString } from '@contentful/rich-text-html-renderer';
|
|||
import { IPost } from '../types/cms';
|
||||
import { METADATA } from '../constants';
|
||||
|
||||
const baseUrl = METADATA.OXEN_HOST_URL;
|
||||
const categories = [
|
||||
'Privacy',
|
||||
'decentralisation',
|
||||
'decentralised',
|
||||
'Open Source',
|
||||
'Private messaging',
|
||||
'Onion routing',
|
||||
'Cryptocurrency',
|
||||
'Digital finance',
|
||||
'Privacy Tools',
|
||||
];
|
||||
const baseUrl = METADATA.HOST_URL;
|
||||
const date = new Date();
|
||||
const feed = new Feed({
|
||||
title: METADATA.TITLE_SUFFIX,
|
||||
description: METADATA.SITE_META_DESCRIPTION,
|
||||
title: METADATA.TITLE,
|
||||
description: METADATA.DESCRIPTION,
|
||||
id: baseUrl,
|
||||
link: baseUrl,
|
||||
language: 'en', // optional, used only in RSS 2.0, possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
|
||||
|
@ -34,8 +23,9 @@ const feed = new Feed({
|
|||
atom: `${baseUrl}/rss/atom.xml`,
|
||||
},
|
||||
});
|
||||
categories.forEach(category => {
|
||||
feed.addCategory(category);
|
||||
|
||||
METADATA.TAGS.forEach(tag => {
|
||||
feed.addCategory(tag);
|
||||
});
|
||||
|
||||
export default function generateRSSFeed(posts: IPost[]) {
|
||||
|
|
Loading…
Reference in New Issue