---
title: Content and Site Generation
---
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
[Lume](https://lume.land) is a static site generator for the Deno platform.
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.12.0` on 2022 October 4.
:::
Complete Example (click to show)
1) Create a stock site:
```bash
mkdir -p sheetjs-lume
cd sheetjs-lume
deno run -A 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 -LO https://sheetjs.com/pres.numbers
mv pres.numbers _data
```
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
Name | Index |
{% for row in pres %}{% if (loop.index >= 1) %}
{{ row.Name }} |
{{ row.Index }} |
{% endif %}{% endfor %}
```
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.
:::
## ViteJS
:::note
Library integration in ViteJS is covered in ["Bundlers" demo](./bundler#vite)
:::
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'], // mark that 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 path = id.replace(/\?sheetjs/, "");
var wb = read(readFileSync(path));
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 = ``;
```
## NextJS
:::note
This was tested against `next v12.2.5` on 2022 August 16.
:::
:::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
`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 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
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` 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
```
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 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` subfolder, which can be hosted with
```bash
npx http-server out
```
The command will start a local HTTP server on port 8080.
## NuxtJS
`@nuxt/content` is a file-based CMS for Nuxt, enabling static-site generation
and on-demand server rendering powered by spreadsheets.
#### 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
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
Complete Example (click to show)
:::note
This was tested against `create-nuxt-app v4.0.0` on 2022 August 13.
:::
1) Create a stock app:
```bash
npx create-nuxt-app 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.
4) Modify `nuxt.config.js` as described [earlier](#nuxtconfigjs-configuration)
5) Replace `pages/index.vue` with the following:
```html
{{ item.name }}
Name | Index |
{{ row.Name }} |
{{ row.Index }} |
```
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.