diff --git a/docz/docs/03-demos/01-frontend/01-react.md b/docz/docs/03-demos/01-frontend/01-react.md index 79ba1b1..c08ed23 100644 --- a/docz/docs/03-demos/01-frontend/01-react.md +++ b/docz/docs/03-demos/01-frontend/01-react.md @@ -12,7 +12,7 @@ familiarity is assumed. Other demos cover general React deployments, including: -- [Static Site Generation powered by NextJS](/docs/demos/content#nextjs) +- [Static Site Generation powered by NextJS](/docs/demos/static/nextjs) - [iOS and Android applications powered by React Native](/docs/demos/mobile/reactnative) - [Desktop application powered by React Native Windows + macOS](/docs/demos/desktop/reactnative) - [React Data Grid UI component](/docs/demos/grid#react-data-grid) diff --git a/docz/docs/03-demos/01-frontend/02-vue.md b/docz/docs/03-demos/01-frontend/02-vue.md index a31db7a..1c42d73 100644 --- a/docz/docs/03-demos/01-frontend/02-vue.md +++ b/docz/docs/03-demos/01-frontend/02-vue.md @@ -12,7 +12,7 @@ Components (SFC) and VueJS familiarity is assumed. Other demos cover general VueJS deployments, including: -- [Static Site Generation powered by NuxtJS](/docs/demos/content#nuxtjs) +- [Static Site Generation powered by NuxtJS](/docs/demos/static/nuxtjs) - [iOS and Android applications powered by Quasar](/docs/demos/mobile/quasar) - [Desktop application powered by Tauri](/docs/demos/desktop/tauri) - [`vue3-table-lite` UI component](/docs/demos/grid#vue3-table-lite) diff --git a/docz/docs/03-demos/01-frontend/08-bundler.md b/docz/docs/03-demos/01-frontend/08-bundler.md index 07acdab..d88fb90 100644 --- a/docz/docs/03-demos/01-frontend/08-bundler.md +++ b/docz/docs/03-demos/01-frontend/08-bundler.md @@ -1066,7 +1066,7 @@ Access http://localhost:8080 in your web browser. :::note -The [Vite section of the Content demo](/docs/demos/content#vitejs) covers asset +The [Vite section of the Content demo](/docs/demos/static/vitejs) covers asset loaders. They are ideal for static sites pulling data from sheets at build time. ::: diff --git a/docz/docs/03-demos/06-content.md b/docz/docs/03-demos/06-content.md deleted file mode 100644 index a54efb2..0000000 --- a/docz/docs/03-demos/06-content.md +++ /dev/null @@ -1,1022 +0,0 @@ ---- -title: Content and Site Generation ---- - -import current from '/version.js'; - -With the advent of server-side frameworks and content management systems, it is -possible to build sites whose source of truth is a spreadsheet! This demo -explores a number of approaches. - -## Lume - -The official [Sheets plugin](https://lume.land/plugins/sheets/) uses SheetJS -to load data from spreadsheets. - -#### Lume Demo - -:::note - -This was tested against `lume v1.14.2` on 2022 December 27. - -::: - -
Complete Example (click to show) - -1) Create a stock site: - -```bash -mkdir -p sheetjs-lume -cd sheetjs-lume -deno run -Ar https://deno.land/x/lume/init.ts -``` - -When prompted, enter the following options: - -- `Use TypeScript for the configuration file`: press Enter (use default `N`) -- `Do you want to use plugins`: type `sheets` and press Enter - -The project will be configured and modules will be installed. - -2) Download and place in a `_data` folder: - -```bash -mkdir -p _data -curl -L -o _data/pres.numbers https://sheetjs.com/pres.numbers -``` - -3) Create a `index.njk` file that references the file. Since the file is - `pres.numbers`, the parameter name is `pres`: - -```liquid title="index.njk" -

Presidents

- - - {% for row in pres %}{% if (loop.index >= 1) %} - - - - - {% endif %}{% endfor %} - -
NameIndex
{{ row.Name }}{{ row.Index }}
-``` - -4) Run the development server: - -```bash -deno task lume --serve -``` - -To verify it works, access http://localhost:3000 from your web browser. -Adding a new row and saving `pres.numbers` should refresh the data - -5) Stop the server (press `CTRL+C` in the terminal window) and run - -```bash -deno task lume -``` - -This will create a static site in the `_site` folder, which can be served with: - -```bash -npx http-server _site -``` - -Accessing the page http://localhost:8080 will show the page contents. - -
- -## GatsbyJS - -[`gatsby-transformer-excel`](https://www.gatsbyjs.com/plugins/gatsby-transformer-excel/) -generates nodes for each data row of each worksheet. The official documentation -includes examples and more detailed usage instructions. - -:::note - -`gatsby-transformer-excel` is maintained by the Gatsby core team and all bugs -should be directed to the main Gatsby project. If it is determined to be a bug -in the parsing logic, issues should then be raised with the SheetJS project. - -::: - -
GraphQL details (click to show) - -`gatsby-transformer-excel` generates nodes for each data row of each worksheet. -Under the hood, it uses [`sheet_to_json`](/docs/api/utilities#array-output) -to generate row objects using the headers in the first row as keys. - -![pres.xlsx](pathname:///pres.png) - -Assuming the file name is `pres.xlsx` and the data is stored in "Sheet1", the -following nodes will be created: - -```js -[ - { Name: "Bill Clinton", Index: 42, type: "PresXlsxSheet1" }, - { Name: "GeorgeW Bush", Index: 43, type: "PresXlsxSheet1" }, - { Name: "Barack Obama", Index: 44, type: "PresXlsxSheet1" }, - { Name: "Donald Trump", Index: 45, type: "PresXlsxSheet1" }, - { Name: "Joseph Biden", Index: 46, type: "PresXlsxSheet1" }, -] -``` - -The type is a proper casing of the file name concatenated with the sheet name. - -The following query pulls the `Name` and `Index` fields from each row: - -```graphql -{ - allPresXlsxSheet1 { # "all" followed by type - edges { - node { # each line in this block should be a field in the data - Name - Index - } - } - } -} -``` - -
- -:::caution - -`gatsby-transformer-excel` uses an older version of the library. It can be -overridden through a `package.json` override in the latest versions of NodeJS: - -
{`\
-{
-  "overrides": {
-    "xlsx": "https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz"
-  }
-}`}
-
- -::: - -#### GatsbyJS Demo - -
Complete Example (click to show) - -:::note - -This demo was tested on 2022 November 11 against `create-gatsby@3.0.0`. The -generated project used `gatsby@5.0.0` and `react@18.2.0`. - -::: - -1) Run `npm init gatsby -- -y sheetjs-gatsby` to create the template site. - -2) Follow the on-screen instructions for starting the local development server: - -```bash -cd sheetjs-gatsby -npm run develop -``` - -Open a web browser to the displayed URL (typically `http://localhost:8000/`) - -3) Edit `package.json` and add the highlighted lines in the JSON object: - -
{`\
-{
-  // highlight-start
-  "overrides": {
-    "xlsx": "https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz"
-  },
-  // highlight-end
-  "name": "sheetjs-gatsby",
-  "version": "1.0.0",
-`}
-
- -4) Install the library and plugins: - -
{`\
-npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz
-npm i --save gatsby-transformer-excel gatsby-source-filesystem
-`}
-
- -5) Edit `gatsby-config.js` and add the following lines to the `plugins` array: - -```js - plugins: [ - { - resolve: `gatsby-source-filesystem`, - options: { - name: `data`, - path: `${__dirname}/src/data/`, - }, - }, - `gatsby-transformer-excel`, - ], -``` - -Stop and restart the development server process (`npm run develop`). - -6) Make a `src/data` directory, download , and -move the downloaded file into the new folder: - -```bash -mkdir -p src/data -curl -L -o src/data/pres.xlsx https://sheetjs.com/pres.xlsx -``` - -7) To verify, open the GraphiQL editor at `http://localhost:8000/___graphql`. - -There is an editor in the left pane. Paste the following query into the editor: - -```graphql -{ - allPresXlsxSheet1 { - edges { - node { - Name - Index - } - } - } -} -``` - -Press the Execute Query button and data should show up in the right pane: - -![GraphiQL Screenshot](pathname:///gatsby/graphiql.png) - -8) Create a new file `src/pages/pres.js` that uses the query and displays the result: - -```jsx title="src/pages/pres.js" -import { graphql } from "gatsby" -import * as React from "react" - -export const query = graphql`query { - allPresXlsxSheet1 { - edges { - node { - Name - Index - } - } - } -}`; - -const PageComponent = ({data}) => { - return (
{JSON.stringify(data, 2, 2)}
); -}; -export default PageComponent; -``` - -After saving the file, access `http://localhost:8000/pres`. The displayed JSON -is the data that the component receives: - -```js -{ - "allPresXlsxSheet1": { - "edges": [ - { - "node": { - "Name": "Bill Clinton", - "Index": 42 - } - }, - // .... -``` - -9) Change `PageComponent` to display a table based on the data: - -```jsx title="src/pages/pres.js" -import { graphql } from "gatsby" -import * as React from "react" - -export const query = graphql`query { - allPresXlsxSheet1 { - edges { - node { - Name - Index - } - } - } -}`; - -// highlight-start -const PageComponent = ({data}) => { - const rows = data.allPresXlsxSheet1.edges.map(r => r.node); - return ( - - {rows.map(row => ( - - - ))} -
NameIndex
{row.Name}{row.Index}
); -}; -// highlight-end - -export default PageComponent; -``` - -Going back to the browser, `http://localhost:8000/pres` will show a table: - -![Data in Table](pathname:///gatsby/table1.png) - -10) Open the file `src/data/pres.xlsx` in Excel or LibreOffice or Numbers. -Add a new row at the end of the file: - -![New Row in File](pathname:///gatsby/pres2.png) - -Save the file and notice that the table has refreshed with the new data: - -![Updated Table](pathname:///gatsby/table2.png) - -11) Stop the development server and run `npm run build`. Once the build is -finished, the display will confirm that the `/pres` route is static: - -``` -Pages - -┌ src/pages/404.js -│ ├ /404/ -│ └ /404.html -├ src/pages/index.js -│ └ / -└ src/pages/pres.js - └ /pres/ - - ╭────────────────────────────────────────────────────────────────╮ - │ │ - │ (SSG) Generated at build time │ - │ D (DSG) Deferred static generation - page generated at runtime │ - │ ∞ (SSR) Server-side renders at runtime (uses getServerData) │ - │ λ (Function) Gatsby function │ - │ │ - ╰────────────────────────────────────────────────────────────────╯ -``` - -The built page will be placed in `public/pres/index.html`. Open the page with a -text editor and search for "SheetJS" to verify raw HTML was generated: - -```html -SheetJS Dev47 -``` - -
- -## Bundling Data - -Bundlers can run JS code and process assets during development and during site -builds. Custom plugins can extract data from spreadsheets. - -### ViteJS - -:::note - -This demo covers static asset imports. For processing files in the browser, the -["Bundlers" demo](/docs/demos/bundler#vite) includes an example. - -::: - -ViteJS supports static asset imports, but the default raw loader interprets data -as UTF-8 strings. This corrupts binary formats like XLSX and XLS, but a custom -loader can override the default behavior. - -For a pure static site, a plugin can load data into an array of row objects. The -SheetJS work is performed in the plugin. The library is not loaded in the page! - -```js title="vite.config.js" -import { readFileSync } from 'fs'; -import { read, utils } from 'xlsx'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - assetsInclude: ['**/*.xlsx'], // xlsx file should be treated as assets - - plugins: [ - { // this plugin handles ?sheetjs tags - name: "vite-sheet", - transform(code, id) { - if(!id.match(/\?sheetjs$/)) return; - var wb = read(readFileSync(id.replace(/\?sheetjs$/, ""))); - var data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); - return `export default JSON.parse('${JSON.stringify(data)}')`; - } - } - ] -}); -``` - -This loader uses the query `sheetjs`: - -```js title="main.js" -import data from './data.xlsx?sheetjs'; - -document.querySelector('#app').innerHTML = `
-${data.map(row => JSON.stringify(row)).join("\n")}
-
`; -``` - -
Base64 plugin (click to show) - -This loader pulls in data as a Base64 string that can be read with `XLSX.read`. -While this approach works, it is not recommended since it loads the library in -the front-end site. - -```js title="vite.config.js" -import { readFileSync } from 'fs'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - assetsInclude: ['**/*.xlsx'], // mark that xlsx file should be treated as assets - - plugins: [ - { // this plugin handles ?b64 tags - name: "vite-b64-plugin", - transform(code, id) { - if(!id.match(/\?b64$/)) return; - var path = id.replace(/\?b64/, ""); - var data = readFileSync(path, "base64"); - return `export default '${data}'`; - } - } - ] -}); -``` - -When importing using the `b64` query, the raw Base64 string will be exposed. -This can be read directly with `XLSX.read` in JS code: - -```js title="main.js" -import { read, utils } from "xlsx"; - -/* reference workbook */ -import b64 from './data.xlsx?b64'; -/* parse workbook and export first sheet to CSV */ -const wb = await read(b64); -const wsname = wb.SheetNames[0]; -const csv = utils.sheet_to_csv(wb.Sheets[wsname]); - -document.querySelector('#app').innerHTML = `
-${wsname}
-${csv}
-
`; -``` - -
- -## Site Generators - -### NextJS - -:::note - -This was tested against `next v13.1.1` on 2022 December 28. - -::: - -:::info - -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 Reading and writing files during the build process - -`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 - // ... -} -``` - -::: - -:::caution Next 13+ and SWC - -Next 13 switched to the SWC minifier. There are known issues with the minifier. -Until those issues are resolved, SWC should be disabled in `next.config.js`: - -```js title="next.config.js" -module.exports = { -// highlight-next-line - swcMinify: false -}; -``` - -::: - -#### Demo - -
Complete Example (click to show) - -0) Disable NextJS telemetry: - -```js -npx next@13.1.1 telemetry disable -``` - -Confirm it is disabled by running - -```js -npx next@13.1.1 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@13.1.1 -``` - -4) Download test scripts: - -Download and place the following scripts in the `pages` subfolder: - -- [`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` subfolder. - -:::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@13.1.1 -``` - -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 server and run a production build: - -```bash -npx next@13.1.1 build -``` - -The final output will show a list of the routes and types: - -``` -Route (pages) Size First Load JS -┌ ○ / 541 B 77.4 kB -├ ○ /404 181 B 73.7 kB -├ λ /getServerSideProps 594 B 77.4 kB -├ ● /getStaticPaths 2.56 kB 79.4 kB -├ ● /getStaticProps 591 B 77.4 kB -└ ● /sheets/[id] (447 ms) 569 B 77.4 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@13.1.1 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@13.1.1 build` - -Inspecting the output, there should be no lines with the `λ` symbol: - -``` -Route (pages) Size First Load JS -┌ ○ / 541 B 77.4 kB -├ ○ /404 181 B 73.7 kB -├ ● /getStaticPaths 2.56 kB 79.4 kB -├ ● /getStaticProps 591 B 77.4 kB -└ ● /sheets/[id] (459 ms) 569 B 77.4 kB - ├ /sheets/0 - ├ /sheets/1 - └ /sheets/2 -``` - -9) Generate the static site: - -```bash -npx next@13.1.1 export -``` - -The static site will be written to the `out` subfolder, which can be hosted with - -```bash -npx http-server out -``` - -The command will start a local HTTP server on port 8080. - -
- -#### "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 static site generation (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 } }; -}; -``` - - -### NuxtJS - -`@nuxt/content` is a file-based CMS for Nuxt, enabling static-site generation -and on-demand server rendering powered by spreadsheets. - -:::note - -This demo was tested on 2022 November 18 against Nuxt Content `v1.15.1`. - -::: - -#### Nuxt Content Demo - -
Complete Example (click to show) - -:::note - -The project was generated using `create-nuxt-app v4.0.0`. The generated project -used Nuxt `v2.15.8` and Nuxt Content `v1.15.1`. - -::: - -1) Create a stock app: - -```bash -npx create-nuxt-app@4.0.0 SheetJSNuxt -``` - -When prompted, enter the following options: - -- `Project name`: press Enter (use default `SheetJSNuxt`) -- `Programming language`: press Down Arrow (`TypeScript` selected) then Enter -- `Package manager`: select `Npm` and press Enter -- `UI framework`: select `None` and press Enter -- `Nuxt.js modules`: scroll to `Content`, select with Space, then press Enter -- `Linting tools`: press Enter (do not select any Linting tools) -- `Testing framework`: select `None` and press Enter -- `Rendering mode`: select `Universal (SSR / SSG)` and press Enter -- `Deployment target`: select `Static (Static/Jamstack hosting)` and press Enter -- `Development tools`: press Enter (do not select any Development tools) -- `What is your GitHub username?`: press Enter -- `Version control system`: select `None` - -The project will be configured and modules will be installed. - -2) Install the SheetJS library and start the server: - -```bash -cd SheetJSNuxt -npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz -npm run dev -``` - -When the build finishes, the terminal will display a URL like: - -``` -ℹ Listening on: http://localhost:64688/ -``` - -The server is listening on that URL. Open the link in a web browser. - -3) Download and move to the `content` folder. - -```bash -curl -L -o content/pres.xlsx https://sheetjs.com/pres.xlsx -``` - -4) Modify `nuxt.config.js` as follows: - -- Add the following to the top of the script: - -```js -import { readFile, utils } from 'xlsx'; - -// This will be called when the files change -const parseSheet = (file, { path }) => { - // `path` is a path that can be read with `XLSX.readFile` - const wb = readFile(path); - const o = wb.SheetNames.map(name => ({ name, data: utils.sheet_to_json(wb.Sheets[name])})); - return { data: o }; -} -``` - -- Look for the exported object. There should be a `content` property: - -```js - // Content module configuration: https://go.nuxtjs.dev/config-content - content: {}, -``` - -Replace the property with the following definition: - -```js - // content.extendParser allows us to hook into the parsing step - content: { - extendParser: { - // the keys are the extensions that will be matched. The "." is required - ".numbers": parseSheet, - ".xlsx": parseSheet, - ".xls": parseSheet, - // can add other extensions like ".fods" as desired - } - }, -``` - -(If the property is missing, add it to the end of the exported object) - -5) Replace `pages/index.vue` with the following: - -```html - - - - -``` - -The browser should refresh to show the contents of the spreadsheet. If it does -not, click Refresh manually or open a new browser window. - -![Nuxt Demo end of step 5](pathname:///nuxt/nuxt5.png) - -6) To verify that hot loading works, open `pres.xlsx` from the `content` folder -in Excel. Add a new row to the bottom and save the file: - -![Adding a new line to `pres.xlsx`](pathname:///nuxt/nuxl6.png) - -The server terminal window should show a line like: - -``` -ℹ Updated ./content/pres.xlsx @nuxt/content 05:43:37 -``` - -The page should automatically refresh with the new content: - -![Nuxt Demo end of step 6](pathname:///nuxt/nuxt6.png) - -7) Stop the server (press `CTRL+C` in the terminal window) and run - -```bash -npm run generate -``` - -This will create a static site in the `dist` folder, which can be served with: - -```bash -npx http-server dist -``` - -Accessing the page http://localhost:8080 will show the page contents. Verifying -the static nature is trivial: make another change in Excel and save. The page -will not change. - -
- -#### nuxt.config.js configuration - -Through an override in `nuxt.config.js`, Nuxt Content will use custom parsers. -Differences from a stock `create-nuxt-app` config are shown below: - -```js title="nuxt.config.js" -import { readFile, utils } from 'xlsx'; - -// This will be called when the files change -const parseSheet = (file, { path }) => { - // `path` is a path that can be read with `XLSX.readFile` - const wb = readFile(path); - const o = wb.SheetNames.map(name => ({ name, data: utils.sheet_to_json(wb.Sheets[name])})); - return { data: o }; -} - -export default { -// ... - - // content.extendParser allows us to hook into the parsing step - content: { - extendParser: { - // the keys are the extensions that will be matched. The "." is required - ".numbers": parseSheet, - ".xlsx": parseSheet, - ".xls": parseSheet, - // can add other extensions like ".fods" as desired - } - }, - -// ... -} -``` - -#### Template Use - -When a spreadsheet is placed in the `content` folder, Nuxt will find it. The -data can be referenced in a view with `asyncData`. The name should not include -the extension, so `"sheetjs.numbers"` would be referenced as `"sheetjs"`: - -```js - async asyncData ({$content}) { - return { - // $content('sheetjs') will match files with extensions in nuxt.config.js - data: await $content('sheetjs').fetch() - }; - } -``` - -In the template, `data.data` is an array of objects. Each object has a `name` -property for the worksheet name and a `data` array of row objects. This maps -neatly with nested `v-for`: - -```xml - -
- - - - - - - -
{{ row.Name }}{{ row.Index }}
-
-``` diff --git a/docz/docs/03-demos/10-extensions/01-extendscript.md b/docz/docs/03-demos/10-extensions/01-extendscript.md index aa64011..58a63e6 100644 --- a/docz/docs/03-demos/10-extensions/01-extendscript.md +++ b/docz/docs/03-demos/10-extensions/01-extendscript.md @@ -1,7 +1,7 @@ --- title: Photoshop and Creative Suite pagination_prev: demos/server -pagination_next: demos/gsheet +pagination_next: demos/static/index --- import Tabs from '@theme/Tabs'; diff --git a/docz/docs/03-demos/10-extensions/02-chromium.md b/docz/docs/03-demos/10-extensions/02-chromium.md index 61a6005..190be42 100644 --- a/docz/docs/03-demos/10-extensions/02-chromium.md +++ b/docz/docs/03-demos/10-extensions/02-chromium.md @@ -1,7 +1,7 @@ --- title: Chrome and Chromium pagination_prev: demos/server -pagination_next: demos/gsheet +pagination_next: demos/static/index --- :::warning diff --git a/docz/docs/03-demos/10-extensions/03-excelapi.md b/docz/docs/03-demos/10-extensions/03-excelapi.md index 8d1fe6f..9019700 100644 --- a/docz/docs/03-demos/10-extensions/03-excelapi.md +++ b/docz/docs/03-demos/10-extensions/03-excelapi.md @@ -1,7 +1,7 @@ --- title: Excel JavaScript API pagination_prev: demos/server -pagination_next: demos/gsheet +pagination_next: demos/static/index --- :::info diff --git a/docz/docs/03-demos/10-extensions/index.md b/docz/docs/03-demos/10-extensions/index.md index 8268e44..72f2dfb 100644 --- a/docz/docs/03-demos/10-extensions/index.md +++ b/docz/docs/03-demos/10-extensions/index.md @@ -1,7 +1,7 @@ --- title: Extensions pagination_prev: demos/server -pagination_next: demos/gsheet +pagination_next: demos/static/index --- import DocCardList from '@theme/DocCardList'; diff --git a/docz/docs/03-demos/11-static/01-lume.md b/docz/docs/03-demos/11-static/01-lume.md new file mode 100644 index 0000000..0ce59cd --- /dev/null +++ b/docz/docs/03-demos/11-static/01-lume.md @@ -0,0 +1,90 @@ +--- +title: Lume +pagination_prev: demos/extensions/index +pagination_next: demos/gsheet +sidebar_custom_props: + type: native +--- + +Lume is a lightweight, fast and flexible static site generator. + +The official [Sheets plugin](https://lume.land/plugins/sheets/) uses SheetJS to +load data from spreadsheets. New users should consult the official docs. + +Lume supports refreshing data during development. The generated static sites +include the raw data without referencing the underlying spreadsheet files. + +## Lume Demo + +:::note + +This was tested against `lume v1.14.2` on 2022 December 27. + +This example uses the Nunjucks template format. Lume plugins support additional +template formats, including Markdown and JSX. + +::: + +1) Create a stock site: + +```bash +mkdir -p sheetjs-lume +cd sheetjs-lume +deno run -Ar https://deno.land/x/lume/init.ts +``` + +When prompted, enter the following options: + +- `Use TypeScript for the configuration file`: press Enter (use default `N`) +- `Do you want to use plugins`: type `sheets` and press Enter + +The project will be configured and modules will be installed. + +2) Download and place in a `_data` folder: + +```bash +mkdir -p _data +curl -L -o _data/pres.numbers https://sheetjs.com/pres.numbers +``` + +3) Create a `index.njk` file that references the file. Since the file is + `pres.numbers`, the parameter name is `pres`: + +```liquid title="index.njk" +

Presidents

+ + + {% for row in pres %}{% if (loop.index >= 1) %} + + + + + {% endif %}{% endfor %} + +
NameIndex
{{ row.Name }}{{ row.Index }}
+``` + +4) Run the development server: + +```bash +deno task lume --serve +``` + +To verify it works, access http://localhost:3000 from your web browser. +Adding a new row and saving `pres.numbers` should refresh the data + +5) Stop the server (press `CTRL+C` in the terminal window) and run + +```bash +deno task lume +``` + +This will create a static site in the `_site` folder, which can be served with: + +```bash +npx http-server _site +``` + +Accessing the page http://localhost:8080 will show the page contents. + +This site is self-contained and ready for deployment! diff --git a/docz/docs/03-demos/11-static/02-gatsbyjs.md b/docz/docs/03-demos/11-static/02-gatsbyjs.md new file mode 100644 index 0000000..3ae9f9d --- /dev/null +++ b/docz/docs/03-demos/11-static/02-gatsbyjs.md @@ -0,0 +1,292 @@ +--- +title: GatsbyJS +pagination_prev: demos/extensions/index +pagination_next: demos/gsheet +sidebar_custom_props: + type: native +--- + +import current from '/version.js'; + +Gatsby is a framework for creating websites. It uses React components for page +templates and GraphQL for loading data. + +[`gatsby-transformer-excel`](https://www.gatsbyjs.com/plugins/gatsby-transformer-excel/) +is a transformer that generates GraphQL nodes for each row of each worksheet. +The plugin is officially supported by the Gatsby team. The plugin documentation +includes examples and more detailed usage instructions. + +:::note + +`gatsby-transformer-excel` is maintained by the Gatsby core team and all bugs +should be directed to the main Gatsby project. If it is determined to be a bug +in the parsing logic, issues should then be raised with the SheetJS project. + +::: + +:::caution + +`gatsby-transformer-excel` uses an older version of the library. It can be +overridden through a `package.json` override in the latest versions of NodeJS: + +
{`\
+{
+  "overrides": {
+    "xlsx": "https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz"
+  }
+}`}
+
+ +::: + +## GraphQL details + +`gatsby-transformer-excel` generates nodes for each data row of each worksheet. +Under the hood, it uses [`sheet_to_json`](/docs/api/utilities#array-output) +to generate row objects using the headers in the first row as keys. + +![pres.xlsx](pathname:///pres.png) + +Assuming the file name is `pres.xlsx` and the data is stored in "Sheet1", the +following nodes will be created: + +```js +[ + { Name: "Bill Clinton", Index: 42, type: "PresXlsxSheet1" }, + { Name: "GeorgeW Bush", Index: 43, type: "PresXlsxSheet1" }, + { Name: "Barack Obama", Index: 44, type: "PresXlsxSheet1" }, + { Name: "Donald Trump", Index: 45, type: "PresXlsxSheet1" }, + { Name: "Joseph Biden", Index: 46, type: "PresXlsxSheet1" }, +] +``` + +The type is a proper casing of the file name concatenated with the sheet name. + +The following query pulls the `Name` and `Index` fields from each row: + +```graphql +{ + allPresXlsxSheet1 { # "all" followed by type + edges { + node { # each line in this block should be a field in the data + Name + Index + } + } + } +} +``` + +## GatsbyJS Demo + +:::note + +This demo was tested on 2022 November 11 against `create-gatsby@3.0.0`. The +generated project used `gatsby@5.0.0` and `react@18.2.0`. + +::: + +### Project setup + +1) Run `npm init gatsby -- -y sheetjs-gatsby` to create the template site. + +2) Follow the on-screen instructions for starting the local development server: + +```bash +cd sheetjs-gatsby +npm run develop +``` + +Open a web browser to the displayed URL (typically `http://localhost:8000/`) + +3) Edit `package.json` and add the highlighted lines in the JSON object: + +
{`\
+{
+  // highlight-start
+  "overrides": {
+    "xlsx": "https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz"
+  },
+  // highlight-end
+  "name": "sheetjs-gatsby",
+  "version": "1.0.0",
+`}
+
+ +4) Install the library and plugins: + +
{`\
+npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz
+npm i --save gatsby-transformer-excel gatsby-source-filesystem
+`}
+
+ +5) Edit `gatsby-config.js` and add the following lines to the `plugins` array: + +```js + plugins: [ + { + resolve: `gatsby-source-filesystem`, + options: { + name: `data`, + path: `${__dirname}/src/data/`, + }, + }, + `gatsby-transformer-excel`, + ], +``` + +Stop and restart the development server process (`npm run develop`). + +6) Make a `src/data` directory, download , and +move the downloaded file into the new folder: + +```bash +mkdir -p src/data +curl -L -o src/data/pres.xlsx https://sheetjs.com/pres.xlsx +``` + +### GraphiQL test + +7) Open the GraphiQL editor at `http://localhost:8000/___graphql`. + +There is an editor in the left pane. Paste the following query into the editor: + +```graphql +{ + allPresXlsxSheet1 { + edges { + node { + Name + Index + } + } + } +} +``` + +Press the Execute Query button and data should show up in the right pane: + +![GraphiQL Screenshot](pathname:///gatsby/graphiql.png) + +### React page + +8) Create a new file `src/pages/pres.js` that uses the query and displays the result: + +```jsx title="src/pages/pres.js" +import { graphql } from "gatsby" +import * as React from "react" + +export const query = graphql`query { + allPresXlsxSheet1 { + edges { + node { + Name + Index + } + } + } +}`; + +const PageComponent = ({data}) => { + return (
{JSON.stringify(data, 2, 2)}
); +}; +export default PageComponent; +``` + +After saving the file, access `http://localhost:8000/pres`. The displayed JSON +is the data that the component receives: + +```js +{ + "allPresXlsxSheet1": { + "edges": [ + { + "node": { + "Name": "Bill Clinton", + "Index": 42 + } + }, + // .... +``` + +9) Change `PageComponent` to display a table based on the data: + +```jsx title="src/pages/pres.js" +import { graphql } from "gatsby" +import * as React from "react" + +export const query = graphql`query { + allPresXlsxSheet1 { + edges { + node { + Name + Index + } + } + } +}`; + +// highlight-start +const PageComponent = ({data}) => { + const rows = data.allPresXlsxSheet1.edges.map(r => r.node); + return ( + + {rows.map(row => ( + + + ))} +
NameIndex
{row.Name}{row.Index}
); +}; +// highlight-end + +export default PageComponent; +``` + +Going back to the browser, `http://localhost:8000/pres` will show a table: + +![Data in Table](pathname:///gatsby/table1.png) + +### Live refresh + +10) Open the file `src/data/pres.xlsx` in Excel or LibreOffice or Numbers. +Add a new row at the end of the file: + +![New Row in File](pathname:///gatsby/pres2.png) + +Save the file and notice that the table has refreshed with the new data: + +![Updated Table](pathname:///gatsby/table2.png) + +### Static site + +11) Stop the development server and run `npm run build`. Once the build is +finished, the display will confirm that the `/pres` route is static: + +``` +Pages + +┌ src/pages/404.js +│ ├ /404/ +│ └ /404.html +├ src/pages/index.js +│ └ / +└ src/pages/pres.js + └ /pres/ + + ╭────────────────────────────────────────────────────────────────╮ + │ │ + │ (SSG) Generated at build time │ + │ D (DSG) Deferred static generation - page generated at runtime │ + │ ∞ (SSR) Server-side renders at runtime (uses getServerData) │ + │ λ (Function) Gatsby function │ + │ │ + ╰────────────────────────────────────────────────────────────────╯ +``` + +The built page will be placed in `public/pres/index.html`. Open the page with a +text editor and search for "SheetJS" to verify raw HTML was generated: + +```html +SheetJS Dev47 +``` diff --git a/docz/docs/03-demos/11-static/05-vitejs.md b/docz/docs/03-demos/11-static/05-vitejs.md new file mode 100644 index 0000000..9055fec --- /dev/null +++ b/docz/docs/03-demos/11-static/05-vitejs.md @@ -0,0 +1,283 @@ +--- +title: ViteJS +pagination_prev: demos/extensions/index +pagination_next: demos/gsheet +sidebar_custom_props: + type: bundler +--- + +:::note + +This demo covers static asset imports. For processing files in the browser, the +["Bundlers" demo](/docs/demos/bundler#vite) includes an example. + +::: + +## Loaders + +ViteJS supports static asset imports, but the default raw loader interprets data +as UTF-8 strings. This corrupts binary formats like XLSX and XLS, but a custom +loader can override the default behavior. + +:::note Recommendation + +For simple tables of data, ["Pure Data Loader"](#pure-data-loader) is strongly +recommended. The heavy work is performed at build time and the generated site +only includes the raw data. + +For more complex parsing or display logic, ["Base64 Loader"](#base64-loader) is +preferable. Since the raw parsing logic is performed in the page, + +::: + +### Pure Data Loader + +For a pure static site, a plugin can load data into an array of row objects. The +SheetJS work is performed in the plugin. The library is not loaded in the page! + +```js title="vite.config.js" +import { readFileSync } from 'fs'; +import { read, utils } from 'xlsx'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + assetsInclude: ['**/*.xlsx'], // xlsx file should be treated as assets + + plugins: [ + { // this plugin handles ?sheetjs tags + name: "vite-sheet", + transform(code, id) { + if(!id.match(/\?sheetjs$/)) return; + var wb = read(readFileSync(id.replace(/\?sheetjs$/, ""))); + var data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); + return `export default JSON.parse('${JSON.stringify(data)}')`; + } + } + ] +}); +``` + +This loader uses the query `sheetjs`: + +```js title="main.js" +import data from './data.xlsx?sheetjs'; + +document.querySelector('#app').innerHTML = `
+${data.map(row => JSON.stringify(row)).join("\n")}
+
`; +``` + +### Base64 Loader + +This loader pulls in data as a Base64 string that can be read with `XLSX.read`. +While this approach works, it is not recommended since it loads the library in +the front-end site. + +```js title="vite.config.js" +import { readFileSync } from 'fs'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + assetsInclude: ['**/*.xlsx'], // mark that xlsx file should be treated as assets + + plugins: [ + { // this plugin handles ?b64 tags + name: "vite-b64-plugin", + transform(code, id) { + if(!id.match(/\?b64$/)) return; + var path = id.replace(/\?b64/, ""); + var data = readFileSync(path, "base64"); + return `export default '${data}'`; + } + } + ] +}); +``` + +When importing using the `b64` query, the raw Base64 string will be exposed. +This can be read directly with `XLSX.read` in JS code: + +```js title="main.js" +import { read, utils } from "xlsx"; + +/* reference workbook */ +import b64 from './data.xlsx?b64'; +/* parse workbook and export first sheet to CSV */ +const wb = read(b64); +const wsname = wb.SheetNames[0]; +const csv = utils.sheet_to_csv(wb.Sheets[wsname]); + +document.querySelector('#app').innerHTML = `
+${wsname}
+${csv}
+
`; +``` + +## Complete Demo + +:::note + +This demo was tested on 2023 January 14 against `vite v4.0.4`. + +::: + +### Initial Setup + +1) Create a new site using the `vue-ts` template: + +```bash +npm create vite@latest sheetjs-vite -- --template vue-ts +cd sheetjs-vite +npm install +npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz +``` + +2) Replace `vite.config.ts` with the following: + +```js title="vite.config.ts" +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { readFileSync } from 'fs'; +import { read, utils } from 'xlsx'; + +export default defineConfig({ + assetsInclude: ['**/*.xlsx'], // xlsx file should be treated as assets + + plugins: [ + vue(), + { // this plugin handles ?sheetjs tags + name: "vite-sheet", + transform(code, id) { + if(!id.match(/\?sheetjs$/)) return; + var wb = read(readFileSync(id.replace(/\?sheetjs$/, ""))); + var data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); + return `export default JSON.parse('${JSON.stringify(data)}')`; + } + }, + { // this plugin handles ?b64 tags + name: "vite-b64-plugin", + transform(code, id) { + if(!id.match(/\?b64$/)) return; + var path = id.replace(/\?b64/, ""); + var data = readFileSync(path, "base64"); + return `export default '${data}'`; + } + } + ] +}); +``` + +3) Make a `data` folder and download : + +```bash +mkdir -p data +curl -L -o data/pres.xlsx https://sheetjs.com/pres.xlsx +``` + +### Pure Data Test + +4) Run the dev server: + +```bash +npm run dev +``` + +Open a browser window to the displayed URL (typically http://localhost:5173 ) + +5) Replace the component `src/components/HelloWorld.vue` with: + +```html title="src/components/HelloWorld.vue" + + + +``` + +Save and refresh the page. A data table should be displayed + +6) Stop the dev server and build the site + +```bash +npm run build +npx http-server dist/ +``` + +The terminal will display a url like http://127.0.0.1:8080. Access that page +with a web browser. + +7) To confirm that only the raw data is present in the page, view the page +source. The code will reference some script like `/assets/index-HASH.js`. +Open that script. Searching for `Bill Clinton` reveals the following: + +``` +JSON.parse('[{"Name":"Bill Clinton","Index":42} +``` + +Searching for `BESSELJ` should reveal no results. The SheetJS scripts are not +included in the final site! + +### Base64 Test + +8) Run the dev server: + +```bash +npm run dev +``` + +Open a browser window to the displayed URL. + +9) Replace the component `src/components/HelloWorld.vue` with: + +```html title="src/components/HelloWorld.vue" + + + +``` + +10) Stop the dev server and build the site + +```bash +npm run build +npx http-server dist/ +``` + +The terminal will display a url like http://127.0.0.1:8080. Access that page +with a web browser. + +11) To confirm that only the raw data is present in the page, view the page +source. The code will reference some script like `/assets/index-HASH.js`. +Open that script. Searching for `Bill Clinton` should yield no results. +Searching for `BESSELJ` should match the code: + +``` +425:"BESSELJ" +``` + +The SheetJS library is embedded in the final site. diff --git a/docz/docs/03-demos/11-static/08-nextjs.md b/docz/docs/03-demos/11-static/08-nextjs.md new file mode 100644 index 0000000..fb742d2 --- /dev/null +++ b/docz/docs/03-demos/11-static/08-nextjs.md @@ -0,0 +1,327 @@ +--- +title: NextJS +pagination_prev: demos/extensions/index +pagination_next: demos/gsheet +--- + +:::note + +This was tested against `next v13.1.1` on 2023 January 14. + +::: + +:::info + +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 Reading and writing files during the build process + +`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 + // ... +} +``` + +::: + +:::caution Next 13+ and SWC + +Next 13 switched to the SWC minifier. There are known issues with the minifier. +Until those issues are resolved, SWC should be disabled in `next.config.js`: + +```js title="next.config.js" +module.exports = { +// highlight-next-line + swcMinify: false +}; +``` + +::: + +## NextJS Strategies + +NextJS currently provides 3 strategies: + +- "Static Site Generation" using `getStaticProps` +- "SSG with Dynamic Routes" using `getStaticPaths` +- "Server-Side Rendering" using `getServerSideProps` + +### Static Site Generation + +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 } }; +}; +``` + +### Dynamic Routes + +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 + +:::caution Do not use on a static site + +These routes require a NodeJS dynamic server. Static page generation will fail! + +`getStaticProps` and `getStaticPaths` support static site generation (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 + +0) Disable NextJS telemetry: + +```js +npx next@13.1.1 telemetry disable +``` + +Confirm it is disabled by running + +```js +npx next@13.1.1 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@13.1.1 +``` + +4) Download test scripts: + +Download and place the following scripts in the `pages` subfolder: + +- [`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` subfolder. + +:::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@13.1.1 +``` + +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 server and run a production build: + +```bash +npx next@13.1.1 build +``` + +The final output will show a list of the routes and types: + +``` +Route (pages) Size First Load JS +┌ ○ / 541 B 77.4 kB +├ ○ /404 181 B 73.7 kB +├ λ /getServerSideProps 594 B 77.4 kB +├ ● /getStaticPaths 2.56 kB 79.4 kB +├ ● /getStaticProps 591 B 77.4 kB +└ ● /sheets/[id] (447 ms) 569 B 77.4 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@13.1.1 export +``` + +:::note The static export will fail! + +A static page cannot be generated at this point because `/getServerSideProps` +is still server-rendered. + +::: + +8) Delete `pages/getServerSideProps.js` and rebuild: + +```bash +rm -f pages/getServerSideProps.js +npx next@13.1.1 build +``` + +Inspecting the output, there should be no lines with the `λ` symbol: + +``` +Route (pages) Size First Load JS +┌ ○ / 541 B 77.4 kB +├ ○ /404 181 B 73.7 kB +├ ● /getStaticPaths 2.56 kB 79.4 kB +├ ● /getStaticProps 591 B 77.4 kB +└ ● /sheets/[id] (459 ms) 569 B 77.4 kB + ├ /sheets/0 + ├ /sheets/1 + └ /sheets/2 +``` + +9) Generate the static site: + +```bash +npx next@13.1.1 export +``` + +The static site will be written to the `out` subfolder, which can be hosted with + +```bash +npx http-server out +``` + +The command will start a local HTTP server for testing the generated site. Note +that `/getServerSideProps` will 404 since the page was removed. diff --git a/docz/docs/03-demos/11-static/09-nuxtjs.md b/docz/docs/03-demos/11-static/09-nuxtjs.md new file mode 100644 index 0000000..0c5684f --- /dev/null +++ b/docz/docs/03-demos/11-static/09-nuxtjs.md @@ -0,0 +1,250 @@ +--- +title: NuxtJS +pagination_prev: demos/extensions/index +pagination_next: demos/gsheet +--- + +### NuxtJS + +`@nuxt/content` is a file-based CMS for Nuxt, enabling static-site generation +and on-demand server rendering powered by spreadsheets. + +:::note + +This demo was tested on 2022 November 18 against Nuxt Content `v1.15.1`. + +::: + +:::warning + +Nuxt Content `v2` (NuxtJS `v3`) employs a different architecture from `v1`. +There are known bugs related to corrupted binary spreadsheet files. + +Greenfield projects should stick to the stable NuxtJS + Nuxt Content versions +until the issues are resolved. + +::: + +## Configuration + +Through an override in `nuxt.config.js`, Nuxt Content will use custom parsers. +Differences from a stock `create-nuxt-app` config are shown below: + +```js title="nuxt.config.js" +import { readFile, utils } from 'xlsx'; + +// This will be called when the files change +const parseSheet = (file, { path }) => { + // `path` is a path that can be read with `XLSX.readFile` + const wb = readFile(path); + const o = wb.SheetNames.map(name => ({ name, data: utils.sheet_to_json(wb.Sheets[name])})); + return { data: o }; +} + +export default { +// ... + + // content.extendParser allows us to hook into the parsing step + content: { + extendParser: { + // the keys are the extensions that will be matched. The "." is required + ".numbers": parseSheet, + ".xlsx": parseSheet, + ".xls": parseSheet, + // can add other extensions like ".fods" as desired + } + }, + +// ... +} +``` + +## Template Use + +When a spreadsheet is placed in the `content` folder, Nuxt will find it. The +data can be referenced in a view with `asyncData`. The name should not include +the extension, so `"sheetjs.numbers"` would be referenced as `"sheetjs"`: + +```js + async asyncData ({$content}) { + return { + // $content('sheetjs') will match files with extensions in nuxt.config.js + data: await $content('sheetjs').fetch() + }; + } +``` + +In the template, `data.data` is an array of objects. Each object has a `name` +property for the worksheet name and a `data` array of row objects. This maps +neatly with nested `v-for`: + +```xml + +
+ + + + + + + +
{{ row.Name }}{{ row.Index }}
+
+``` + +## Nuxt Content Demo + +:::note + +The project was generated using `create-nuxt-app v4.0.0`. The generated project +used Nuxt `v2.15.8` and Nuxt Content `v1.15.1`. + +::: + +1) Create a stock app: + +```bash +npx create-nuxt-app@4.0.0 SheetJSNuxt +``` + +When prompted, enter the following options: + +- `Project name`: press Enter (use default `SheetJSNuxt`) +- `Programming language`: press Down Arrow (`TypeScript` selected) then Enter +- `Package manager`: select `Npm` and press Enter +- `UI framework`: select `None` and press Enter +- `Nuxt.js modules`: scroll to `Content`, select with Space, then press Enter +- `Linting tools`: press Enter (do not select any Linting tools) +- `Testing framework`: select `None` and press Enter +- `Rendering mode`: select `Universal (SSR / SSG)` and press Enter +- `Deployment target`: select `Static (Static/Jamstack hosting)` and press Enter +- `Development tools`: press Enter (do not select any Development tools) +- `What is your GitHub username?`: press Enter +- `Version control system`: select `None` + +The project will be configured and modules will be installed. + +2) Install the SheetJS library and start the server: + +```bash +cd SheetJSNuxt +npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz +npm run dev +``` + +When the build finishes, the terminal will display a URL like: + +``` +ℹ Listening on: http://localhost:64688/ +``` + +The server is listening on that URL. Open the link in a web browser. + +3) Download and move to the `content` folder. + +```bash +curl -L -o content/pres.xlsx https://sheetjs.com/pres.xlsx +``` + +4) Modify `nuxt.config.js` as follows: + +- Add the following to the top of the script: + +```js +import { readFile, utils } from 'xlsx'; + +// This will be called when the files change +const parseSheet = (file, { path }) => { + // `path` is a path that can be read with `XLSX.readFile` + const wb = readFile(path); + const o = wb.SheetNames.map(name => ({ name, data: utils.sheet_to_json(wb.Sheets[name])})); + return { data: o }; +} +``` + +- Look for the exported object. There should be a `content` property: + +```js + // Content module configuration: https://go.nuxtjs.dev/config-content + content: {}, +``` + +Replace the property with the following definition: + +```js + // content.extendParser allows us to hook into the parsing step + content: { + extendParser: { + // the keys are the extensions that will be matched. The "." is required + ".numbers": parseSheet, + ".xlsx": parseSheet, + ".xls": parseSheet, + // can add other extensions like ".fods" as desired + } + }, +``` + +(If the property is missing, add it to the end of the exported object) + +5) Replace `pages/index.vue` with the following: + +```html + + + + +``` + +The browser should refresh to show the contents of the spreadsheet. If it does +not, click Refresh manually or open a new browser window. + +![Nuxt Demo end of step 5](pathname:///nuxt/nuxt5.png) + +6) To verify that hot loading works, open `pres.xlsx` from the `content` folder +in Excel. Add a new row to the bottom and save the file: + +![Adding a new line to `pres.xlsx`](pathname:///nuxt/nuxl6.png) + +The server terminal window should show a line like: + +``` +ℹ Updated ./content/pres.xlsx @nuxt/content 05:43:37 +``` + +The page should automatically refresh with the new content: + +![Nuxt Demo end of step 6](pathname:///nuxt/nuxt6.png) + +7) Stop the server (press `CTRL+C` in the terminal window) and run + +```bash +npm run generate +``` + +This will create a static site in the `dist` folder, which can be served with: + +```bash +npx http-server dist +``` + +Accessing the page http://localhost:8080 will show the page contents. Verifying +the static nature is trivial: make another change in Excel and save. The page +will not change. diff --git a/docz/docs/03-demos/11-static/_category_.json b/docz/docs/03-demos/11-static/_category_.json new file mode 100644 index 0000000..63b54c4 --- /dev/null +++ b/docz/docs/03-demos/11-static/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Content and Static Sites", + "position": 11 +} \ No newline at end of file diff --git a/docz/docs/03-demos/11-static/index.md b/docz/docs/03-demos/11-static/index.md new file mode 100644 index 0000000..61e79b4 --- /dev/null +++ b/docz/docs/03-demos/11-static/index.md @@ -0,0 +1,86 @@ +--- +title: Content and Static Sites +pagination_prev: demos/extensions/index +pagination_next: demos/gsheet +--- + +import DocCardList from '@theme/DocCardList'; +import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; + +With the advent of server-side frameworks and content management systems, it is +possible to build sites whose source of truth is a spreadsheet! This demo +explores a number of approaches. + +## Flow + +At its core, the site generators provide a structure for supplying content and +templates. During a publish step, the generators will analyze the content and +generate web pages by applying the template to the content. It is spiritually +similar to "Mail Merge". + +This separation of content and presentation makes it easy for analysts and other +spreadsheet users to generate files with their data and quickly update websites! + +#### Live Reload + +Many frameworks offer a development flow which watches files for changes. When +using a spreadsheet as the data source, this allows the content creators to see +updates in a preview environment as they make changes to the spreadsheet! + +The following GIF animation shows a static site generator in development mode. +The user is editing a spreadsheet with Numbers. Every time the page is saved, +the browser refreshes to show the new content. + +![Live example](pathname:///static/live.gif) + +## Ecosystem + +:::note Recommendation + +It is strongly recommended to use a framework that provides an official plugin +for working with SheetJS. Lume is a great choice for getting started. GatsbyJS +is excellent for teams well-versed in the React JS framework. + +::: + +### Official + +Some frameworks provide official extensions They are strongly recommended for +greenfield projects. Demos: + +
    {useCurrentSidebarCategory().items.filter(item => item.customProps?.type == "native").map(item => { + const listyle = (item.customProps?.icon) ? { + listStyleImage: `url("${item.customProps.icon}")` + } : {}; + return (
  • + {item.label}{item.customProps?.summary && (" - " + item.customProps.summary)} +
  • ); +})}
