From 99dd5c8834be2094bbcf85f8b60a9f90946c1f7d Mon Sep 17 00:00:00 2001 From: SheetJS Date: Tue, 16 Aug 2022 05:29:15 -0400 Subject: [PATCH] ssg --- docz/docs/03-demos/20-content.md | 306 ++++++++++++++++++++++++- docz/static/next/[id].js | 47 ++++ docz/static/next/getServerSideProps.js | 29 +++ docz/static/next/getStaticPaths.js | 34 +++ docz/static/next/getStaticProps.js | 29 +++ docz/static/next/index.js | 16 ++ docz/static/next/sheetjs.xlsx | Bin 0 -> 10557 bytes 7 files changed, 460 insertions(+), 1 deletion(-) create mode 100644 docz/static/next/[id].js create mode 100644 docz/static/next/getServerSideProps.js create mode 100644 docz/static/next/getStaticPaths.js create mode 100644 docz/static/next/getStaticProps.js create mode 100644 docz/static/next/index.js create mode 100644 docz/static/next/sheetjs.xlsx diff --git a/docz/docs/03-demos/20-content.md b/docz/docs/03-demos/20-content.md index 64f91f8..8eca7ca 100644 --- a/docz/docs/03-demos/20-content.md +++ b/docz/docs/03-demos/20-content.md @@ -21,6 +21,310 @@ in the parsing logic, issues should then be raised with the SheetJS project. ::: +## NextJS + +:::note + +This was tested against `next v12.2.5` on 2022 August 16. + +::: + +:::caution + +At a high level, there are two ways to pull spreadsheet data into NextJS apps: +loading an asset module or performing the file read operations from the NextJS +lifecycle. At the time of writing, NextJS does not offer an out-of-the-box +asset module solution, so this demo focuses on raw operations. NextJS does not +watch the spreadsheets, so `next dev` hot reloading will not work! + +::: + +The general strategy with NextJS apps is to generate HTML snippets or data from +the lifecycle functions and reference them in the template. + +HTML output can be generated using `XLSX.utils.sheet_to_html` and inserted into +the document using the `dangerouslySetInnerHTML` attribute: + +```jsx +export default function Index({html, type}) { return ( + // ... +// highlight-next-line +
+ // ... +); } +``` + +:::warning + +`fs` cannot be statically imported from the top level in NextJS pages. The +dynamic import must happen within a lifecycle function. For example: + +```js +/* it is safe to import the library from the top level */ +import { readFile, utils, set_fs } from 'xlsx'; +/* it is not safe to import 'fs' from the top level ! */ +// import * as fs from 'fs'; // this will fail +import { join } from 'path'; +import { cwd } from 'process'; + +export async function getServerSideProps() { +// highlight-next-line + set_fs(await import("fs")); // dynamically import 'fs' when needed + const wb = readFile(join(cwd(), "public", "sheetjs.xlsx")); // works + // ... +} +``` + + + +::: + +### Strategies + +#### "Static Site Generation" using `getStaticProps` + +When using `getStaticProps`, the file will be read once during build time. + +```js +import { readFile, set_fs, utils } from 'xlsx'; + +export async function getStaticProps() { + /* read file */ + set_fs(await import("fs")); + const wb = readFile(path_to_file) + + /* generate and return the html from the first worksheet */ + const html = utils.sheet_to_html(wb.Sheets[wb.SheetNames[0]]); + return { props: { html } }; +}; +``` + +#### "Static Site Generation with Dynamic Routes" using `getStaticPaths` + +Typically a static site with dynamic routes has an endpoint `/sheets/[id]` that +implements both `getStaticPaths` and `getStaticProps`. + +- `getStaticPaths` should return an array of worksheet indices: + +```js +export async function getStaticPaths() { + /* read file */ + set_fs(await import("fs")); + const wb = readFile(path); + + /* generate an array of objects that will be used for generating pages */ + const paths = wb.SheetNames.map((name, idx) => ({ params: { id: idx.toString() } })); + return { paths, fallback: false }; +}; +``` + +:::note + +For a pure static site, `fallback` must be set to `false`! + +::: + +- `getStaticProps` will generate the actual HTML for each page: + +```js +export async function getStaticProps(ctx) { + /* read file */ + set_fs(await import("fs")); + const wb = readFile(path); + + /* get the corresponding worksheet and generate HTML */ + const ws = wb.Sheets[wb.SheetNames[ctx.params.id]]; // id from getStaticPaths + const html = utils.sheet_to_html(ws); + return { props: { html } }; +}; +``` + +#### "Server-Side Rendering" using `getServerSideProps` + +:::caution Do not use on a static site + +These routes require a NodeJS dynamic server. Static page generation will fail! + +`getStaticProps` and `getStaticPaths` support SSG. + +`getServerSideProps` is suited for NodeJS hosted deployments where the workbook +changes frequently and a static site is undesirable. + +::: + +When using `getServerSideProps`, the file will be read on each request. + +```js +import { readFile, set_fs, utils } from 'xlsx'; + +export async function getServerSideProps() { + /* read file */ + set_fs(await import("fs")); + const wb = readFile(path_to_file); + + /* generate and return the html from the first worksheet */ + const html = utils.sheet_to_html(wb.Sheets[wb.SheetNames[0]]); + return { props: { html } }; +}; +``` + +### Demo + +
Complete Example (click to show) + +0) Disable NextJS telemetry: + +```js +npx next telemetry disable +``` + +Confirm it is disabled by running + +```js +npx next telemetry status +``` + +1) Set up folder structure. At the end, a `pages` folder with a `sheets` + subfolder must be created. On Linux or macOS or WSL: + +```bash +mkdir -p pages/sheets/ +``` + +2) Download the [test file](pathname:///next/sheetjs.xlsx) and place in the + project root. On Linux or macOS or WSL: + +```bash +curl -LO https://docs.sheetjs.com/next/sheetjs.xlsx +``` + +3) Install dependencies: + +```bash +npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz next +``` + +4) Download test scripts: + +Download and place the following scripts in the `pages` subdirectory: + +- [`index.js`](pathname:///next/index.js) +- [`getServerSideProps.js`](pathname:///next/getServerSideProps.js) +- [`getStaticPaths.js`](pathname:///next/getStaticPaths.js) +- [`getStaticProps.js`](pathname:///next/getStaticProps.js) + +Download [`[id].js`](pathname:///next/%5Bid%5D.js) and place in the +`pages/sheets` subdirectory. + +:::caution Percent-Encoding in the script name + +The `[id].js` script must have the literal square brackets in the name. If your +browser saved the file to `%5Bid%5D.js`. rename the file. + +::: + +On Linux or macOS or WSL: + +```bash +cd pages +curl -LO https://docs.sheetjs.com/next/index.js +curl -LO https://docs.sheetjs.com/next/getServerSideProps.js +curl -LO https://docs.sheetjs.com/next/getStaticPaths.js +curl -LO https://docs.sheetjs.com/next/getStaticProps.js +cd sheets +curl -LOg 'https://docs.sheetjs.com/next/[id].js' +cd ../.. +``` + +5) Test the deployment: + +```bash +npx next +``` + +Open a web browser and access: + +- http://localhost:3000 landing page +- http://localhost:3000/getStaticProps shows data from the first sheet +- http://localhost:3000/getServerSideProps shows data from the first sheet +- http://localhost:3000/getStaticPaths shows a list (3 sheets) + +The individual worksheets are available at + +- http://localhost:3000/sheets/0 +- http://localhost:3000/sheets/1 +- http://localhost:3000/sheets/2 + +6) Stop the dev server and run a production build: + +```bash +npx next build +``` + +The final output will show a list of the routes and types: + +``` +Route (pages) Size First Load JS +┌ ○ / 551 B 81.7 kB +├ ○ /404 194 B 77.2 kB +├ λ /getServerSideProps 602 B 81.7 kB +├ ● /getStaticPaths 2.7 kB 83.8 kB +├ ● /getStaticProps 600 B 81.7 kB +└ ● /sheets/[id] (312 ms) 580 B 81.7 kB + ├ /sheets/0 + ├ /sheets/1 + └ /sheets/2 +``` + +As explained in the summary, the `/getStaticPaths` and `/getStaticProps` routes +are completely static. 3 `/sheets/#` pages were generated, corresponding to 3 +worksheets in the file. `/getServerSideProps` is server-rendered. + +7) Try to build a static site: + +```bash +npx next export +``` + +:::note The static export will fail! + +A static page cannot be generated at this point because `/getServerSideProps` +is still server-rendered. + +::: + +8) Remove `pages/getServerSideProps.js` and rebuild with `npx next build`. + +Inspecting the output, there should be no lines with the `λ` symbol: + +``` +Route (pages) Size First Load JS +┌ ○ / 551 B 81.7 kB +├ ○ /404 194 B 77.2 kB +├ ● /getStaticPaths 2.7 kB 83.8 kB +├ ● /getStaticProps 600 B 81.7 kB +└ ● /sheets/[id] (312 ms) 580 B 81.7 kB + ├ /sheets/0 + ├ /sheets/1 + └ /sheets/2 +``` + +9) Generate the static site: + +```bash +npx next export +``` + +The static site will be written to the `out` subdirectory, which can be hosted with + +```bash +npx http-server out +``` + +The command will start a local webserver on port 8080. + +
+ ## NuxtJS `@nuxt/content` is a file-based CMS for Nuxt, enabling static-site generation @@ -99,7 +403,7 @@ neatly with nested `v-for`: ### Nuxt Content Demo -
Complete Example (click to show) +
Complete Example (click to show) :::note diff --git a/docz/static/next/[id].js b/docz/static/next/[id].js new file mode 100644 index 0000000..7ad71e9 --- /dev/null +++ b/docz/static/next/[id].js @@ -0,0 +1,47 @@ +import Head from 'next/head'; +import { readFile, set_fs, utils } from 'xlsx'; +import { join } from 'path'; +import { cwd } from 'process'; + +export default function Index({type, html, name}) { return (
+ + + {`SheetJS Next.JS ${type} Demo`} + + +
+    

{`SheetJS Next.JS ${type} Demo`}

+ This demo reads from /sheetjs.xlsx

+ {name} +
+
+
); } + +let cache = []; + +export async function getStaticProps(ctx) { + if(!cache || !cache.length) { + set_fs(await import("fs")); + const wb = readFile(join(cwd(), "sheetjs.xlsx")); + cache = wb.SheetNames.map((name) => ({ name, sheet: wb.Sheets[name] })); + } + const entry = cache[ctx.params.id]; + return { + props: { + type: "getStaticPaths", + name: entry.name, + id: ctx.params.id.toString(), + html: entry.sheet ? utils.sheet_to_html(entry.sheet) : "", + }, + } +} + +export async function getStaticPaths() { + set_fs(await import("fs")); + const wb = readFile(join(cwd(), "sheetjs.xlsx")); + cache = wb.SheetNames.map((name) => ({ name, sheet: wb.Sheets[name] })); + return { + paths: wb.SheetNames.map((name, idx) => ({ params: { id: idx.toString() } })), + fallback: false, + }; +} diff --git a/docz/static/next/getServerSideProps.js b/docz/static/next/getServerSideProps.js new file mode 100644 index 0000000..139c752 --- /dev/null +++ b/docz/static/next/getServerSideProps.js @@ -0,0 +1,29 @@ +import Head from 'next/head'; +import { readFile, set_fs, utils } from 'xlsx'; +import { join } from 'path'; +import { cwd } from 'process'; + +export default function Index({type, html}) { return (
+ + + {`SheetJS Next.JS ${type} Demo`} + + +
+    

{`SheetJS Next.JS ${type} Demo`}

+ This demo reads from /sheetjs.xlsx

+ It generates HTML from the first sheet.

+
+
+
); } + +export async function getServerSideProps() { + set_fs(await import("fs")); + const wb = readFile(join(cwd(), "sheetjs.xlsx")) + return { + props: { + type: "getServerSideProps", + html: utils.sheet_to_html(wb.Sheets[wb.SheetNames[0]]), + }, + } +} diff --git a/docz/static/next/getStaticPaths.js b/docz/static/next/getStaticPaths.js new file mode 100644 index 0000000..09affec --- /dev/null +++ b/docz/static/next/getStaticPaths.js @@ -0,0 +1,34 @@ +import Head from 'next/head'; +import Link from "next/link"; +import { readFile, set_fs, utils } from 'xlsx'; +import { join } from 'path'; +import { cwd } from 'process'; + +export default function Index({type, snames}) { return (
+ + + {`SheetJS Next.JS ${type} Demo`} + + +
+    

{`SheetJS Next.JS ${type} Demo`}

+ This demo reads from /sheetjs.xlsx

+ Each worksheet maps to a path:

+ +
+
); } + +export async function getStaticProps() { + set_fs(await import("fs")); + const wb = readFile(join(cwd(), "sheetjs.xlsx")) + return { + props: { + type: "getStaticPaths", + snames: wb.SheetNames, + }, + } +} diff --git a/docz/static/next/getStaticProps.js b/docz/static/next/getStaticProps.js new file mode 100644 index 0000000..280322b --- /dev/null +++ b/docz/static/next/getStaticProps.js @@ -0,0 +1,29 @@ +import Head from 'next/head'; +import { readFile, set_fs, utils } from 'xlsx'; +import { join } from 'path'; +import { cwd } from 'process'; + +export default function Index({type, html}) { return (
+ + + {`SheetJS Next.JS ${type} Demo`} + + +
+    

{`SheetJS Next.JS ${type} Demo`}

+ This demo reads from /sheetjs.xlsx

+ It generates HTML from the first sheet.

+
+
+
); } + +export async function getStaticProps() { + set_fs(await import("fs")); + const wb = readFile(join(cwd(), "sheetjs.xlsx")) + return { + props: { + type: "getStaticProps", + html: utils.sheet_to_html(wb.Sheets[wb.SheetNames[0]]), + }, + } +} diff --git a/docz/static/next/index.js b/docz/static/next/index.js new file mode 100644 index 0000000..ff0a830 --- /dev/null +++ b/docz/static/next/index.js @@ -0,0 +1,16 @@ +import Head from 'next/head'; + +export default function Index() { return (
+ + + SheetJS Next.JS Demo + + +
+    

SheetJS Next.JS Demo

+ This demo reads from /sheetjs.xlsx

+ - getStaticProps

+ - getServerSideProps

+ - getStaticPaths
+
+
); } diff --git a/docz/static/next/sheetjs.xlsx b/docz/static/next/sheetjs.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..beb3b2d5d35d6c35a305dae8e63f20aba1dbf4b9 GIT binary patch literal 10557 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