13 KiB
title | pagination_prev | pagination_next |
---|---|---|
NuxtJS | demos/extensions/index | demos/gsheet |
@nuxt/content
is a file-based CMS for Nuxt, enabling static-site generation
and on-demand server rendering powered by spreadsheets.
:::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.
:::
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:
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
.
:::
- Create a stock app:
npx create-nuxt-app@4.0.0 SheetJSNuxt
When prompted, enter the following options:
Project name
: press Enter (use defaultSheetJSNuxt
)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 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.
- 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
:::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
.
:::
- Create a stock app and install dependencies:
npx nuxi init -t content sheetjs-nc2
cd sheetjs-nc2
npx yarn install
npx yarn add --dev @types/node
- 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.
- 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({
modules: [
SheetJSModule,
'@nuxt/content'
],
content: {}
})
Restart the dev server by exiting the process (Control+C) and running:
npx nuxi clean
npx nuxi typecheck
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
},
- 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 nuxi clean
npx 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 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.