diff --git a/api/render-social-image/[pageId].ts b/api/render-social-image/[pageId].ts index d72258b..52c3267 100644 --- a/api/render-social-image/[pageId].ts +++ b/api/render-social-image/[pageId].ts @@ -12,7 +12,8 @@ import { // socialImageSubtitle, defaultPageCover, defaultPageIcon, - rootNotionPageId + rootNotionPageId, + socialImageSubtitle } from '../../lib/config' export interface SocialImageConfig { @@ -80,7 +81,8 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { logo: mapNotionImageUrl(defaultPageIcon, block), title: isRootPage ? socialImageTitle - : getBlockTitle(block, recordMap) || socialImageTitle + : getBlockTitle(block, recordMap) || socialImageTitle, + subtitle: isRootPage ? socialImageSubtitle : undefined // subtitle: getPageDescription(block, recordMap) || socialImageSubtitle }) diff --git a/components/Footer.tsx b/components/Footer.tsx index 01840f5..6cd34f4 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import { FaTwitter, FaGithub, FaLinkedin } from 'react-icons/fa' +import * as config from 'lib/config' import styles from './styles.module.css' @@ -8,35 +9,41 @@ export const Footer: React.FC<{}> = () => { ) diff --git a/components/NotionPage.tsx b/components/NotionPage.tsx index b381dd7..8f9ffc5 100644 --- a/components/NotionPage.tsx +++ b/components/NotionPage.tsx @@ -12,20 +12,12 @@ import { NotionRenderer } from 'react-notion-x' // utils import { getBlockTitle } from 'notion-utils' -import * as types from 'lib/types' import { mapPageUrl, getCanonicalPageUrl } from 'lib/map-page-url' -import { mapImageUrl, mapNotionImageUrl } from 'lib/map-image-url' +import { mapNotionImageUrl } from 'lib/map-image-url' import { getPageDescription } from 'lib/get-page-description' -import { - isDev, - api, - siteDescription, - siteAuthorTwitter, - defaultPageCover, - defaultPageCoverPosition, - defaultPageIcon -} from 'lib/config' import { searchNotion } from 'lib/search-notion' +import * as types from 'lib/types' +import * as config from 'lib/config' // components import { CustomFont } from './CustomFont' @@ -38,8 +30,6 @@ import { ReactUtterances } from './ReactUtterances' import styles from './styles.module.css' -const isServer = typeof window === 'undefined' - export const NotionPage: React.FC = ({ site, recordMap, @@ -80,14 +70,14 @@ export const NotionPage: React.FC = ({ const title = getBlockTitle(block, recordMap) || site.name console.log('notion page', { - isDev, + isDev: config.isDev, title, pageId, rootNotionPageId: site.rootNotionPageId, recordMap }) - if (!isServer) { + if (!config.isServer) { // add important objects global window for easy debugging ;(window as any).recordMap = recordMap ;(window as any).block = block @@ -96,13 +86,13 @@ export const NotionPage: React.FC = ({ const siteMapPageUrl = mapPageUrl(site, recordMap, searchParams) const canonicalPageUrl = - !isDev && getCanonicalPageUrl(site, recordMap)(pageId) + !config.isDev && getCanonicalPageUrl(site, recordMap)(pageId) const isBlogPost = block.type === 'page' && block.parent_table === 'collection' - const socialImage = api.renderSocialImage(pageId) + const socialImage = config.api.renderSocialImage(pageId) const socialDescription = - getPageDescription(block, recordMap) ?? siteDescription + getPageDescription(block, recordMap) ?? config.description let comments: React.ReactNode = null // only display comments on blog post pages @@ -129,8 +119,8 @@ export const NotionPage: React.FC = ({ - {siteAuthorTwitter && ( - + {config.twitter && ( + )} {socialDescription && ( @@ -203,9 +193,9 @@ export const NotionPage: React.FC = ({ darkMode={isDarkMode} previewImages={site.previewImages !== false} showCollectionViewDropdown={false} - defaultPageIcon={defaultPageIcon} - defaultPageCover={defaultPageCover} - defaultPageCoverPosition={defaultPageCoverPosition} + defaultPageIcon={config.defaultPageIcon} + defaultPageCover={config.defaultPageCover} + defaultPageCoverPosition={config.defaultPageCoverPosition} mapPageUrl={siteMapPageUrl} mapImageUrl={mapNotionImageUrl} searchNotion={searchNotion} diff --git a/lib/config.ts b/lib/config.ts index 3a2bfdc..528ce84 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -1,43 +1,65 @@ /** * Site-wide app configuration. - * - * @see env.ts for config relating to third-party dependencies. */ -import { getEnv } from './get-env' +import { getSiteConfig, getEnv } from './get-config-value' // where it all starts -- the site's root Notion page -export const rootNotionPageId = '78fc5a4b88d74b0e824e29407e9f1ec1' +export const rootNotionPageId: string = getSiteConfig('rootNotionPageId') // general site config -export const siteName = 'Transitive Bullshit' -export const siteAuthor = 'Travis Fischer' -export const siteAuthorTwitter = 'transitive_bs' -export const siteDomain = 'transitivebullsh.it' -export const siteDescription = - 'Personal site of Travis Fischer aka Transitive Bullshit' -export const siteFavicon = `https://${siteDomain}/favicon.png` -export const socialImageTitle = 'Transitive Bullshit' -export const socialImageSubtitle = 'Hello World! 👋' +export const name: string = getSiteConfig('name') +export const author: string = getSiteConfig('author') +export const domain: string = getSiteConfig('domain') +export const description: string = getSiteConfig('description', 'Notion Blog') + +// social accounts +export const twitter: string | null = getSiteConfig('twitter', null) +export const github: string | null = getSiteConfig('github', null) +export const linkedin: string | null = getSiteConfig('linkedin', null) + +export const socialImageTitle: string | null = getSiteConfig( + 'socialImageTitle', + null +) +export const socialImageSubtitle: string | null = getSiteConfig( + 'socialImageSubtitle', + null +) // default notion values for site-wide consistency (optional; may be overridden on a per-page basis) -export const defaultPageIcon = - 'https://ssfy.io/https%3A%2F%2Fwww.notion.so%2Fimage%2Fhttps%253A%252F%252Fs3-us-west-2.amazonaws.com%252Fsecure.notion-static.com%252F797768e4-f24a-4e65-bd4a-b622ae9671dc%252Fprofile-2020-280w-circle.png%3Ftable%3Dblock%26id%3D78fc5a4b-88d7-4b0e-824e-29407e9f1ec1%26cache%3Dv2' -export const defaultPageCover = - 'https://ssfy.io/https%3A%2F%2Fwww.notion.so%2Fimage%2Fhttps%253A%252F%252Fs3-us-west-2.amazonaws.com%252Fsecure.notion-static.com%252F9fc5ecae-2b4b-4e73-b0d4-918c829ba69f%252FIMG_0259-opt.jpg%3Ftable%3Dblock%26id%3D78fc5a4b-88d7-4b0e-824e-29407e9f1ec1%26cache%3Dv2' -export const defaultPageCoverPosition = 0.1862 +export const defaultPageIcon: string | null = getSiteConfig( + 'defaultPageIcon', + null +) +export const defaultPageCover: string | null = getSiteConfig( + 'defaultPageCover', + null +) +export const defaultPageCoverPosition: number = getSiteConfig( + 'defaultPageCoverPosition', + 0.5 +) + +// image CDN host to proxy all image requests through +export const imageCDNHost: string | null = getSiteConfig('imageCDNHost', null) // whether or not to enable support for LQIP preview images // (requires a Google Firebase collection) -export const isPreviewImageSupportEnabled = true +export const isPreviewImageSupportEnabled: boolean = getSiteConfig( + 'isPreviewImageSupportEnabled', + false +) // ---------------------------------------------------------------------------- export const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV +export const isServer = typeof window === 'undefined' + export const port = getEnv('PORT', '3000') -export const host = isDev ? `http://localhost:${port}` : `https://${siteDomain}` +export const host = isDev ? `http://localhost:${port}` : `https://${domain}` export const apiBaseUrl = `${host}/api` @@ -47,6 +69,8 @@ export const api = { renderSocialImage: (pageId) => `${apiBaseUrl}/render-social-image/${pageId}` } +// ---------------------------------------------------------------------------- + export const fathomId = isDev ? null : getEnv('FATHOM_ID', null) export const fathomConfig = fathomId @@ -54,3 +78,43 @@ export const fathomConfig = fathomId excludedDomains: ['localhost', 'localhost:3000'] } : undefined + +const defaultEnvValueForPreviewImageSupport = + isPreviewImageSupportEnabled && isServer ? undefined : null + +export const googleProjectId = getEnv( + 'GCLOUD_PROJECT', + defaultEnvValueForPreviewImageSupport +) + +export const googleApplicationCredentials = getGoogleApplicationCredentials() + +export const firebaseCollectionImages = getEnv( + 'FIREBASE_COLLECTION_IMAGES', + defaultEnvValueForPreviewImageSupport +) + +// this hack is necessary because vercel doesn't support secret files so we need to encode our google +// credentials a base64-encoded string of the JSON-ified content +function getGoogleApplicationCredentials() { + if (!isPreviewImageSupportEnabled || !isServer) { + return null + } + + try { + const googleApplicationCredentialsBase64 = getEnv( + 'GOOGLE_APPLICATION_CREDENTIALS', + defaultEnvValueForPreviewImageSupport + ) + + return JSON.parse( + Buffer.from(googleApplicationCredentialsBase64, 'base64').toString() + ) + } catch (err) { + console.error( + 'Firebase config error: invalid "GOOGLE_APPLICATION_CREDENTIALS" should be base64-encoded JSON\n' + ) + + throw err + } +} diff --git a/lib/db.ts b/lib/db.ts index f36cd04..d7882f8 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -1,18 +1,17 @@ import * as firestore from '@google-cloud/firestore' import * as types from './types' -import * as env from './env' -import { isPreviewImageSupportEnabled } from './config' +import * as config from './config' -export let db = null -export let images = null +export let db: firestore.Firestore = null +export let images: firestore.CollectionReference = null -if (isPreviewImageSupportEnabled) { +if (config.isPreviewImageSupportEnabled) { db = new firestore.Firestore({ - projectId: env.googleProjectId, - credentials: env.googleApplicationCredentials + projectId: config.googleProjectId, + credentials: config.googleApplicationCredentials }) - images = db.collection(env.firebaseCollectionImages) + images = db.collection(config.firebaseCollectionImages) } async function get( diff --git a/lib/env.ts b/lib/env.ts deleted file mode 100644 index 042e356..0000000 --- a/lib/env.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Config for third-party dependencies. - * - * - Google Cloud (Firebase) - for simple database functionality. - * - Fathom - simple analytics. - * - * @see config.ts for primary configuration. - */ - -import { getEnv } from './get-env' -import { isPreviewImageSupportEnabled } from './config' - -export { isPreviewImageSupportEnabled } - -const defaultEnvValueForPreviewImageSupport = isPreviewImageSupportEnabled - ? undefined - : null - -export const googleProjectId = getEnv( - 'GCLOUD_PROJECT', - defaultEnvValueForPreviewImageSupport -) - -export const googleApplicationCredentials = getGoogleApplicationCredentials() - -export const firebaseCollectionImages = getEnv( - 'FIREBASE_COLLECTION_IMAGES', - defaultEnvValueForPreviewImageSupport -) - -// this hack is necessary because vercel doesn't support secret files so we need to encode our google -// credentials a base64-encoded string of the JSON-ified content -function getGoogleApplicationCredentials() { - if (!isPreviewImageSupportEnabled) { - return null - } - - try { - const googleApplicationCredentialsBase64 = getEnv( - 'GOOGLE_APPLICATION_CREDENTIALS', - defaultEnvValueForPreviewImageSupport - ) - - return JSON.parse( - Buffer.from(googleApplicationCredentialsBase64, 'base64').toString() - ) - } catch (err) { - console.error( - 'Firebase config error: invalid "GOOGLE_APPLICATION_CREDENTIALS" should be base64-encoded JSON\n' - ) - - throw err - } -} diff --git a/lib/get-config-value.ts b/lib/get-config-value.ts new file mode 100644 index 0000000..153e7d7 --- /dev/null +++ b/lib/get-config-value.ts @@ -0,0 +1,37 @@ +import siteConfig from '../site.config' + +if (!siteConfig) { + throw new Error(`Config error: invalid site.config.js`) +} + +export function getSiteConfig(key: string, defaultValue?: T): T { + const value = siteConfig[key] + + if (value !== undefined) { + return value + } + + if (defaultValue !== undefined) { + return defaultValue + } + + throw new Error(`Config error: missing required site config value "${key}"`) +} + +export function getEnv( + key: string, + defaultValue?: string, + env = process.env +): string { + const value = env[key] + + if (value !== undefined) { + return value + } + + if (defaultValue !== undefined) { + return defaultValue + } + + throw new Error(`Config error: missing required env variable "${key}"`) +} diff --git a/lib/get-env.ts b/lib/get-env.ts deleted file mode 100644 index e9bde85..0000000 --- a/lib/get-env.ts +++ /dev/null @@ -1,17 +0,0 @@ -export function getEnv( - key: string, - defaultValue?: string, - env = process.env -): string { - const value = env[key] - - if (value !== undefined) { - return value - } - - if (defaultValue !== undefined) { - return defaultValue - } - - throw new Error(`Config error: missing required env var "${key}"`) -} diff --git a/lib/get-site-for-domain.ts b/lib/get-site-for-domain.ts index c85dd93..f80df25 100644 --- a/lib/get-site-for-domain.ts +++ b/lib/get-site-for-domain.ts @@ -6,8 +6,8 @@ export const getSiteForDomain = async ( ): Promise => { return { domain, - name: config.siteName, + name: config.name, rootNotionPageId: config.rootNotionPageId, - description: config.siteDescription + description: config.description } as types.Site } diff --git a/lib/get-sites.ts b/lib/get-sites.ts index ecde722..e9c8b31 100644 --- a/lib/get-sites.ts +++ b/lib/get-sites.ts @@ -3,5 +3,5 @@ import * as config from './config' import * as types from './types' export async function getSites(): Promise { - return [await getSiteForDomain(config.siteDomain)] + return [await getSiteForDomain(config.domain)] } diff --git a/lib/map-image-url.ts b/lib/map-image-url.ts index 06e1cd9..9098d5f 100644 --- a/lib/map-image-url.ts +++ b/lib/map-image-url.ts @@ -1,6 +1,5 @@ import { Block } from 'notion-types' - -const imageCDNHost = 'https://ssfy.io' +import { imageCDNHost } from './config' export const mapNotionImageUrl = (url: string, block: Block) => { if (!url) { @@ -11,7 +10,7 @@ export const mapNotionImageUrl = (url: string, block: Block) => { return url } - if (url.startsWith(imageCDNHost)) { + if (imageCDNHost && url.startsWith(imageCDNHost)) { return url } @@ -48,6 +47,10 @@ export const mapImageUrl = (imageUrl: string) => { return imageUrl } - // Our proxy uses Cloudflare's global CDN to cache these image assets - return `${imageCDNHost}/${encodeURIComponent(imageUrl)}` + if (imageCDNHost) { + // Our proxy uses Cloudflare's global CDN to cache these image assets + return `${imageCDNHost}/${encodeURIComponent(imageUrl)}` + } else { + return imageUrl + } } diff --git a/lib/oembed.ts b/lib/oembed.ts index 1044e10..71d6ba2 100644 --- a/lib/oembed.ts +++ b/lib/oembed.ts @@ -16,8 +16,8 @@ export const oembed = async ({ // TODO: handle pages with no pageId via domain const pageId = parsePageId(url) - let title = config.siteName - let authorName = config.siteAuthor + let title = config.name + let authorName = config.author try { const page = await getPage(pageId) @@ -50,7 +50,7 @@ export const oembed = async ({ return { version: '1.0', type: 'rich', - provider_name: config.siteName, + provider_name: config.author, provider_url: config.host, title, author_name: authorName, diff --git a/next.config.js b/next.config.js index 389c5ae..b3dfdc8 100644 --- a/next.config.js +++ b/next.config.js @@ -4,8 +4,4 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true' }) -module.exports = withBundleAnalyzer({ - // images: { - // domains: ['ssfy.io'] - // } -}) +module.exports = withBundleAnalyzer({}) diff --git a/pages/[pageId].tsx b/pages/[pageId].tsx index 57a48d6..7f46d86 100644 --- a/pages/[pageId].tsx +++ b/pages/[pageId].tsx @@ -1,5 +1,5 @@ import React from 'react' -import { isDev, siteDomain } from 'lib/config' +import { isDev, domain } from 'lib/config' import { getSiteMaps } from 'lib/get-site-maps' import { resolveNotionPage } from 'lib/resolve-notion-page' import { NotionPage } from 'components' @@ -8,11 +8,11 @@ export const getStaticProps = async (context) => { const rawPageId = context.params.pageId as string try { - const props = await resolveNotionPage(siteDomain, rawPageId) + const props = await resolveNotionPage(domain, rawPageId) return { props, revalidate: 10 } } catch (err) { - console.error('page error', siteDomain, rawPageId, err) + console.error('page error', domain, rawPageId, err) return { props: { diff --git a/pages/_document.tsx b/pages/_document.tsx index 87593d1..aac5189 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -1,13 +1,12 @@ import React from 'react' import Document, { Html, Head, Main, NextScript } from 'next/document' -import { siteFavicon } from 'lib/config' export default class MyDocument extends Document { render() { return ( - + { try { - const props = await resolveNotionPage(siteDomain) + const props = await resolveNotionPage(domain) return { props, revalidate: 10 } } catch (err) { - console.error('page error', siteDomain, err) + console.error('page error', domain, err) return { props: { diff --git a/site.config.js b/site.config.js new file mode 100644 index 0000000..81036f2 --- /dev/null +++ b/site.config.js @@ -0,0 +1,42 @@ +module.exports = { + // where it all starts -- the site's root Notion page + // required + rootNotionPageId: '78fc5a4b88d74b0e824e29407e9f1ec1', + + // basic site info + // required + name: 'Transitive Bullshit', + domain: 'transitivebullsh.it', + author: 'Travis Fischer', + + // open graph metadata + // optional + description: 'Personal site of Travis Fischer aka Transitive Bullshit', + socialImageTitle: 'Transitive Bullshit', + socialImageSubtitle: 'Hello World! 👋', + + // social usernames + // optional + twitter: 'transitive_bs', + github: 'transitive-bullshit', + linkedin: 'fisch2', + + // default notion values for site-wide consistency + // page-specific values will override these site-wide defaults + // optional + defaultPageIcon: + 'https://ssfy.io/https%3A%2F%2Fwww.notion.so%2Fimage%2Fhttps%253A%252F%252Fs3-us-west-2.amazonaws.com%252Fsecure.notion-static.com%252F797768e4-f24a-4e65-bd4a-b622ae9671dc%252Fprofile-2020-280w-circle.png%3Ftable%3Dblock%26id%3D78fc5a4b-88d7-4b0e-824e-29407e9f1ec1%26cache%3Dv2', + defaultPageCover: + 'https://ssfy.io/https%3A%2F%2Fwww.notion.so%2Fimage%2Fhttps%253A%252F%252Fs3-us-west-2.amazonaws.com%252Fsecure.notion-static.com%252F9fc5ecae-2b4b-4e73-b0d4-918c829ba69f%252FIMG_0259-opt.jpg%3Ftable%3Dblock%26id%3D78fc5a4b-88d7-4b0e-824e-29407e9f1ec1%26cache%3Dv2', + defaultPageCoverPosition: 0.1862, + + // image CDN host to proxy all image requests through + // optional + imageCDNHost: 'https://ssfy.io', + + // whether or not to enable support for LQIP preview images + // NOTE: this requires you to set up Google Firebase and add the environment + // variables specified in .env.example + // optional + isPreviewImageSupportEnabled: true +}