Skip to main content

NuxtJS

@nuxt/content is a file-based CMS for Nuxt, enabling static-site generation and on-demand server rendering powered by spreadsheets.

danger

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.

Nuxt Content v1

note

This demo was tested on 2022 November 18 against Nuxt Content v1.15.1.

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:

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":

  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:

  <!-- 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

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:

npx [email protected] 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:

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 https://sheetjs.com/pres.xlsx and move to the content folder.

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:
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:
  // Content module configuration: https://go.nuxtjs.dev/config-content
content: {},

Replace the property with the following definition:

  // 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:

<!-- 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.

Nuxt Demo end of step 5

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`

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

7) Stop the server (press CTRL+C in the terminal window) and run

npm run generate

This will create a static site in the dist folder, which can be served with:

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

note

This demo was tested on 2023 January 19 against Nuxt Content v2.3.0.

Overview

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:

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',
extensions: ['.xlsx'],
parse (_id: string, rawContent: string) {
const wb = read(readFileSync(resolve("./content/" + _id.slice(8))));
const body = wb.SheetNames.map(name => ({ name, data: utils.sheet_to_json(wb.Sheets[name])}));
return { _id, body };
}
});

Pages can pull data using useAsyncData:

<script setup>
const key = "pres"; // matches pres.xlsx
const {data} = await useAsyncData('x', ()=>queryContent(`/${key}`).findOne());
// data.body is the output from the transformer and can be used in the template
</script>

Pages should use ContentRenderer to reference the data:

<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 worsheet 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

note

This demo was tested on 2023 January 19 against Nuxt Content v2.3.0.

The generated project used Nuxt v3.0.0.

1) Create a stock app and install dependencies:

npx nuxi init -t content sheetjs-nc2
cd sheetjs-nc2
npx yarn install

2) Install the SheetJS library and start the server:

npx yarn add https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
npx yarn dev

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.

curl -L -o content/pres.xlsx https://sheetjs.com/pres.xlsx

4) Create the transformer.

Two files must be written:

  • sheetformer.ts (the raw transformer module):
sheetformer.ts
// @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',
extensions: ['.xlsx'],
parse (_id: string, rawContent: string) {
const wb = read(readFileSync(resolve("./content/" + _id.slice(8))));
const body = wb.SheetNames.map(name => ({ name, data: utils.sheet_to_json(wb.Sheets[name])}));
return { _id, body };
}
});
  • sheetmodule.ts (the nuxt configuration module):
sheetmodule.ts
import { resolve } from 'path'
import { defineNuxtModule } from '@nuxt/kit'

export default defineNuxtModule({
setup (_options, nuxt) {
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'))
// @ts-ignore
nuxt.hook('content:context', (contentContext) => {
contentContext.transformers.push(resolve('./sheetformer.ts'))
})
}
})

After creating the source files, the module must be added to nuxt.config.ts:

nuxt.config.ts
import SheetJSModule from './sheetmodule'

export default defineNuxtConfig({
modules: [
SheetJSModule,
'@nuxt/content'
],
content: {}
})

Restart the dev server by exiting the process (Control+C) and running:

npx nuxi clean
npx yarn run dev

Loading http://localhost:3000/pres should show some JSON data:

{
// ...
"data": {
"_path": "/pres",
// ...
"_id": "content:pres.xlsx",
"body": [
{
"name": "Sheet1", // <-- sheet name
"data": [ // <-- array of data objects
{
"Name": "Bill Clinton",
"Index": 42
},

5) Create a page. Save the following content to pages/pres.vue:

pages/pres.vue
<script setup>
const {data} = await useAsyncData('s5s', () => queryContent('/pres').findOne());
</script>
<template><ContentRenderer :value="data">
<div v-for="item in data.body" 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>
</ContentRenderer></template>

Restart the dev server by exiting the process (Control+C) and running:

npx nuxi clean
npx 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

npx yarn run generate

This will create a static site in .output/public, which can be served with:

npx 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.