Merge pull request #35 from yougotwill/seo_improvements

Seo improvements
This commit is contained in:
William Grant 2021-09-19 14:31:42 +10:00 committed by GitHub
commit 4ac7335460
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 620 additions and 353 deletions

View File

@ -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>

252
components/CustomHead.tsx Normal file
View File

@ -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>
);
}

View File

@ -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>

View File

@ -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',
},
},
};

View File

@ -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',
},
];
},
};

View File

@ -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(

View File

@ -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} />;
}
}

View File

@ -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>

View File

@ -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;

140
pages/api/sitemap.ts Normal file
View File

@ -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();
}

View File

@ -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">

View File

@ -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>

View File

@ -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 />

View File

@ -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

View File

@ -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">

4
public/robots.txt Normal file
View File

@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://oxen.io/sitemap.xml

View File

@ -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) ?? [],

View File

@ -28,6 +28,7 @@ export interface IPost {
description: string;
body: Document;
author?: IAuthor;
publishedDateISO: string;
publishedDate: string;
featureImage?: IFigureImage;
tags: Array<string>;

View File

@ -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 => {

View File

@ -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;
}

View File

@ -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[]) {