+ + +### Bundlers + +Bundlers can run JS code and process assets during development and during site +builds. Custom plugins can extract data from spreadsheets. Demos: + +
    {useCurrentSidebarCategory().items.filter(item => item.customProps?.type == "bundler").map(item => { + const listyle = (item.customProps?.icon) ? { + listStyleImage: `url("${item.customProps.icon}")` + } : {}; + return (
  • + {item.label}{item.customProps?.summary && (" - " + item.customProps.summary)} +
  • ); +})}
+ +### Workarounds + +Other site generators require workarounds for various limitations and assumptions: + +
    {useCurrentSidebarCategory().items.filter(item => !item?.customProps?.type).map(item => { + const listyle = (item.customProps?.icon) ? { + listStyleImage: `url("${item.customProps.icon}")` + } : {}; + return (
  • + {item.label}{item.customProps?.summary && (" - " + item.customProps.summary)} +
  • ); +})}
diff --git a/docz/docs/03-demos/25-gsheet.md b/docz/docs/03-demos/25-gsheet.md index 52196dd..4da556b 100644 --- a/docz/docs/03-demos/25-gsheet.md +++ b/docz/docs/03-demos/25-gsheet.md @@ -1,6 +1,6 @@ --- title: Google Sheets -pagination_prev: demos/extensions/index +pagination_prev: demos/static/index --- import Tabs from '@theme/Tabs'; diff --git a/docz/docs/03-demos/index.md b/docz/docs/03-demos/index.md index f075b10..564e693 100644 --- a/docz/docs/03-demos/index.md +++ b/docz/docs/03-demos/index.md @@ -55,23 +55,35 @@ run in the web browser, demos will include interactive examples. - [`NeutralinoJS`](/docs/demos/desktop/neutralino) - [`React Native for Desktop`](/docs/demos/desktop/reactnative) +### Content Management and Static Sites + +- [`Lume`](/docs/demos/static/lume) +- [`GatsbyJS`](/docs/demos/static/gatsbyjs) +- [`ViteJS`](/docs/demos/static/vitejs) +- [`NextJS`](/docs/demos/static/nextjs) +- [`NuxtJS`](/docs/demos/static/nuxtjs) + +### App Extensions + +- [`Excel JavaScript API`](/docs/demos/extensions/excelapi) +- [`ExtendScript for Adobe Apps`](/docs/demos/extensions/extendscript) +- [`Chrome and Chromium Extensions`](/docs/demos/extensions/chromium) + +### Cloud Platforms + +- [`Amazon Web Services`](/docs/demos/cloud/aws) +- [`Azure Functions and Storage`](/docs/demos/cloud/azure) +- [`NetSuite SuiteScript`](/docs/demos/cloud/netsuite) +- [`Salesforce Lightning Web Components`](/docs/demos/cloud/salesforce) + ### Platforms and Integrations - [`Command-Line Tools`](/docs/demos/cli) -- [`iOS and Android Mobile Applications`](/docs/demos/mobile) - [`NodeJS Server-Side Processing`](/docs/demos/server#nodejs) - [`Deno Server-Side Processing`](/docs/demos/server#deno) -- [`Content Management and Static Sites`](/docs/demos/content) -- [`Chrome and Chromium Extensions`](/docs/demos/chromium) - [`Google Sheets API`](/docs/demos/gsheet) -- [`ExtendScript for Adobe Apps`](/docs/demos/extendscript) -- [`NetSuite SuiteScript`](/docs/demos/cloud/netsuite) -- [`Salesforce Lightning Web Components`](/docs/demos/cloud/salesforce) -- [`Excel JavaScript API`](/docs/demos/excelapi) - [`Headless Automation`](/docs/demos/headless) - [`Other JavaScript Engines`](/docs/demos/engines) -- [`Azure Functions and Storage`](/docs/demos/cloud/azure) -- [`Amazon Web Services`](/docs/demos/cloud/aws) - [`Databases and Structured Data Stores`](/docs/demos/database) - [`NoSQL and Unstructured Data Stores`](/docs/demos/nosql) - [`Legacy Internet Explorer`](/docs/demos/frontend/legacy#internet-explorer) diff --git a/docz/docusaurus.config.js b/docz/docusaurus.config.js index add5303..b0a89be 100644 --- a/docz/docusaurus.config.js +++ b/docz/docusaurus.config.js @@ -171,6 +171,7 @@ const config = { { from: '/docs/demos/excel', to: '/docs/demos/' }, { from: '/docs/getting-started/demos/', to: '/docs/demos/' }, { from: '/docs/getting-started/demos/excel', to: '/docs/demos/' }, + { from: '/docs/demos/content', to: '/docs/demos/static/' }, /* frontend */ { from: '/docs/demos/angular', to: '/docs/demos/frontend/angular/' }, { from: '/docs/demos/react', to: '/docs/demos/frontend/react/' }, diff --git a/docz/static/static/live.gif b/docz/static/static/live.gif new file mode 100644 index 0000000..68b96ae Binary files /dev/null and b/docz/static/static/live.gif differ