diff --git a/components/BlogPost.tsx b/components/BlogPost.tsx
index f4090ec..eae9361 100644
--- a/components/BlogPost.tsx
+++ b/components/BlogPost.tsx
@@ -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 (
<>
-
-
diff --git a/constants/metadata.tsx b/constants/metadata.tsx
index 1802008..d6fe00f 100644
--- a/constants/metadata.tsx
+++ b/constants/metadata.tsx
@@ -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',
+ },
},
};
diff --git a/next.config.js b/next.config.js
index 9a131bc..e0bd933 100644
--- a/next.config.js
+++ b/next.config.js
@@ -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',
+ },
];
},
};
diff --git a/pages/404.tsx b/pages/404.tsx
index b7e756b..5909fb9 100644
--- a/pages/404.tsx
+++ b/pages/404.tsx
@@ -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 (
-
-
{pageTitle}
-
-
-
-
-
-
-
-
-
+
{
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
;
+ return
;
} else {
- return
;
+ return
;
}
}
diff --git a/pages/_app.tsx b/pages/_app.tsx
index e216815..4a7e73f 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -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) {
<>
-
- {METADATA.TITLE_SUFFIX}
-
-
-
-
-
-
+
diff --git a/pages/_document.tsx b/pages/_document.tsx
index 0a35800..8532f4e 100644
--- a/pages/_document.tsx
+++ b/pages/_document.tsx
@@ -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 {
render() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
- {this.props?.styleTags}
-
+
@@ -59,3 +24,5 @@ export default class CustomDocument extends Document {
);
}
}
+
+export default MyDocument;
diff --git a/pages/api/sitemap.ts b/pages/api/sitemap.ts
new file mode 100644
index 0000000..0ab1082
--- /dev/null
+++ b/pages/api/sitemap.ts
@@ -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 = `
+
+ ${[
+ ...staticPages,
+ ...navigationPages,
+ ...redirectPages,
+ ...bloglistPages,
+ ...taglistPages,
+ ]
+ .map(url => {
+ return `
+
+ ${url}
+ ${new Date().toISOString()}
+ monthly
+ 1.0
+
+ `;
+ })
+ .join('')}
+ ${blogPages
+ .map(post => {
+ return `
+
+ ${post.url}
+ ${post.published}
+ monthly
+ 1.0
+
+ `;
+ })
+ .join('')}
+
+ `;
+
+ res.statusCode = 200;
+ res.setHeader('Content-Type', 'text/xml');
+ res.write(sitemap);
+ res.end();
+}
diff --git a/pages/blog/[[...page]].tsx b/pages/blog/[[...page]].tsx
index 1127db9..0c50e87 100644
--- a/pages/blog/[[...page]].tsx
+++ b/pages/blog/[[...page]].tsx
@@ -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 (
<>
-
- {pageTitle}
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/pages/faq.tsx b/pages/faq.tsx
index 2237fe2..ec63823 100644
--- a/pages/faq.tsx
+++ b/pages/faq.tsx
@@ -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 (
<>
-
- {pageTitle}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/pages/index.tsx b/pages/index.tsx
index 105b5a3..2768ff6 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -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 (
<>
-
-
{METADATA.TITLE_SUFFIX}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{/* Only visible when no pages are open */}
diff --git a/pages/roadmap.tsx b/pages/roadmap.tsx
index b683ed7..ff1c43f 100644
--- a/pages/roadmap.tsx
+++ b/pages/roadmap.tsx
@@ -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 (
<>
-
-
{pageTitle}
-
-
-
-
-
-
-
-
-
-
-
-
-
+
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 (
<>
-
-
{pageTitle}
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..b0fece2
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://oxen.io/sitemap.xml
diff --git a/services/cms.tsx b/services/cms.tsx
index 9943348..22208d9 100644
--- a/services/cms.tsx
+++ b/services/cms.tsx
@@ -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) ?? [],
diff --git a/types/cms.ts b/types/cms.ts
index d5453e1..649dd00 100644
--- a/types/cms.ts
+++ b/types/cms.ts
@@ -28,6 +28,7 @@ export interface IPost {
description: string;
body: Document;
author?: IAuthor;
+ publishedDateISO: string;
publishedDate: string;
featureImage?: IFigureImage;
tags: Array;
diff --git a/utils/links.ts b/utils/links.ts
index e38bd40..22781e3 100644
--- a/utils/links.ts
+++ b/utils/links.ts
@@ -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 => {
diff --git a/utils/metadata.ts b/utils/metadata.ts
deleted file mode 100644
index de125fa..0000000
--- a/utils/metadata.ts
+++ /dev/null
@@ -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;
-}
diff --git a/utils/rss.ts b/utils/rss.ts
index 5dbe3b7..d5008e9 100644
--- a/utils/rss.ts
+++ b/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[]) {