feat: refactoring config to site.config.js

This commit is contained in:
Travis Fischer
2021-01-19 20:54:08 -05:00
parent b58d2b69f6
commit 6707fc8d56
17 changed files with 243 additions and 175 deletions

View File

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

View File

@@ -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<{}> = () => {
<footer className={styles.footer}>
<div className={styles.copyright}>Copyright 2021 Travis Fischer</div>
<div className={styles.social}>
<a
className={styles.twitter}
href='https://twitter.com/transitive_bs'
title='Twitter @transitive_bs'
target='_blank'
rel='noopener noreferrer'
>
<FaTwitter />
</a>
{config.twitter && (
<a
className={styles.twitter}
href={`https://twitter.com/${config.twitter}`}
title={`Twitter @${config.twitter}`}
target='_blank'
rel='noopener noreferrer'
>
<FaTwitter />
</a>
)}
<a
className={styles.github}
href='https://github.com/transitive-bullshit'
title='GitHub @transitive-bullshit'
target='_blank'
rel='noopener noreferrer'
>
<FaGithub />
</a>
{config.github && (
<a
className={styles.github}
href={`https://github.com/${config.github}`}
title={`GitHub @${config.github}`}
target='_blank'
rel='noopener noreferrer'
>
<FaGithub />
</a>
)}
<a
className={styles.linkedin}
href='https://www.linkedin.com/in/fisch2'
title='LinkedIn Travis Fischer'
target='_blank'
rel='noopener noreferrer'
>
<FaLinkedin />
</a>
{config.linkedin && (
<a
className={styles.linkedin}
href={`https://www.linkedin.com/in/${config.linkedin}`}
title={`LinkedIn ${config.author}`}
target='_blank'
rel='noopener noreferrer'
>
<FaLinkedin />
</a>
)}
</div>
</footer>
)

View File

@@ -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<types.PageProps> = ({
site,
recordMap,
@@ -80,14 +70,14 @@ export const NotionPage: React.FC<types.PageProps> = ({
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<types.PageProps> = ({
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<types.PageProps> = ({
<meta name='twitter:title' content={title} />
<meta property='twitter:domain' content={site.domain} />
{siteAuthorTwitter && (
<meta name='twitter:creator' content={`@${siteAuthorTwitter}`} />
{config.twitter && (
<meta name='twitter:creator' content={`@${config.twitter}`} />
)}
{socialDescription && (
@@ -203,9 +193,9 @@ export const NotionPage: React.FC<types.PageProps> = ({
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}

View File

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

View File

@@ -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<T extends types.Model>(

View File

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

37
lib/get-config-value.ts Normal file
View File

@@ -0,0 +1,37 @@
import siteConfig from '../site.config'
if (!siteConfig) {
throw new Error(`Config error: invalid site.config.js`)
}
export function getSiteConfig<T>(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}"`)
}

View File

@@ -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}"`)
}

View File

@@ -6,8 +6,8 @@ export const getSiteForDomain = async (
): Promise<types.Site | null> => {
return {
domain,
name: config.siteName,
name: config.name,
rootNotionPageId: config.rootNotionPageId,
description: config.siteDescription
description: config.description
} as types.Site
}

View File

@@ -3,5 +3,5 @@ import * as config from './config'
import * as types from './types'
export async function getSites(): Promise<types.Site[]> {
return [await getSiteForDomain(config.siteDomain)]
return [await getSiteForDomain(config.domain)]
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 (
<Html lang='en'>
<Head>
<link rel='shortcut icon' href={siteFavicon} />
<link rel='shortcut icon' href='/favicon.png' />
<link
rel='apple-touch-icon'

View File

@@ -1,15 +1,15 @@
import React from 'react'
import { siteDomain } from 'lib/config'
import { domain } from 'lib/config'
import { resolveNotionPage } from 'lib/resolve-notion-page'
import { NotionPage } from 'components'
export const getStaticProps = async (context) => {
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: {

42
site.config.js Normal file
View File

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