--- title: Sheets in VueJS Sites with NuxtJS sidebar_label: NuxtJS description: Make static websites from spreadsheets using NuxtJS. Seamlessly integrate data into the data layer using SheetJS. Create content without leaving the comfort of Excel. pagination_prev: demos/net/index pagination_next: demos/mobile/index --- import current from '/version.js'; import CodeBlock from '@theme/CodeBlock'; [Nuxt Content](https://content.nuxtjs.org/) is a file-based CMS for NuxtJS, enabling static-site generation and on-demand server rendering from data files. [SheetJS](https://sheetjs.com) is a JavaScript library for reading and writing data from spreadsheets. This demo uses NuxtJS and SheetJS to pull data from a spreadsheet and display the content in an HTML table. :::info pass There were breaking changes between VueJS 2.x and VueJS 3.x. Since many projects still use VueJS 2.x, this demo includes examples for both versions of VueJS. ::: The ["Nuxt Content v1"](#nuxt-content-v1) section explores "parsers" for NuxtJS Content v1 (paired with VueJS 2.x and NuxtJS 2.x) The ["Nuxt Content v2"](#nuxt-content-v2) section explores "transformers" for NuxtJS Content v2 (paired with VueJS 3.x and NuxtJS 3.x) :::note The following deployments were tested: | Nuxt Content | Nuxt | Date | |:-------------|:---------|:-----------| | `1.15.1` | `2.17.1` | 2023-08-16 | | `2.7.2` | `3.6.5` | 2023-08-16 | ::: :::caution Telemetry Nuxt embeds telemetry. According to the developers, it is disabled by default. To explicitly disable telemetry, the official documentation recommends: ```bash npx nuxt telemetry disable ``` At the time the demo was last tested, this command did not work. Instead, a option should be added in `nuxt.config.ts` or `nuxt.config.js` for Nuxt 3 sites: ```js title="nuxt.config.js" // ... // highlight-start export default defineNuxtConfig({ // @ts-ignore telemetry: false, // highlight-end // ... }) ``` A global setting can be added to `.nuxtrc` in the user home directory: ```ini title=".nuxtrc" telemetry.enabled=false ``` ::: ## Nuxt Content v1 Nuxt Content v1 is designed to work with NuxtJS v2 and VueJS v2. The following diagram depicts the workbook waltz: ```mermaid flowchart LR file[(workbook\nfile)] subgraph SheetJS operations buffer(NodeJS\nBuffer) aoo(array of\nobjects) end html{{HTML\nTABLE}} file --> |nuxt.config.js\ncustom parser| buffer buffer --> |nuxt.config.js\ncustom parser| aoo aoo --> |index.vue\ntemplate| html ``` ### Installation The [SheetJS NodeJS Module](/docs/getting-started/installation/nodejs) can be imported from `nuxt.config.js` for build-time processing. ### Custom Parser :::info pass Custom parsers receive the file content interpreted as UTF-8 strings. For binary formats including XLSX and XLS, the data is corrupt and cannot be used. The workaround involves safely re-reading the spreadsheets. ::: The second argument passed to the parser is an object. The `path` property of the object is the path to the file. The SheetJS `readFile`[^1] method can locate and parse the spreadsheet files. The `sheet_to_json`[^2] utility function can generate arrays of row objects for use in NuxtJS pages: ```js title="Custom Parser (in 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); /* iterate through each worksheet name and generate row objects */ const o = wb.SheetNames.map(name => ({ name: name, data: utils.sheet_to_json(wb.Sheets[name]) })); /* The final result must be stored in the `data` key of an object */ return { data: o }; }; ``` ### Configuration Using `content.extendParser`[^3], Nuxt Content will use custom parsers. The property is expected to be an object whose keys are file extensions (with the `.` before the extension) and whose values are custom parser functions. The relevant part of the config is shown below. In this snippet, the custom parser `parseSheet` will be associated with XLSX, XLS and NUMBERS files: ```js title="nuxt.config.js" 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 title="Script section of .vue VueJS page" 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`[^4]: ```xml title="Template section of .vue VueJS page" <!-- loop over the worksheets --> <div v-for="item in data.data" v-bind:key="item.name"> <table> <!-- loop over the rows of each worksheet --> <tr v-for="row in item.data" v-bind:key="row.Index"> <!-- here `row` is a row object generated from sheet_to_json --> <td>{{ row.Name }}</td> <td>{{ row.Index }}</td> </tr> </table> </div> ``` ### Nuxt Content Demo :::caution pass For some older versions, parts of the Nuxt dependency tree did not support NodeJS version 20. `EBADENGINE` warnings were displayed during app creation: ``` npm WARN EBADENGINE Unsupported engine { npm WARN EBADENGINE package: '@nuxt/types@2.16.3', npm WARN EBADENGINE required: { node: '^14.18.0 || ^16.10.0 || ^17.0.0 || ... npm WARN EBADENGINE current: { node: 'v20.2.0', npm: '9.6.6' } npm WARN EBADENGINE } ``` The recommended solution is to switch to Node 18. ::: 1) Create a stock app: ```bash npx create-nuxt-app@4.0.0 sheetjs-nuxt ``` When prompted, enter the following options: - `Project name`: press Enter (use default `sheetjs-nuxt`) - `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: <CodeBlock language="bash">{`\ cd sheetjs-nuxt npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz npm run dev`} </CodeBlock> 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 <https://sheetjs.com/pres.xlsx> 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 title="pages/index.vue" <!-- sheetjs (C) 2013-present SheetJS -- https://sheetjs.com --> <template><div> <div v-for="item in data.data" v-bind:key="item.name"> <h2>{{ item.name }}</h2> <table><thead><tr><th>Name</th><th>Index</th></tr></thead><tbody> <tr v-for="row in item.data" v-bind:key="row.Index"> <td>{{ row.Name }}</td> <td>{{ row.Index }}</td> </tr> </tbody></table> </div> </div></template> <script> export default { async asyncData ({$content}) { return { data: await $content('pres').fetch() }; } }; </script> ``` The browser should refresh to show the contents of the spreadsheet. If it does not, click Refresh manually or open a new browser window.  6) To verify that live reload works, open `pres.xlsx` from the `content` folder in Excel. Add a new row to the bottom and save the file:  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:  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 Content v2 Nuxt Content v2 is designed to work with NuxtJS v3 and VueJS v3. The following diagram depicts the workbook waltz: ```mermaid flowchart LR file[(workbook\nfile)] subgraph SheetJS operations buffer(NodeJS\nBuffer) aoo(array of\nobjects) end html{{HTML\nTABLE}} file --> |custom module\ntransformer| buffer buffer --> |custom module\ntransformer| aoo aoo --> |index.vue\nContentRenderer| html ``` ### Installation The [SheetJS NodeJS Module](/docs/getting-started/installation/nodejs) can be safely imported from `nuxt.config.js` or transformer or module scripts. As long as the SheetJS modules are not imported in the various `.vue` pages, the library will not be added to the final page bundle! ### Custom Transformer Nuxt Content `v2` supports custom transformers for controlling data. Although the library hard-codes UTF-8 interpretations, the `_id` field currently uses the pattern `content:` followed by the filename (if files are placed in the `content` folder directly). This enables a transformer to re-read the file. For example, if the file `pres.xlsx` is stored in the `content` folder, NuxtJS Content will use ID `"content:pres.xlsx"`. `"./content/" + _id.slice(8)` will be the original path `"./content/pres.xlsx"`. The NodeJS `resolve`[^5] method will return a file path. `readFileSync`[^6] will read the file and return a NodeJS `Buffer`. That `Buffer` object can be parsed with the SheetJS `read`[^7] method. The `sheet_to_json`[^8] utility function can generate arrays of row objects for use in NuxtJS pages. ```ts title="sheetformer.ts (Transformer)" // @ts-ignore import { defineTransformer } from "@nuxt/content/transformers/utils"; import { read, utils } from "xlsx"; import { readFileSync } from "node:fs"; import { resolve } from 'node:path'; export default defineTransformer({ name: 'sheetformer', /* list of file extensions */ extensions: ['.xlsx'], parse (_id: string, rawContent: string) { // highlight-start /* read the underlying file */ const buf = readFileSync(resolve("./content/" + _id.slice(8))); /* parse */ const wb = read(buf); // highlight-end /* generate JS objects for each worksheet */ const body = wb.SheetNames.map(name => ({ name: name, data: utils.sheet_to_json(wb.Sheets[name]) })); /* The final result must have the `_id` key and must store data in `body` */ return { _id, body }; } }); ``` The data object returned by the transformer must have the original `_id` key. The data is stored in the `body` property of the final object. ### Custom Modules NuxtJS modules are the main mechanism for adding transformers to the pipeline. <details><summary><b>Module Details</b> (click to show)</summary> Due to the structure of the NuxtJS system, modules must be defined in separate script files. The module script is expected to export a module configured with `defineNuxtModule`. The setup method is expected to do the following: - Register itself with the "Nitro" subsystem - Add the transformer to Nuxt Content in the `content:context` hook ```js title="sheetmodule.ts (Module)" import { resolve } from 'path' import { defineNuxtModule } from '@nuxt/kit' export default defineNuxtModule({ /* module setup method */ setup (_options, nuxt) { /* register with the nitro subsystem */ nuxt.options.nitro.externals = nuxt.options.nitro.externals || {}; nuxt.options.nitro.externals.inline = nuxt.options.nitro.externals.inline || []; nuxt.options.nitro.externals.inline.push(resolve('./sheetmodule')); /* add transformer in the content:context hook */ // @ts-ignore nuxt.hook('content:context', (contentContext) => { contentContext.transformers.push(resolve('./sheetformer.ts')); }); } }); ``` The module must be loaded in `nuxt.config.ts` and added to the `modules` array: ```ts title="nuxt.config.ts" // highlight-next-line import SheetJSModule from './sheetmodule' export default defineNuxtConfig({ // @ts-ignore telemetry: false, modules: [ // highlight-next-line SheetJSModule, /* it is recommended to load the custom modules before @nuxt/content */ '@nuxt/content' ], content: {} }); ``` </details> ### Rendering Data Pages can pull data using `useAsyncData` in the page setup: ```html title="Script section of .vue VueJS page" <script setup> const key = "pres"; // matches pres.xlsx const { data } = await useAsyncData('x', ()=>queryContent(`/${key}`).findOne()); </script> ``` Pages should use `ContentRenderer` to reference the data. The `data` variable from the script setup will be shaped like the return value from the transformer. `data.body` is an array of objects that holds the worksheet name and data. ```html title="Template section of .vue VueJS page" <template> <ContentRenderer :value="data"> <!-- data.body is the array defined in the transformer --> <div v-for="item in data.body" v-bind:key="item.name"> <!-- each item has a "name" string for worksheet name --> <h2>{{ item.name }}</h2> <!-- each item has a "body" array of data rows --> <table><thead><tr><th>Name</th><th>Index</th></tr></thead><tbody> <tr v-for="row in item.data" v-bind:key="row.Index"> <!-- Assuming the sheet uses the columns "Name" and "Index" --> <td>{{ row.Name }}</td> <td>{{ row.Index }}</td> </tr> </tbody></table> </div> </ContentRenderer> </template> ``` ### Nuxt Content 2 Demo :::caution pass For some older versions, parts of the Nuxt dependency tree did not support NodeJS version 20. If the `yarn install` step fails with a message like ``` error @nuxt/kit@3.4.1: The engine "node" is incompatible with this module. ``` The recommended solution is to switch to Node 18. ::: 1) Create a stock app and install dependencies: ```bash npx -y nuxi init -t content sheetjs-nc2 cd sheetjs-nc2 npx -y yarn install npx -y yarn add --dev @types/node ``` 2) Install the SheetJS library and start the server: <CodeBlock language="bash">{`\ npx -y yarn add https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz npx -y yarn dev`} </CodeBlock> When the build finishes, the terminal will display a URL like: ``` > Local: http://localhost:3000/ ``` The server is listening on that URL. Open the link in a web browser. 3) Download <https://sheetjs.com/pres.xlsx> and move to the `content` folder. ```bash curl -L -o content/pres.xlsx https://sheetjs.com/pres.xlsx ``` 4) Create the transformer. Two files must be saved at the root of the project: - [`sheetformer.ts`](https://docs.sheetjs.com/nuxt/3/sheetformer.ts) (raw transformer module) - [`sheetmodule.ts`](https://docs.sheetjs.com/nuxt/3/sheetmodule.ts) (Nuxt configuration module) ```bash curl -O https://docs.sheetjs.com/nuxt/3/sheetformer.ts curl -O https://docs.sheetjs.com/nuxt/3/sheetmodule.ts ``` After creating the source files, the module must be added to `nuxt.config.ts`: ```ts title="nuxt.config.ts" import SheetJSModule from './sheetmodule' export default defineNuxtConfig({ // @ts-ignore telemetry: false, modules: [ SheetJSModule, '@nuxt/content' ], content: {} }); ``` Restart the dev server by exiting the process (Control+C) and running: ```bash npx -y nuxi clean npx -y nuxi typecheck npx -y yarn run dev ``` Loading `http://localhost:3000/pres` should show some JSON data: ```json { // ... "data": { "_path": "/pres", // ... "_id": "content:pres.xlsx", "body": [ { "name": "Sheet1", // <-- sheet name "data": [ // <-- array of data objects { "Name": "Bill Clinton", "Index": 42 }, ``` 5) Download [`pres.vue`](pathname:///nuxt/3/pres.vue) and save to `pages`: ```bash curl -o pages/pres.vue https://docs.sheetjs.com/nuxt/3/pres.vue ``` Restart the dev server by exiting the process (Control+C) and running: ```bash npx -y nuxi clean npx -y yarn run dev ``` The browser should now display an HTML table. 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. The page should automatically refresh with the new content. 7) Stop the server (press `CTRL+C` in the terminal window) and run ```bash npx -y yarn run generate ``` This will create a static site in `.output/public`, which can be served with: ```bash npx -y http-server .output/public ``` Accessing `http://localhost:8080/pres` will show the page contents. Verifying the static nature is trivial: make another change in Excel and save. The page will not change. [^1]: See [`readFile` in "Reading Files"](/docs/api/parse-options) [^2]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output) [^3]: See [`extendParser`](https://content.nuxtjs.org/v1/getting-started/configuration#extendparser) in the NuxtJS documentation. [^4]: See ["Array of Objects" in the VueJS demo](/docs/demos/frontend/vue#array-of-objects) [^5]: See [`resolve`](https://nodejs.org/api/path.html#pathresolvepaths) in the NodeJS `node:path` documentation [^6]: See [`readFileSync`](https://nodejs.org/api/fs.html#fsreadfilesyncpath-options) in the NodeJS `node:fs` documentation [^7]: See [`read` in "Reading Files"](/docs/api/parse-options) [^8]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output)