15 KiB
title | pagination_prev | pagination_next |
---|---|---|
NuxtJS | demos/net/index | demos/mobile/index |
import current from '/version.js'; import CodeBlock from '@theme/CodeBlock';
@nuxt/content
is a file-based CMS for Nuxt, enabling static-site generation
and on-demand server rendering powered by spreadsheets.
The NodeJS module can be imported from Content v1 "parsers" and Content v2 "transformers".
:::note
The following deployments were tested:
Nuxt Content | Nuxt | Date |
---|---|---|
1.15.1 |
2.16.3 |
2023-06-01 |
2.3.0 |
3.0.0 |
2023-01-19 |
2.6.0 |
3.5.2 |
2023-06-01 |
:::
:::caution Telemetry
Nuxt embeds telemetry. According to the developers, it is disabled by default. To explicitly disable telemetry, the official documentation recommends:
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:
// ...
// highlight-start
export default defineNuxtConfig({
// @ts-ignore
telemetry: false,
// highlight-end
// ...
})
A global setting can be added to .nuxtrc
in the user home directory:
telemetry.enabled=false
:::
Nuxt Content v1
Nuxt Content v1 is designed to work with Nuxt v2.
The following diagram depicts the workbook waltz:
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
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:
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
:::caution
When the demo was last tested, parts of the Nuxt dependency tree did not support NodeJS version 20. The creation step will show warnings like
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.
:::
- Create a stock app:
npx create-nuxt-app@4.0.0 sheetjs-nuxt
When prompted, enter the following options:
Project name
: press Enter (use defaultsheetjs-nuxt
)Programming language
: press Down Arrow (TypeScript
selected) then EnterPackage manager
: selectNpm
and press EnterUI framework
: selectNone
and press EnterNuxt.js modules
: scroll toContent
, select with Space, then press EnterLinting tools
: press Enter (do not select any Linting tools)Testing framework
: selectNone
and press EnterRendering mode
: selectUniversal (SSR / SSG)
and press EnterDeployment target
: selectStatic (Static/Jamstack hosting)
and press EnterDevelopment tools
: press Enter (do not select any Development tools)What is your GitHub username?
: press EnterVersion control system
: selectNone
The project will be configured and modules will be installed.
- Install the SheetJS library and start the server:
{\ cd sheetjs-nuxt npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.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.
- Download https://sheetjs.com/pres.xlsx and move to the
content
folder.
curl -L -o content/pres.xlsx https://sheetjs.com/pres.xlsx
- 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)
- 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.
- To verify that hot loading works, open
pres.xlsx
from thecontent
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:
- 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
Nuxt Content v2 is designed to work with Nuxt v3.
The following diagram depicts the workbook waltz:
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
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:
// @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) {
// 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, 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
:::caution
When the demo was last tested, 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.
:::
- Create a stock app and install dependencies:
npx -y nuxi init -t content sheetjs-nc2
cd sheetjs-nc2
npx -y yarn install
npx -y yarn add --dev @types/node
- Install the SheetJS library and start the server:
{\ npx -y yarn add https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz npx -y 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.
- Download https://sheetjs.com/pres.xlsx and move to the
content
folder.
curl -L -o content/pres.xlsx https://sheetjs.com/pres.xlsx
- Create the transformer.
Two files must be written:
sheetformer.ts
(the raw transformer module):
// @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):
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
:
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:
npx -y nuxi clean
npx -y nuxi typecheck
npx -y 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
},
- Create a page. Save the following content to
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 -y nuxi clean
npx -y yarn run dev
The browser should now display an HTML table.
- To verify that hot loading works, open
pres.xlsx
from thecontent
folder in Excel. Add a new row to the bottom and save the file.
The page should automatically refresh with the new content.
- Stop the server (press
CTRL+C
in the terminal window) and run
npx -y yarn run generate
This will create a static site in .output/public
, which can be served with:
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.