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')) {