diff --git a/docs/multisite.md b/docs/multisite.md
new file mode 100644
index 0000000..6f6e47d
--- /dev/null
+++ b/docs/multisite.md
@@ -0,0 +1,6 @@
+# Multisite
+
+In version 1.4, we introduced the getSettings object, which allows you to configure the site's settings via a middleware function in addition to ENV variables. This allows you to configure the site's settings based on the current request, which is useful for multisite setups.
+
+More to come on this approach in the future, but for now you can see more on the Vercel site on how they accomplish this:
+https://vercel.com/guides/nextjs-multi-tenant-application#4.-configure-rewrites-for-multi-tenancy
diff --git a/website/src/components/Header.js b/website/src/components/Header.js
index 940d15d..fd14c34 100644
--- a/website/src/components/Header.js
+++ b/website/src/components/Header.js
@@ -1,6 +1,5 @@
import cx from 'classnames';
import GlobalsContext from 'contexts/GlobalsContext';
-import { META } from 'lib/constants';
import formatMenu from 'lib/formatMenu';
import Link from 'next/link';
import { useContext, useState } from 'react';
@@ -9,6 +8,7 @@ import styles from './Header.module.scss';
export default function Header() {
const globals = useContext(GlobalsContext);
+ const THEME = globals.THEME;
const [navOpen, setNavOpen] = useState(false);
function handleHamburgerClick() {
@@ -104,10 +104,10 @@ export default function Header() {
- {META.siteName && (
+ {THEME.meta.siteName && (
- {META.siteName}
+ {THEME.meta.siteName}
)}
diff --git a/website/src/components/Meta.js b/website/src/components/Meta.js
index eba9b22..2077617 100644
--- a/website/src/components/Meta.js
+++ b/website/src/components/Meta.js
@@ -1,5 +1,4 @@
import GlobalsContext from 'contexts/GlobalsContext';
-import { META, WORDPRESS_URL } from 'lib/constants';
import { trimTrailingSlash } from 'lib/utils';
import { NextSeo } from 'next-seo';
import Head from 'next/head';
@@ -7,29 +6,33 @@ import { useContext } from 'react';
export default function Meta({ title, description, image, seo }) {
const globals = useContext(GlobalsContext);
+ const THEME = globals.THEME;
+ const CONFIG = globals.CONFIG;
// make sure image is absolute
function imagePath(imageUrl) {
// if enabled rewrite WordPress image paths to local (origin often has noindex tags)
- if (META.proxyWordPressImages && WORDPRESS_URL) {
- let newUrl = imageUrl.replace(WORDPRESS_URL, '');
+ if (THEME.meta.proxyWordPressImages && CONFIG.wordpress_url) {
+ let newUrl = imageUrl.replace(CONFIG.wordpress_url, '');
imageUrl = newUrl;
}
// if root relative, make absolute so that twitter doesn't complain
if (
imageUrl.indexOf('http://') === -1 &&
imageUrl.indexOf('https://') === -1 &&
- META.url &&
+ THEME.meta.url &&
imageUrl.startsWith('/')
) {
- imageUrl = trimTrailingSlash(META.url) + imageUrl;
+ imageUrl = trimTrailingSlash(THEME.meta.url) + imageUrl;
}
return imageUrl;
}
// if passing in a title string, append default append if defined in META.
- let generatedTitle = title ? `${title} ${META.titleAppend}` : '';
+ let generatedTitle = title
+ ? `${title} ${THEME.meta.titleAppend}`
+ : '';
// populate SEO settings with either our SEO values if present, otherwise passed in specifics
let seoSettings = {
@@ -79,13 +82,13 @@ export default function Meta({ title, description, image, seo }) {
/>
{/* favicons */}
- {META.icon32 && (
-
+ {THEME.meta.icon32 && (
+
)}
- {META.iconApple && (
+ {THEME.meta.iconApple && (
)}
diff --git a/website/src/components/layouts/LayoutDefault.js b/website/src/components/layouts/LayoutDefault.js
index 0bddea8..924dbe4 100644
--- a/website/src/components/layouts/LayoutDefault.js
+++ b/website/src/components/layouts/LayoutDefault.js
@@ -2,6 +2,9 @@ import Footer from 'components/Footer';
import Header from 'components/Header';
import Meta from 'components/Meta';
import PreviewModeBar from 'components/PreviewModeBar';
+import GlobalsContext from 'contexts/GlobalsContext';
+import { DefaultSeo } from 'next-seo';
+import { useContext } from 'react';
export default function LayoutDefault({
children,
@@ -13,8 +16,23 @@ export default function LayoutDefault({
isRevision,
preview,
}) {
+ const globals = useContext(GlobalsContext);
+ const THEME = globals.THEME;
+
return (
<>
+
o) =>
+ Object.keys(obj).find((key) => predicate(obj[key], key, obj));
+
+ project = findKey(LAUNCH_PROJECT_CONFIGS, function (o) {
+ return o.site_domain == host;
+ });
+
+ if (!project) {
+ // if no match, use the ENV defined project
+ // console.log('no project still after host lookup, checking ENV');
+ project = process.env.NEXT_PUBLIC_LAUNCH_HOST;
+ }
+ }
+
+ if (project && !host) {
+ // console.log('no host provided, must have project only');
+ // todo, set host do we need to repeat logic from middleware?
+ // should be DRY
+ }
+
+ if (project && !LAUNCH_PROJECT_CONFIGS[project]) {
+ throw new Error(
+ `Project '${project}' does not exist in LAUNCH_PROJECT_CONFIGS`,
+ );
+ }
+
+ // https://stackoverflow.com/questions/28044373/use-lo-dash-merge-without-modifying-underlying-object
+ const merged = _merge(
+ {},
+ THEME_BASE,
+ LAUNCH_PROJECT_CONFIGS[project].theme,
+ );
+
+ return {
+ PROJECT: project,
+ CONFIG: LAUNCH_PROJECT_CONFIGS[project].config,
+ THEME: merged,
+ };
+}
diff --git a/website/src/lib/wordpress/index.js b/website/src/lib/wordpress/index.js
index 39aab7a..14ed045 100644
--- a/website/src/lib/wordpress/index.js
+++ b/website/src/lib/wordpress/index.js
@@ -1,11 +1,21 @@
import fetch from 'isomorphic-unfetch';
-import { WORDPRESS_API_URL } from 'lib/constants';
+import { getSettings } from 'lib/getSettings';
import { trimLeadingSlash } from 'lib/utils';
import { queryContent } from './graphql/queryContent';
import { queryGlobals } from './graphql/queryGlobals';
import { queryPosts } from './graphql/queryPosts';
-async function fetchAPI(query, { variables } = {}, token) {
+async function fetchAPI({
+ project = '',
+ query = {},
+ variables = {},
+ token,
+}) {
+ const SETTINGS = getSettings({ project });
+ let wordpress_api_url =
+ process.env.NEXT_PUBLIC_WORDPRESS_API_URL ||
+ process.env.WORDPRESS_API_URL ||
+ SETTINGS.CONFIG.wordpress_api_url;
const headers = { 'Content-Type': 'application/json' };
if (variables?.preview && token) {
@@ -19,7 +29,7 @@ async function fetchAPI(query, { variables } = {}, token) {
// console.log('-------');
// console.log('variables', variables);
// console.log('query', typeof query, query);
- const res = await fetch(WORDPRESS_API_URL, {
+ const res = await fetch(wordpress_api_url, {
method: 'POST',
headers,
body: JSON.stringify({
@@ -41,11 +51,12 @@ async function fetchAPI(query, { variables } = {}, token) {
* To assist with Preview Mode, this will grab status for content by DB id
* (needed for revisions, unpublished content)
*/
-export async function getPreviewContent(
+export async function getPreviewContent({
+ project,
id,
idType = 'DATABASE_ID',
token,
-) {
+}) {
const query = /* GraphQL */ `
query PreviewContent($id: ID!, $idType: ContentNodeIdTypeEnum!) {
contentNode(id: $id, idType: $idType) {
@@ -63,13 +74,12 @@ export async function getPreviewContent(
}
`;
- const data = await fetchAPI(
+ const data = await fetchAPI({
+ project,
query,
- {
- variables: { id, idType, preview: true },
- },
+ variables: { id, idType, preview: true },
token,
- );
+ });
return data.contentNode;
}
@@ -81,7 +91,10 @@ export async function getPreviewContent(
* If a contentType is passed, the allQuery graphql is modified to query for
* only that post type instead of getting posts from any CPT
*/
-export async function getAllContentWithSlug(contentType) {
+export async function getAllContentWithSlug({
+ project,
+ contentType,
+}) {
const query = /* GraphQL */ `
${
contentType
@@ -101,7 +114,9 @@ export async function getAllContentWithSlug(contentType) {
}
`;
- const data = await fetchAPI(query, {
+ const data = await fetchAPI({
+ project,
+ query,
variables: {
contentType,
},
@@ -114,7 +129,7 @@ export async function getAllContentWithSlug(contentType) {
* @param {*} slug pathname from URL
* @returns
*/
-export async function getNodeType(slug) {
+export async function getNodeType({ project, slug }) {
const query = /* GraphQL */ `
query getNodeType($slug: String!) {
nodeByUri(uri: $slug) {
@@ -124,7 +139,11 @@ export async function getNodeType(slug) {
}
`;
- const data = await fetchAPI(query, { variables: { slug } });
+ const data = await fetchAPI({
+ project,
+ query,
+ variables: { slug },
+ });
return data;
}
@@ -132,12 +151,13 @@ export async function getNodeType(slug) {
/**
* Get fields for single page regardless of post type.
*/
-export async function getContent(
+export async function getContent({
+ project,
slug,
preview,
previewData,
options = {},
-) {
+}) {
let draft = false;
if (preview) {
// This is based on Next.js wordpress example: https://github.com/vercel/next.js/blob/canary/examples/cms-wordpress/lib/api.ts#L105-L112
@@ -161,13 +181,12 @@ export async function getContent(
let query = queryContent(draft, options);
- const data = await fetchAPI(
+ const data = await fetchAPI({
+ project,
query,
- {
- variables: { slug, preview: !!preview },
- },
- previewData?.token,
- );
+ variables: { slug, preview: !!preview },
+ token: previewData?.token,
+ });
return data;
}
@@ -177,6 +196,7 @@ export async function getContent(
*/
export async function getPosts({
+ project,
ids,
first = 12,
after = null,
@@ -190,6 +210,8 @@ export async function getPosts({
let query = queryPosts(taxonomyType, taxonomyTerms);
const data = await fetchAPI(query, {
+ project,
+ query,
variables: {
contentTypes,
first,
@@ -208,7 +230,7 @@ export async function getPosts({
/** All Taxonomy Terms */
-export async function getCategories() {
+export async function getCategories({ project }) {
let query = /* GraphQL */ `
query AllCategories {
categories {
@@ -222,15 +244,15 @@ export async function getCategories() {
}
`;
- const data = await fetchAPI(query);
+ const data = await fetchAPI({ project, query });
return data.categories;
}
/**
* Global Props
* */
-export async function getGlobalProps() {
+export async function getGlobalProps({ project }) {
let query = queryGlobals;
- const data = await fetchAPI(query);
+ const data = await fetchAPI({ project, query });
return data;
}
diff --git a/website/src/pages/[[...slug]].js b/website/src/pages/[[...slug]].js
index 084eb91..bdf7798 100644
--- a/website/src/pages/[[...slug]].js
+++ b/website/src/pages/[[...slug]].js
@@ -3,6 +3,7 @@ import LayoutDefault from 'components/layouts/LayoutDefault';
import PostBody from 'components/post/PostBody';
import { GlobalsProvider } from 'contexts/GlobalsContext';
import checkRedirects from 'lib/checkRedirects';
+import { getSettings } from 'lib/getSettings';
import { isStaticFile } from 'lib/utils';
import {
getContent,
@@ -59,6 +60,8 @@ export async function getStaticProps({
preview = false,
previewData,
}) {
+ const SETTINGS = getSettings({});
+
let slug = '/';
if (params.slug?.length) {
@@ -73,7 +76,7 @@ export async function getStaticProps({
};
}
- const globals = await getGlobalProps();
+ const globals = await getGlobalProps({ project: SETTINGS.PROJECT });
// Check for redirects first
const redirect = checkRedirects(
@@ -91,7 +94,10 @@ export async function getStaticProps({
if (!preview) {
// Check nodeType before assuming it's a contentNode. We 404 on nonsupported types, but you could handle.
- const { nodeByUri } = await getNodeType(slug);
+ const { nodeByUri } = await getNodeType({
+ project: SETTINGS.PROJECT,
+ slug,
+ });
if (!nodeByUri?.isContentNode) {
return {
notFound: true,
@@ -101,7 +107,12 @@ export async function getStaticProps({
}
}
- const data = await getContent(slug, preview, previewData);
+ const data = await getContent({
+ project: SETTINGS.PROJECT,
+ slug,
+ preview,
+ previewData,
+ });
if (!preview && !data?.contentNode?.slug) {
return {
@@ -116,6 +127,8 @@ export async function getStaticProps({
globals: {
...globals,
pageOptions: data.contentNode?.acfPageOptions || null,
+ THEME: SETTINGS.THEME,
+ CONFIG: SETTINGS.CONFIG,
},
preview: preview || false,
post: data.contentNode,
@@ -125,7 +138,10 @@ export async function getStaticProps({
}
export async function getStaticPaths() {
- const { contentNodes } = await getAllContentWithSlug();
+ const SETTINGS = getSettings({});
+ const { contentNodes } = await getAllContentWithSlug({
+ project: SETTINGS.PROJECT,
+ });
return {
paths: contentNodes?.nodes.map(({ uri }) => uri) || [],
diff --git a/website/src/pages/_app.js b/website/src/pages/_app.js
index 7c0649e..16d8ee8 100644
--- a/website/src/pages/_app.js
+++ b/website/src/pages/_app.js
@@ -1,24 +1,9 @@
-import { META } from 'lib/constants';
-import { DefaultSeo } from 'next-seo';
import 'styles/global.scss';
export default function App({ Component, pageProps }) {
return (
// You can set sitewide tags in the component
<>
-
>
);
diff --git a/website/src/pages/api/graphcdn.js b/website/src/pages/api/graphcdn.js
index 055615b..cf598fa 100644
--- a/website/src/pages/api/graphcdn.js
+++ b/website/src/pages/api/graphcdn.js
@@ -12,16 +12,15 @@
import fetch from 'isomorphic-unfetch';
-const GRAPHCDN_PURGE_API_URL = process.env.GRAPHCDN_PURGE_API_URL;
-const GRAPHCDN_PURGE_API_TOKEN = process.env.GRAPHCDN_PURGE_API_TOKEN;
+import { getSettings } from 'lib/getSettings';
-async function purgeAllPosts() {
- const response = await fetch(GRAPHCDN_PURGE_API_URL, {
+async function purgeAllPosts(url, token) {
+ const response = await fetch(url, {
method: 'POST', // Always POST purge mutations
body: JSON.stringify({ query: 'mutation { _purgeAll }' }),
headers: {
'Content-Type': 'application/json',
- 'graphcdn-token': GRAPHCDN_PURGE_API_TOKEN,
+ 'graphcdn-token': token,
},
});
@@ -29,6 +28,8 @@ async function purgeAllPosts() {
}
export default async function handler(req, res) {
+ const SETTINGS = getSettings({ ...req });
+
// Only allow POST requests
if (req.method !== 'POST') {
res.setHeader('Allow', 'POST');
@@ -36,7 +37,10 @@ export default async function handler(req, res) {
}
// Verify env variable presense
- if (!GRAPHCDN_PURGE_API_URL || !GRAPHCDN_PURGE_API_TOKEN) {
+ if (
+ !SETTINGS.CONFIG.graphcdn_purge_api_url ||
+ !SETTINGS.CONFIG.graphcdn_purge_api_token
+ ) {
return res
.status(500)
.end('Purge Failed, Graph CDN API endpoint not defined');
@@ -45,7 +49,10 @@ export default async function handler(req, res) {
// @TODO: Process the incoming body.post_id and create a targetted purge request
try {
- const response = await purgeAllPosts();
+ const response = await purgeAllPosts(
+ SETTINGS.CONFIG.graphcdn_purge_api_url,
+ SETTINGS.CONFIG.graphcdn_purge_api_token,
+ );
// eslint-disable-next-line no-console
console.log('CDN data', response.data);
diff --git a/website/src/pages/api/preview.js b/website/src/pages/api/preview.js
index 801e391..7eb868b 100644
--- a/website/src/pages/api/preview.js
+++ b/website/src/pages/api/preview.js
@@ -1,15 +1,25 @@
import { authorize } from 'lib/auth';
+import { getSettings } from 'lib/getSettings';
import { getPreviewContent } from 'lib/wordpress';
const COOKIE_MAX_AGE = 86400;
export default async function preview(req, res) {
+ const SETTINGS = getSettings({ ...req });
+
let accessToken;
const { code, id, path, slug } = req.query;
// Get Auth Token
if (code) {
- const result = await authorize(decodeURIComponent(code));
+ // console.log(
+ // `fetching authorize token at ${SETTINGS.CONFIG.wordpress_url}`,
+ // );
+ const result = await authorize(
+ decodeURIComponent(code),
+ SETTINGS.CONFIG.wordpress_url,
+ );
+ // console.log(`authorize response ${result}`);
accessToken = result.access_token;
} else if (req.previewData.token) {
accessToken = req.previewData.token;
@@ -39,11 +49,12 @@ export default async function preview(req, res) {
}
// Fetch WordPress to check if the provided `id` exists
- const post = await getPreviewContent(
+ const post = await getPreviewContent({
+ project: SETTINGS.PROJECT,
id,
- 'DATABASE_ID',
- accessToken,
- );
+ idType: 'DATABASE_ID',
+ token: accessToken,
+ });
// If the post doesn't exist prevent preview mode from being enabled
if (!post) {
diff --git a/website/src/pages/api/robots.js b/website/src/pages/api/robots.js
index 07a1f81..5faa774 100644
--- a/website/src/pages/api/robots.js
+++ b/website/src/pages/api/robots.js
@@ -1,13 +1,19 @@
-import { META } from 'lib/constants';
+import { getSettings } from 'lib/getSettings';
import { trimTrailingSlash } from 'lib/utils';
export default function handler(req, res) {
+ const SETTINGS = getSettings({ ...req });
+
+ const publicUrl = trimTrailingSlash(
+ `https://${SETTINGS.CONFIG.site_domain}`,
+ );
+ const sitemap = publicUrl + '/sitemap_index.xml';
+ // let sitemap = trimTrailingSlash(META.url) + '/sitemap_index.xml';
+
if (
process.env.VERCEL_ENV === 'production' &&
!process.env.NOINDEX
) {
- let sitemap = trimTrailingSlash(META.url) + '/sitemap_index.xml';
-
res.write(`Sitemap: ${sitemap}`);
res.write('\n');
res.write('User-agent: *');
diff --git a/website/src/pages/api/upstream-proxy.js b/website/src/pages/api/upstream-proxy.js
index a7a5262..dd7785a 100644
--- a/website/src/pages/api/upstream-proxy.js
+++ b/website/src/pages/api/upstream-proxy.js
@@ -1,15 +1,22 @@
import fetch from 'isomorphic-unfetch';
-import { WORDPRESS_URL, META } from 'lib/constants';
+import { getSettings } from 'lib/getSettings';
import { trimTrailingSlash } from 'lib/utils';
import _replace from 'lodash/replace';
-// Global regex search allows replacing all URLs
-const HOSTNAME_REGEX = new RegExp(WORDPRESS_URL, 'g');
-
export default async function proxy(req, res) {
+ const SETTINGS = getSettings({ ...req });
+
+ const WORDPRESS_URL = SETTINGS.CONFIG.wordpress_url;
+ const PUBLIC_URL = trimTrailingSlash(
+ `https://${SETTINGS.CONFIG.site_domain}`,
+ );
+
let content;
let contentType;
+ // Global regex search allows replacing all URLs
+ const HOSTNAME_REGEX = new RegExp(WORDPRESS_URL, 'g');
+
const upstreamRes = await fetch(`${WORDPRESS_URL}${req.url}`, {
redirect: 'manual',
});
@@ -41,11 +48,7 @@ export default async function proxy(req, res) {
// Pathnames where URLs within need to be replaced
if (req.url.includes('sitemap') || req.url.includes('/feed/')) {
- content = _replace(
- content,
- HOSTNAME_REGEX,
- trimTrailingSlash(META.url),
- );
+ content = _replace(content, HOSTNAME_REGEX, PUBLIC_URL);
}
if (req.url.includes('sitemap')) {