feat(recipes): generate recipe pages from airtable (#178995849)

This commit is contained in:
Bhaskar Murthy 2021-07-24 19:30:45 -04:00
parent e4a45a7ea3
commit ff7a5a3728
8 changed files with 164 additions and 170 deletions

51
lib/api/airtable.ts Normal file
View File

@ -0,0 +1,51 @@
import Airtable from 'airtable'
const API_KEY = process.env.AIRTABLE_API_KEY || ''
const BASE_ID = process.env.AIRTABLE_BASE_ID || ''
const base = new Airtable({ apiKey: API_KEY }).base(BASE_ID)
export interface Recipe {
id: string
title: string
slug: string
}
export const getRecipePages = async (): Promise<string[]> =>
new Promise((resolve, reject) =>
base('Recipes')
.select({
view: 'Pages',
filterByFormula: "{Status} = 'Live'",
fields: ['Slug'],
})
.all((error, records = []) => {
if (error) {
reject(error)
} else {
resolve(records.map((recipe) => recipe.get('Slug') as string))
}
})
)
export const getRecipes = async (): Promise<Recipe[]> =>
new Promise((resolve, reject) =>
base('Recipes')
.select({
view: 'Pages',
filterByFormula: "{Status} = 'Live'",
})
.all((error, records = []) => {
if (error) {
reject(error)
} else {
resolve(
records.map((recipe) => ({
id: recipe.getId(),
title: recipe.get('Recipes') as string,
slug: recipe.get('Slug') as string,
}))
)
}
})
)

View File

@ -14,7 +14,7 @@ const isVendure = provider === 'vendure'
module.exports = withCommerceConfig({ module.exports = withCommerceConfig({
commerce, commerce,
i18n: { i18n: {
locales: ['en-US', 'es'], locales: ['en-US'],
defaultLocale: 'en-US', defaultLocale: 'en-US',
}, },
rewrites() { rewrites() {

View File

@ -5,6 +5,7 @@
"dev": "NODE_OPTIONS='--inspect' next dev", "dev": "NODE_OPTIONS='--inspect' next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint",
"analyze": "BUNDLE_ANALYZE=both yarn build", "analyze": "BUNDLE_ANALYZE=both yarn build",
"prettier-fix": "prettier --write .", "prettier-fix": "prettier --write .",
"find:unused": "npx next-unused", "find:unused": "npx next-unused",
@ -21,6 +22,7 @@
"dependencies": { "dependencies": {
"@react-spring/web": "^9.2.1", "@react-spring/web": "^9.2.1",
"@vercel/fetch": "^6.1.0", "@vercel/fetch": "^6.1.0",
"airtable": "^0.11.1",
"autoprefixer": "^10.2.6", "autoprefixer": "^10.2.6",
"body-scroll-lock": "^3.1.5", "body-scroll-lock": "^3.1.5",
"classnames": "^2.3.1", "classnames": "^2.3.1",

View File

@ -1,86 +0,0 @@
import type {
GetStaticPathsContext,
GetStaticPropsContext,
InferGetStaticPropsType,
} from 'next'
import commerce from '@lib/api/commerce'
import { Text } from '@components/ui'
import { Layout } from '@components/common'
import getSlug from '@lib/get-slug'
import { missingLocaleInPages } from '@lib/usage-warns'
import type { Page } from '@commerce/types/page'
import { useRouter } from 'next/router'
export async function getStaticProps({
preview,
params,
locale,
locales,
}: GetStaticPropsContext<{ pages: string[] }>) {
const config = { locale, locales }
const pagesPromise = commerce.getAllPages({ config, preview })
const siteInfoPromise = commerce.getSiteInfo({ config, preview })
const { pages } = await pagesPromise
const { categories } = await siteInfoPromise
const path = params?.pages.join('/')
const slug = locale ? `${locale}/${path}` : path
const pageItem = pages.find((p: Page) =>
p.url ? getSlug(p.url) === slug : false
)
const data =
pageItem &&
(await commerce.getPage({
variables: { id: pageItem.id! },
config,
preview,
}))
const page = data?.page
if (!page) {
// We throw to make sure this fails at build time as this is never expected to happen
throw new Error(`Page with slug '${slug}' not found`)
}
return {
props: { pages, page, categories },
revalidate: 60 * 60, // Every hour
}
}
export async function getStaticPaths({ locales }: GetStaticPathsContext) {
const config = { locales }
const { pages }: { pages: Page[] } = await commerce.getAllPages({ config })
const [invalidPaths, log] = missingLocaleInPages()
const paths = pages
.map((page) => page.url)
.filter((url) => {
if (!url || !locales) return url
// If there are locales, only include the pages that include one of the available locales
if (locales.includes(getSlug(url).split('/')[0])) return url
invalidPaths.push(url)
})
log()
return {
paths,
fallback: 'blocking',
}
}
export default function Pages({
page,
}: InferGetStaticPropsType<typeof getStaticProps>) {
const router = useRouter()
return router.isFallback ? (
<h1>Loading...</h1> // TODO (BC) Add Skeleton Views
) : (
<div className="max-w-2xl mx-8 sm:mx-auto py-20">
{page?.body && <Text html={page.body} />}
</div>
)
}
Pages.Layout = Layout

View File

@ -1,81 +0,0 @@
import type {
GetStaticPathsContext,
GetStaticPropsContext,
InferGetStaticPropsType,
} from 'next'
import { useRouter } from 'next/router'
import commerce from '@lib/api/commerce'
import { Layout } from '@components/common'
import { ProductView } from '@components/product'
export async function getStaticProps({
params,
locale,
locales,
preview,
}: GetStaticPropsContext<{ slug: string }>) {
const config = { locale, locales }
const pagesPromise = commerce.getAllPages({ config, preview })
const siteInfoPromise = commerce.getSiteInfo({ config, preview })
const productPromise = commerce.getProduct({
variables: { slug: params!.slug },
config,
preview,
})
const allProductsPromise = commerce.getAllProducts({
variables: { first: 4 },
config,
preview,
})
const { pages } = await pagesPromise
const { categories } = await siteInfoPromise
const { product } = await productPromise
const { products: relatedProducts } = await allProductsPromise
if (!product) {
throw new Error(`Product with slug '${params!.slug}' not found`)
}
return {
props: {
pages,
product,
relatedProducts,
categories,
},
revalidate: 200,
}
}
export async function getStaticPaths({ locales }: GetStaticPathsContext) {
const { products } = await commerce.getAllProductPaths()
return {
paths: locales
? locales.reduce<string[]>((arr, locale) => {
// Add a product path for every locale
products.forEach((product: any) => {
arr.push(`/${locale}/product${product.path}`)
})
return arr
}, [])
: products.map((product: any) => `/product${product.path}`),
fallback: 'blocking',
}
}
export default function Slug({
product,
relatedProducts,
}: InferGetStaticPropsType<typeof getStaticProps>) {
const router = useRouter()
return router.isFallback ? (
<h1>Loading...</h1>
) : (
<ProductView product={product} relatedProducts={relatedProducts} />
)
}
Slug.Layout = Layout

64
pages/recipes/[slug].tsx Normal file
View File

@ -0,0 +1,64 @@
import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
import { getRecipePages, getRecipes } from '@lib/api/airtable'
import { Text } from '@components/ui'
interface Recipe {
slug: string
title: string
}
export const getStaticProps = async ({
params,
}: GetStaticPropsContext<{ slug: string }>) => {
// const config = { locale, locales }
// const pagesPromise = commerce.getAllPages({ config, preview })
// const siteInfoPromise = commerce.getSiteInfo({ config, preview })
// const { pages } = await pagesPromise
// const { categories } = await siteInfoPromise
// const path = params?.pages.join('/')
const recipes = await getRecipes()
const recipe = recipes.find((recipe) => recipe.slug === params?.slug)
// const pageItem = pages.find((p: Page) =>
// p.url ? getSlug(p.url) === slug : false
// )
// const data =
// pageItem &&
// (await commerce.getPage({
// variables: { id: pageItem.id! },
// config,
// preview,
// }))
// const page = data?.page
if (!recipe) {
// We throw to make sure this fails at build time as this is never expected to happen
throw new Error(`Page with slug '${params?.slug}' not found`)
}
return {
props: { recipe },
revalidate: 60 * 60, // Every hour
}
}
export const getStaticPaths = async () => {
const pages = await getRecipePages()
return {
paths: pages.map((slug) => ({
params: {
slug,
},
})),
fallback: false,
}
}
const Recipe = ({ recipe }: InferGetStaticPropsType<typeof getStaticProps>) => (
<div className="max-w-2xl mx-8 sm:mx-auto py-20">
<Text>{recipe.title}</Text>
</div>
)
export default Recipe

View File

@ -34,6 +34,11 @@
"./framework/shopify", "./framework/shopify",
"./framework/swell", "./framework/swell",
"./framework/vendure", "./framework/vendure",
"./framework/saleor" "./framework/saleor",
"framework/bigcommerce",
"framework/saleor",
"framework/swell",
"framework/vendure",
"framework/local"
] ]
} }

View File

@ -1116,6 +1116,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67"
integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ== integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==
"@types/node@>=8.0.0 <15":
version "14.17.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.5.tgz#b59daf6a7ffa461b5648456ca59050ba8e40ed54"
integrity sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA==
"@types/parse-json@^4.0.0": "@types/parse-json@^4.0.0":
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@ -1140,6 +1145,11 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==
"@types/uuid@8.3.1":
version "8.3.1"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f"
integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==
"@types/websocket@1.0.2": "@types/websocket@1.0.2":
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.2.tgz#d2855c6a312b7da73ed16ba6781815bf30c6187a" resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.2.tgz#d2855c6a312b7da73ed16ba6781815bf30c6187a"
@ -1185,13 +1195,18 @@
async-retry "1.2.3" async-retry "1.2.3"
lru-cache "5.1.1" lru-cache "5.1.1"
abort-controller@3.0.0: abort-controller@3.0.0, abort-controller@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
dependencies: dependencies:
event-target-shim "^5.0.0" event-target-shim "^5.0.0"
abortcontroller-polyfill@^1.4.0:
version "1.7.3"
resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz#1b5b487bd6436b5b764fd52a612509702c3144b5"
integrity sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q==
acorn-node@^1.6.1: acorn-node@^1.6.1:
version "1.8.2" version "1.8.2"
resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8" resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
@ -1243,6 +1258,17 @@ aggregate-error@^3.0.0:
clean-stack "^2.0.0" clean-stack "^2.0.0"
indent-string "^4.0.0" indent-string "^4.0.0"
airtable@^0.11.1:
version "0.11.1"
resolved "https://registry.yarnpkg.com/airtable/-/airtable-0.11.1.tgz#2fda51da04f5e4be7092351ea7be3cfdcf308347"
integrity sha512-33zBuUDhLl+FWWAFxFjS1a+vJr/b+UK//EV943nuiimChWph6YykQjYPmu/GucQ30g7mgaqq+98uPD4rfDHOgg==
dependencies:
"@types/node" ">=8.0.0 <15"
abort-controller "^3.0.0"
abortcontroller-polyfill "^1.4.0"
lodash "^4.17.21"
node-fetch "^2.6.1"
anser@1.4.9: anser@1.4.9:
version "1.4.9" version "1.4.9"
resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.9.tgz#1f85423a5dcf8da4631a341665ff675b96845760" resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.9.tgz#1f85423a5dcf8da4631a341665ff675b96845760"
@ -6044,6 +6070,19 @@ util@^0.12.0:
safe-buffer "^5.1.2" safe-buffer "^5.1.2"
which-typed-array "^1.1.2" which-typed-array "^1.1.2"
uuid@8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuidv4@^6.2.10:
version "6.2.11"
resolved "https://registry.yarnpkg.com/uuidv4/-/uuidv4-6.2.11.tgz#34d5a03324eb38296b87ae523a64233b5286cc27"
integrity sha512-OTS4waH9KplrXNADKo+Q1kT9AHWr8DaC0S5F54RQzEwcUaEzBEWQQlJyDUw/u1bkRhJyqkqhLD4M4lbFbV+89g==
dependencies:
"@types/uuid" "8.3.1"
uuid "8.3.2"
valid-url@1.0.9, valid-url@^1.0.9: valid-url@1.0.9, valid-url@^1.0.9:
version "1.0.9" version "1.0.9"
resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200" resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200"