From ff752673f6e97f0f97faf235fdcc4817ceb14392 Mon Sep 17 00:00:00 2001 From: Ben Smithett Date: Mon, 3 Aug 2020 22:31:04 +1000 Subject: [PATCH 1/3] WIP generic collections --- app/prerender.js | 73 +++++++++++++++++++++++++++++++----------------- package.json | 4 ++- yarn.lock | 5 ++++ 3 files changed, 55 insertions(+), 27 deletions(-) diff --git a/app/prerender.js b/app/prerender.js index 90f904e..5f20f19 100644 --- a/app/prerender.js +++ b/app/prerender.js @@ -1,13 +1,9 @@ /* -This module's default export (a function called `prerender`) is responsible for creating your HTML files. - -This one... - -- loops through JS and MDX files in the `pages` directory and renders a HTML file for each. -- creates a JSON Feed -- sets up Fela and Helmet so they can be used in pages and components - -...but Tropical doesn't care *how* you generate your HTML files. Change this at your leisure! +The prerender function exported by this module builds your static site from the source in `pages`. +Make it your own! You could alter this function to: +- Generate a sitemap or XML RSS feed +- Remove the JSON Feed if you don't need it +- Build new pages based on different metadata (e.g. tag or date-based archive pages) */ import packageJSON from '../package.json' @@ -21,6 +17,9 @@ import { RendererProvider } from 'react-fela' import { renderToMarkup } from 'fela-dom' import { Helmet } from 'react-helmet' import { MDXProvider } from '@mdx-js/react' +import dayjs from 'dayjs' +import isSameOrBefore from 'dayjs/plugin/isSameOrBefore' +dayjs.extend(isSameOrBefore) import cssReset from './components/cssReset' import DefaultLayout from './layouts/DefaultLayout' import CodeBlock from './components/CodeBlock/CodeBlock' @@ -44,18 +43,19 @@ export default function prerender(manifest, mode) { }) /* - 2. Create a collection of blog posts (if you're not building a blog you can ignore this step!) - A blog must distinguish "posts" from other non-post pages, like your homepage. + 2. Gather collections based on the meta.collection exported by each page. + We'll pass these collections to each page in step 4 below (so you can do things like list every blog post). + */ + const collections = gatherCollections(pages) + const pageProps = { collections } - By default we look for a `meta` export like this: { collection: 'posts', date: '2020-11-30' } - but you could determine "posts" however you like (e.g. they all live in a `posts` directory, etc) + /* + 3. Build a JSON Feed for the feedCollection specified package.json. */ - const posts = collectPosts(pages) - const pageProps = { posts } buildJSONFeedFile(pageProps) /* - 3. Build a HTML file for each page + 4. Build a HTML file for each page */ pages.forEach(({ PageComponent, meta, urlPath }) => { buildPageFile({ @@ -69,10 +69,19 @@ export default function prerender(manifest, mode) { }) } -function collectPosts(pages) { - return pages - .filter((page) => page.meta && page.meta.collection === 'posts') - .sort((a, b) => new Date(b.meta.date) - new Date(a.meta.date)) +/* +That's the gist of your prerender function! +Read on to understand (or change!) some of the details involved in each step. +*/ + +function gatherCollections(pages) { + return pages.reduce((collections, page) => { + if (page.meta.collection) { + if (!collections[page.meta.collection]) collections[page.meta.collection] = [] + collections[page.meta.collection].push(page) + } + return collections + }, {}) } function buildPageFile({ PageComponent, meta, urlPath, manifest, mode, pageProps = {} }) { @@ -82,7 +91,9 @@ function buildPageFile({ PageComponent, meta, urlPath, manifest, mode, pageProps }) cssReset(felaRenderer) - // 2. Render the page body HTML + // 2. Render the page body HTML, wrapped in provider components that provide: + // - Fela renderer + // - Custom MDX component to add syntax highlighting to fenced code blocks if (!meta) throw new Error(`Page ${urlPath} does not export a meta object`) const Layout = meta.Layout || DefaultLayout const bodyHTML = ReactDOMServer.renderToString( @@ -121,15 +132,15 @@ function buildPageFile({ PageComponent, meta, urlPath, manifest, mode, pageProps }) } -// Get clean URLs by naming all HTML files `index.html` in a folder with the same name as the source file. -// (except source files called `index` - they can be output in place) function cleanURLPathForPage(sourceFile) { + // Get clean URLs by naming all HTML files `index.html` in a folder with the same name as the source file. + // (except source files called `index` - they can be output in place) return path.join('/', sourceFile.dir, sourceFile.name === 'index' ? '' : sourceFile.name) } function buildJSONFeedFile(pageProps) { - const { siteURL, feedTitle } = packageJSON.tropical - const { posts } = pageProps + const { siteURL, feedTitle, feedCollection } = packageJSON.tropical + const items = sortByDate(pageProps.collections[feedCollection]) // A minimal JSON Feed (see https://jsonfeed.org/version/1) const feed = { @@ -137,7 +148,7 @@ function buildJSONFeedFile(pageProps) { title: feedTitle, home_page_url: siteURL, feed_url: `${siteURL}/feed.json`, - items: posts.map(({ PageComponent, urlPath, meta }) => ({ + items: items.map(({ PageComponent, urlPath, meta }) => ({ id: urlPath, url: `${siteURL}${urlPath}`, title: meta.title, @@ -160,6 +171,16 @@ function buildJSONFeedFile(pageProps) { }) } +function sortByDate(collection) { + return collection.sort((a, b) => { + if (a.meta.date && b.meta.date) { + return dayjs(a.meta.date).isSameOrBefore(b.meta.date) ? 1 : -1 + } else { + return 0 + } + }) +} + function documentTemplate({ stylesHTML, bodyHTML, helmet, clientBundlePath }) { return ` diff --git a/package.json b/package.json index 87642c9..24de135 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "babel-loader": "^8.0.6", "chalk": "^4.1.0", "concurrently": "^5.1.0", + "dayjs": "^1.8.31", "fela": "^11.1.2", "fela-dom": "^11.1.2", "file-loader": "^6.0.0", @@ -33,7 +34,8 @@ "webpack-manifest-plugin": "^2.2.0" }, "tropical": { + "feedCollection": "posts", "feedTitle": "Blog Posts on another Tropical site", - "siteURL": "" + "siteURL": "https://example.org" } } diff --git a/yarn.lock b/yarn.lock index 42614f7..9c50402 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3814,6 +3814,11 @@ date-fns@^2.0.1: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.14.0.tgz#359a87a265bb34ef2e38f93ecf63ac453f9bc7ba" integrity sha512-1zD+68jhFgDIM0rF05rcwYO8cExdNqxjq4xP1QKM60Q45mnO6zaMWB4tOzrIr4M4GSLntsKeE4c9Bdl2jhL/yw== +dayjs@^1.8.31: + version "1.8.31" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.8.31.tgz#0cd1114c2539dd5ad9428be0c38df6d4bb40b9d3" + integrity sha512-mPh1mslned+5PuIuiUfbw4CikHk6AEAf2Baxih+wP5fssv+wmlVhvgZ7mq+BhLt7Sr/Hc8leWDiwe6YnrpNt3g== + debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" From 3e80e187d159ce69744da12a44fe73eacf22fffa Mon Sep 17 00:00:00 2001 From: Ben Smithett Date: Tue, 4 Aug 2020 17:57:47 +1000 Subject: [PATCH 2/3] Proper sorting, better error traces & more docs --- app/README.md | 8 ++++++-- app/build.js | 2 +- app/pages/example-post.mdx | 2 +- app/pages/index.js | 4 ++-- app/prerender.js | 32 +++++++++++++++++--------------- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/app/README.md b/app/README.md index 97aba69..8f06a66 100644 --- a/app/README.md +++ b/app/README.md @@ -1,5 +1,9 @@ # Core files +These four files are the "framework" part of Tropical (if you can call it that), but I encourage you to experiment and change them to suit your needs! + +Each [new Tropical version released](https://github.com/bensmithett/tropical/releases) comes with [upgrade instructions](https://github.com/bensmithett/tropical/blob/fde7ad64f181f21abc13d23879b94473af573d03/app/decisions/004_provide_upgrade_instructions-with-new-version-releases.md) so you can selectively bring new features into your site even if you've changed these files. + ## `build.js` A script run in Node.js that: @@ -21,8 +25,8 @@ Optional helpers for enabling targeted component [hydration](https://reactjs.org Prerenders your static site: -- Builds a HTML file from every `.js` or `.mdx` page in `pages` -- Builds a [JSON Feed](https://www.jsonfeed.org/) containing every page with `collection: 'posts'` in its metadata +- Builds a HTML file from every `.js`, `.md` or `.mdx` page in `pages` +- Builds a [JSON Feed](https://www.jsonfeed.org/) for the collection specified in `tropical.feedCollection` in package.json ## Hydration helpers diff --git a/app/build.js b/app/build.js index 94d26c4..4e42c7a 100644 --- a/app/build.js +++ b/app/build.js @@ -184,7 +184,7 @@ rimraf(path.resolve(__dirname, '../output/*'), err => { prerender(manifest, mode) } catch (err) { console.error(chalk.red('🏝 Error in prerender function...')) - console.error(chalk.red(err)) + console.error(chalk.red(err.stack)) } } diff --git a/app/pages/example-post.mdx b/app/pages/example-post.mdx index db1a130..1049b0e 100644 --- a/app/pages/example-post.mdx +++ b/app/pages/example-post.mdx @@ -36,7 +36,7 @@ import image from '../components/ExampleComponent/gunayala.jpg' import ExamplePostList from '../components/ExamplePostList/ExamplePostList' - + --- diff --git a/app/pages/index.js b/app/pages/index.js index 5ffcc81..2c3c54f 100644 --- a/app/pages/index.js +++ b/app/pages/index.js @@ -11,11 +11,11 @@ import ExamplePostList from '../components/ExamplePostList/ExamplePostList' // Note we're importing ExampleComponent wrapped in asIsland, so it will be hydrated in the browser. import { ExampleComponentIsland } from '../components/ExampleComponent/ExampleComponent' -export default function IndexPage ({ posts }) { +export default function IndexPage ({ collections }) { return ( <> - + ) } diff --git a/app/prerender.js b/app/prerender.js index 5f20f19..eef1ef3 100644 --- a/app/prerender.js +++ b/app/prerender.js @@ -75,13 +75,25 @@ Read on to understand (or change!) some of the details involved in each step. */ function gatherCollections(pages) { - return pages.reduce((collections, page) => { + const collections = pages.reduce((acc, page) => { if (page.meta.collection) { - if (!collections[page.meta.collection]) collections[page.meta.collection] = [] - collections[page.meta.collection].push(page) + if (!acc[page.meta.collection]) acc[page.meta.collection] = [] + acc[page.meta.collection].push(page) } - return collections + return acc }, {}) + + Object.keys(collections).forEach((collection) => { + collections[collection].sort((a, b) => { + if (a.meta.date && b.meta.date) { + return dayjs(a.meta.date).isSameOrBefore(b.meta.date) ? 1 : -1 + } else { + return 0 + } + }) + }) + + return collections } function buildPageFile({ PageComponent, meta, urlPath, manifest, mode, pageProps = {} }) { @@ -140,7 +152,7 @@ function cleanURLPathForPage(sourceFile) { function buildJSONFeedFile(pageProps) { const { siteURL, feedTitle, feedCollection } = packageJSON.tropical - const items = sortByDate(pageProps.collections[feedCollection]) + const items = pageProps.collections[feedCollection] // A minimal JSON Feed (see https://jsonfeed.org/version/1) const feed = { @@ -171,16 +183,6 @@ function buildJSONFeedFile(pageProps) { }) } -function sortByDate(collection) { - return collection.sort((a, b) => { - if (a.meta.date && b.meta.date) { - return dayjs(a.meta.date).isSameOrBefore(b.meta.date) ? 1 : -1 - } else { - return 0 - } - }) -} - function documentTemplate({ stylesHTML, bodyHTML, helmet, clientBundlePath }) { return ` From 068ba9cdbe4fa6e631445af83b53073aa965b09f Mon Sep 17 00:00:00 2001 From: Ben Smithett Date: Tue, 4 Aug 2020 20:17:19 +1000 Subject: [PATCH 3/3] Update comment --- app/prerender.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/prerender.js b/app/prerender.js index eef1ef3..e85f77f 100644 --- a/app/prerender.js +++ b/app/prerender.js @@ -44,7 +44,7 @@ export default function prerender(manifest, mode) { /* 2. Gather collections based on the meta.collection exported by each page. - We'll pass these collections to each page in step 4 below (so you can do things like list every blog post). + We'll pass these collections to each page in step 4 below (so you can do things like list your blog posts). */ const collections = gatherCollections(pages) const pageProps = { collections }