nuxt-content-assets 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,276 @@
1
+ # Nuxt Content Assets
2
+
3
+ > Enable locally-located assets in Nuxt Content
4
+
5
+ [![npm version][npm-version-src]][npm-version-href]
6
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
7
+ [![License][license-src]][license-href]
8
+ [![Nuxt][nuxt-src]][nuxt-href]
9
+
10
+ ## Overview
11
+
12
+ Nuxt Content Assets enables locally-located assets in your [Nuxt Content](https://content.nuxtjs.org/) folder:
13
+
14
+ ```
15
+ +- content
16
+ +- posts
17
+ +- 2023-01-01
18
+ +- index.md
19
+ +- media
20
+ +- featured.png
21
+ +- mountains.jpg
22
+ +- seaside.mp4
23
+ ```
24
+
25
+ In your documents, simply reference assets using relative paths:
26
+
27
+ ```markdown
28
+ ---
29
+ title: Summer Holiday
30
+ featured: media/featured.png
31
+ ---
32
+
33
+ I loved being in the mountains.
34
+
35
+ ![mountains](media/mountains.png)
36
+
37
+ Almost as much as being in the sea!
38
+
39
+ <video src="media/seaside.mp4"></video>
40
+ ```
41
+
42
+ The module:
43
+
44
+ - supports configurable image, media and file types
45
+ - supports appropriate HTML tags
46
+ - converts markdown and frontmatter content
47
+
48
+ ## Demo
49
+
50
+ To run the demo online, go to:
51
+
52
+ - https://stackblitz.com/github/davestewart/nuxt-content-assets?file=demo%2Fapp.vue
53
+
54
+ You can browse the demo files in:
55
+
56
+ - .https://github.com/davestewart/nuxt-content-assets/tree/main/demo
57
+
58
+ To run the demo locally, clone the application and from the root, run:
59
+
60
+ ```
61
+ npm run demo
62
+ ```
63
+
64
+ ## Setup
65
+
66
+ Install the dependency:
67
+
68
+ ```bash
69
+ npm install nuxt-content-assets
70
+ ```
71
+
72
+ Configure `nuxt.config.ts`:
73
+
74
+ ```js
75
+ export default defineNuxtConfig({
76
+ modules: [
77
+ 'nuxt-content-relative-assets', // make sure to add before content!
78
+ '@nuxt/content',
79
+ ]
80
+ })
81
+ ```
82
+
83
+ Run the dev server or build and assets should now be served alongside markdown content.
84
+
85
+ ## Usage
86
+
87
+ ### Overview
88
+
89
+ Once the build or dev server is running, paths should be rewritten and assets served automatically.
90
+
91
+ Here's how it works:
92
+
93
+ - the module scans content folders for assets
94
+ - these are copied to a temporary build folder
95
+ - matching relative paths in markdown are updated
96
+ - Nitro serves the assets from the new location
97
+
98
+ Right now, if you change any assets, you will need to re-run the dev server / build.
99
+
100
+ ### Supported formats
101
+
102
+ The following file formats are supported out of the box:
103
+
104
+ | Type | Extensions |
105
+ |--------|-------------------------------------------------------------------------|
106
+ | Images | `png`, `jpg`, `jpeg`, `gif`, `svg`, `webp` |
107
+ | Media | `mp3`, `m4a`, `wav`, `mp4`, `mov`, `webm`, `ogg`, `avi`, `flv`, `avchd` |
108
+ | Files | `pdf`, `doc`, `docx`, `xls`, `xlsx`, `ppt`, `pptx`, `odp`, `key` |
109
+
110
+ See the [configuration](#output) section for more options.
111
+
112
+ ### Images
113
+
114
+ The module [optionally](#image-attributes) writes `width` and `height` attributes to the generated HTML:
115
+
116
+ ```html
117
+ <img src="..." width="640" height="480">
118
+ ```
119
+
120
+ This locks the aspect ratio of the image preventing content jumps.
121
+
122
+ If you use custom [ProseImg](https://content.nuxtjs.org/api/components/prose) components, you can even grab these values using the Vue `$attrs` property:
123
+
124
+ ```vue
125
+ <template>
126
+ <div class="image">
127
+ <img :src="$attrs.src" :style="`aspect-ratio:${$attrs.width}/${$attrs.height}`" />
128
+ </div>
129
+ </template>
130
+
131
+ <script>
132
+ export default {
133
+ inheritAttrs: false
134
+ }
135
+ </script>
136
+ ```
137
+
138
+ See the [Demo](demo/components/_content/ProseImg.vue) for an example.
139
+
140
+ ## Configuration
141
+
142
+ The module **doesn't require** any configuration, but you can tweak the following settings:
143
+
144
+ ```ts
145
+ // nuxt.config.ts
146
+ export default defineNuxtConfig({
147
+ 'content-assets': {
148
+ // where to generate and serve the assets from
149
+ output: 'assets/content/[path]/[file]',
150
+
151
+ // add additional extensions
152
+ additionalExtensions: 'html',
153
+
154
+ // completely replace supported extensions
155
+ extensions: 'png jpg',
156
+
157
+ // add image width and height
158
+ imageAttrs: true,
159
+
160
+ // print debug messages to the console
161
+ debug: true,
162
+ }
163
+ })
164
+ ```
165
+
166
+ ### Output
167
+
168
+ The output path can be customised using a template string:
169
+
170
+ ```
171
+ assets/img/content/[path]/[name][extname]
172
+ ```
173
+
174
+ The first part of the path should be public root-relative folder:
175
+
176
+ ```
177
+ assets/img/content
178
+ ```
179
+
180
+ The optional second part of the path indicates specific placement of each image:
181
+
182
+ | Token | Description | Example |
183
+ |-------------|--------------------------------------------|--------------------|
184
+ | `[folder]` | The relative folder of the file | `posts/2023-01-01` |
185
+ | `[file]` | The full filename of the file | `featured.jpg` |
186
+ | `[name]` | The name of the file without the extension | `featured` |
187
+ | `[hash]` | A hash of the absolute source path | `9M00N4l9A0` |
188
+ | `[extname]` | The full extension with the dot | `.jpg` |
189
+ | `[ext]` | The extension without the dot | `jpg` |
190
+
191
+ For example:
192
+
193
+ | Template | Output |
194
+ |--------------------------------------|----------------------------------------------------|
195
+ | `assets/img/content/[folder]/[file]` | `assets/img/content/posts/2023-01-01/featured.jpg` |
196
+ | `assets/img/[name]-[hash].[ext]` | `assets/img/featured-9M00N4l9A0.jpg` |
197
+ | `content/[hash].[ext]` | `content/9M00N4l9A0.jpg` |
198
+
199
+ Note that the module defaults to all files in a single folder:
200
+
201
+ ```
202
+ /assets/content/[name]-[hash].[ext]
203
+ ```
204
+
205
+ ### Extensions
206
+
207
+ You can add or replace supported extensions if you need to:
208
+
209
+ To add extensions, use `additionalExtensions`:
210
+
211
+ ```ts
212
+ {
213
+ additionalExtensions: 'html' // add support for html
214
+ }
215
+ ```
216
+
217
+ To completely replace supported extensions, use `extensions`:
218
+
219
+ ```ts
220
+ {
221
+ extensions: 'png jpg' // serve png and jpg files only
222
+ }
223
+ ```
224
+
225
+ ### Image attributes
226
+
227
+ The module automatically adds `width` and `height` attributes to images.
228
+
229
+ Opt out of this by passing `false`:
230
+
231
+ ```ts
232
+ {
233
+ imageAttrs: false
234
+ }
235
+ ```
236
+
237
+ ## Development
238
+
239
+ Should you wish to develop the project, the scripts are:
240
+
241
+ ```bash
242
+ # install dependencies
243
+ npm install
244
+
245
+ # generate type stubs
246
+ npm run dev:prepare
247
+
248
+ # develop with the demo
249
+ npm run dev
250
+
251
+ # build the demo
252
+ npm run dev:build
253
+
254
+ # run eslint
255
+ npm run lint
256
+
257
+ # run vitest
258
+ npm run test
259
+ npm run test:watch
260
+
261
+ # release new version
262
+ npm run release
263
+ ```
264
+
265
+ <!-- Badges -->
266
+ [npm-version-src]: https://img.shields.io/npm/v/nuxt-content-assets/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
267
+ [npm-version-href]: https://npmjs.com/package/nuxt-content-assets
268
+
269
+ [npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-content-assets.svg?style=flat&colorA=18181B&colorB=28CF8D
270
+ [npm-downloads-href]: https://npmjs.com/package/nuxt-content-assets
271
+
272
+ [license-src]: https://img.shields.io/npm/l/nuxt-content-assets.svg?style=flat&colorA=18181B&colorB=28CF8D
273
+ [license-href]: https://npmjs.com/package/nuxt-content-assets
274
+
275
+ [nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js
276
+ [nuxt-href]: https://nuxt.com
@@ -0,0 +1,5 @@
1
+ module.exports = function(...args) {
2
+ return import('./module.mjs').then(m => m.default.call(this, ...args))
3
+ }
4
+ const _meta = module.exports.meta = require('./module.json')
5
+ module.exports.getMeta = () => Promise.resolve(_meta)
@@ -0,0 +1,12 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ interface ModuleOptions {
4
+ output?: string;
5
+ additionalExtensions?: string;
6
+ extensions?: string;
7
+ imageAttrs?: boolean;
8
+ debug?: boolean;
9
+ }
10
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions>;
11
+
12
+ export { ModuleOptions, _default as default };
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "content-assets",
3
+ "configKey": "content-assets",
4
+ "version": "0.5.1"
5
+ }
@@ -0,0 +1,180 @@
1
+ import { createResolver, defineNuxtModule, addTemplate } from '@nuxt/kit';
2
+ import getImageSize from 'image-size';
3
+ import glob from 'glob';
4
+ import * as Fs from 'fs';
5
+ import * as Path from 'path';
6
+ import Path__default from 'path';
7
+ import { hash } from 'ohash';
8
+
9
+ function log(...data) {
10
+ console.info(`[${name}]`, ...data);
11
+ }
12
+
13
+ function matchWords(value) {
14
+ return value.match(/\w+/g) || [];
15
+ }
16
+ function isImage(path) {
17
+ const ext = Path__default.extname(path).substring(1);
18
+ return imageExtensions.includes(ext);
19
+ }
20
+ function interpolatePattern(pattern, src, dir, warn = false) {
21
+ return Path__default.join(pattern.replace(/\[\w+]/g, (match) => {
22
+ const name = match.substring(1, match.length - 1);
23
+ const fn = replacers[name];
24
+ if (fn) {
25
+ return fn(src, dir);
26
+ }
27
+ if (warn) {
28
+ log(`Unknown output token ${match}`, true);
29
+ }
30
+ return match;
31
+ }));
32
+ }
33
+ const replacers = {
34
+ folder: (src, dir) => Path__default.dirname(src.replace(dir, "")),
35
+ file: (src) => Path__default.basename(src),
36
+ name: (src) => Path__default.basename(src, Path__default.extname(src)),
37
+ extname: (src) => Path__default.extname(src),
38
+ ext: (src) => Path__default.extname(src).substring(1),
39
+ hash: (src) => hash({ src })
40
+ };
41
+
42
+ const name = "content-assets";
43
+ const defaults = {
44
+ assetsDir: "assets/content",
45
+ assetsPattern: "[name]-[hash].[ext]"
46
+ };
47
+ const imageExtensions = matchWords("png jpg jpeg gif svg webp ico");
48
+ const mediaExtensions = matchWords("mp3 m4a wav mp4 mov webm ogg avi flv avchd");
49
+ const fileExtensions = matchWords("pdf doc docx xls xlsx ppt pptx odp key");
50
+ const extensions = [
51
+ ...imageExtensions,
52
+ ...mediaExtensions,
53
+ ...fileExtensions
54
+ ];
55
+
56
+ function getSources(sources) {
57
+ return Object.keys(sources).reduce((output, key) => {
58
+ const source = sources[key];
59
+ if (source) {
60
+ const { driver, base } = source;
61
+ if (driver === "fs") {
62
+ output[key] = base;
63
+ }
64
+ }
65
+ return output;
66
+ }, {});
67
+ }
68
+
69
+ const resolve = createResolver(import.meta.url).resolve;
70
+ const module = defineNuxtModule({
71
+ meta: {
72
+ name
73
+ },
74
+ defaults: {
75
+ output: `${defaults.assetsDir}/${defaults.assetsPattern}`,
76
+ extensions: "",
77
+ additionalExtensions: "",
78
+ imageAttrs: true,
79
+ debug: false
80
+ },
81
+ setup(options, nuxt) {
82
+ var _a;
83
+ const pluginPath = resolve("./runtime/server") + "/plugins/plugin";
84
+ const buildPath = nuxt.options.buildDir;
85
+ const cachePath = Path.resolve(buildPath, "content-assets");
86
+ if (options.debug) {
87
+ log("Removing cache folders...");
88
+ }
89
+ Fs.rmSync(Path.join(buildPath, "content-cache"), { recursive: true, force: true });
90
+ Fs.rmSync(cachePath, { recursive: true, force: true });
91
+ const sources = nuxt.options._layers.map((layer) => layer.config?.content?.sources).reduce((output2, sources2) => {
92
+ if (sources2) {
93
+ Object.assign(output2, getSources(sources2));
94
+ }
95
+ return output2;
96
+ }, {});
97
+ if (Object.keys(sources).length === 0 || !sources.content) {
98
+ const content = nuxt.options.srcDir + "/content";
99
+ if (Fs.existsSync(content)) {
100
+ sources.content = content;
101
+ }
102
+ }
103
+ const output = options.output || defaults.assetsDir;
104
+ const matches = output.match(/([^[]+)(.*)?/);
105
+ const assetsDir = matches ? matches[1] : defaults.assetsDir;
106
+ const assetsPattern = (matches ? matches[2] : "") || defaults.assetsPattern;
107
+ interpolatePattern(assetsPattern, "", "", true);
108
+ if (options.extensions?.trim()) {
109
+ extensions.splice(0, extensions.length, ...matchWords(options.extensions));
110
+ } else if (options.additionalExtensions) {
111
+ extensions.push(...matchWords(options.additionalExtensions));
112
+ }
113
+ if (nuxt.options.content) {
114
+ (_a = nuxt.options.content).ignores || (_a.ignores = []);
115
+ }
116
+ function getAssetConfig(pattern, src, dir) {
117
+ let width = void 0;
118
+ let height = void 0;
119
+ if (options.imageAttrs && isImage(src)) {
120
+ const size = getImageSize(src);
121
+ width = size.width;
122
+ height = size.height;
123
+ }
124
+ const id = Path.join(Path.basename(dir), Path.relative(dir, src)).replaceAll("/", ":");
125
+ const file = interpolatePattern(pattern, src, dir);
126
+ const trg = Path.join(cachePath, assetsDir, file);
127
+ const rel = Path.join("/", assetsDir, file);
128
+ return { id, file, trg, rel, width, height };
129
+ }
130
+ const publicFolder = Path.join(cachePath, assetsDir);
131
+ const sourceFolders = Object.values(sources);
132
+ const assets = {};
133
+ sourceFolders.forEach((folder) => {
134
+ const pattern = `${folder}/**/*.{${extensions.join(",")}}`;
135
+ const paths = glob.globSync(pattern);
136
+ paths.forEach((src) => {
137
+ const config = getAssetConfig(assetsPattern, src, folder);
138
+ nuxt.options.content.ignores.push(config.id);
139
+ assets[src] = config;
140
+ });
141
+ });
142
+ nuxt.hook("build:before", function() {
143
+ Fs.mkdirSync(publicFolder, { recursive: true });
144
+ if (options.debug) {
145
+ const paths = Object.keys(assets).map((key) => " - " + assets[key].id.replaceAll(":", "/"));
146
+ log(`Copying ${Object.keys(assets).length} assets:
147
+
148
+ ${paths.join("\n")}
149
+ `);
150
+ }
151
+ Object.keys(assets).forEach((src) => {
152
+ const { trg } = assets[src];
153
+ const trgFolder = Path.dirname(trg);
154
+ Fs.mkdirSync(trgFolder, { recursive: true });
155
+ Fs.copyFileSync(src, trg);
156
+ });
157
+ });
158
+ const virtualConfig = [
159
+ `export const assets = ${JSON.stringify(assets)}`,
160
+ `export const sources = ${JSON.stringify(sources)}`
161
+ ].join("\n");
162
+ nuxt.options.alias[`#${name}`] = addTemplate({
163
+ filename: `${name}.mjs`,
164
+ getContents: () => virtualConfig
165
+ }).dst;
166
+ nuxt.hook("nitro:config", async (config) => {
167
+ config.plugins || (config.plugins = []);
168
+ config.plugins.push(pluginPath);
169
+ config.virtual || (config.virtual = {});
170
+ config.virtual[`#${name}`] = virtualConfig;
171
+ config.publicAssets || (config.publicAssets = []);
172
+ config.publicAssets.push({
173
+ dir: cachePath
174
+ // maxAge: 60 * 60 * 24 * 365 // 1 year
175
+ });
176
+ });
177
+ }
178
+ });
179
+
180
+ export { module as default };
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,52 @@
1
+ import Path from "path";
2
+ import { visit } from "unist-util-visit";
3
+ import { assets, sources } from "#content-assets";
4
+ import { tags } from "../../../config";
5
+ import { isValidAsset, walk } from "../../../utils";
6
+ function getDocPath(id) {
7
+ const parts = id.split(":");
8
+ const key = parts.shift();
9
+ const relPath = parts.join("/");
10
+ const absBase = sources[key];
11
+ return Path.join(absBase, relPath);
12
+ }
13
+ function getAsset(absDoc, relAsset) {
14
+ const absAsset = Path.join(Path.dirname(absDoc), relAsset);
15
+ return assets[absAsset] || {};
16
+ }
17
+ export default defineNitroPlugin(async (nitroApp) => {
18
+ nitroApp.hooks.hook("content:file:afterParse", async (file) => {
19
+ if (file._id.endsWith(".md")) {
20
+ const absDoc = getDocPath(file._id);
21
+ const filter = (value, key) => !(String(key).startsWith("_") || key === "body");
22
+ walk(file, (value, parent, key) => {
23
+ if (isValidAsset(value)) {
24
+ const { rel } = getAsset(absDoc, value);
25
+ if (rel) {
26
+ parent[key] = rel;
27
+ }
28
+ }
29
+ }, filter);
30
+ visit(file.body, (n) => tags.includes(n.tag), (node) => {
31
+ if (node.props.src) {
32
+ const { rel, width, height } = getAsset(absDoc, node.props.src);
33
+ if (rel) {
34
+ node.props.src = rel;
35
+ }
36
+ if (width && height) {
37
+ node.props.width = width;
38
+ node.props.height = height;
39
+ }
40
+ } else if (node.tag === "a") {
41
+ if (node.props.href) {
42
+ const { rel } = getAsset(absDoc, node.props.href);
43
+ if (rel) {
44
+ node.props.href = rel;
45
+ node.props.target = "_blank";
46
+ }
47
+ }
48
+ }
49
+ });
50
+ }
51
+ });
52
+ });
@@ -0,0 +1,10 @@
1
+
2
+ import { ModuleOptions } from './module'
3
+
4
+ declare module '@nuxt/schema' {
5
+ interface NuxtConfig { ['content-assets']?: Partial<ModuleOptions> }
6
+ interface NuxtOptions { ['content-assets']?: ModuleOptions }
7
+ }
8
+
9
+
10
+ export { ModuleOptions, default } from './module'
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "nuxt-content-assets",
3
+ "version": "0.5.1",
4
+ "description": "Enable locally-located assets in Nuxt Content",
5
+ "repository": "davestewart/nuxt-content-assets",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/types.d.ts",
11
+ "import": "./dist/module.mjs",
12
+ "require": "./dist/module.cjs"
13
+ }
14
+ },
15
+ "main": "./dist/module.cjs",
16
+ "types": "./dist/types.d.ts",
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "prepack": "nuxt-module-build",
22
+ "dev": "nuxi dev demo",
23
+ "dev:build": "nuxi build demo",
24
+ "dev:prepare": "nuxt-module-build --stub && nuxi prepare demo",
25
+ "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
26
+ "lint": "eslint .",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest watch"
29
+ },
30
+ "dependencies": {
31
+ "@nuxt/kit": "^3.3.2",
32
+ "glob": "^9.3.2",
33
+ "image-size": "^1.0.2",
34
+ "ohash": "^1.0.0",
35
+ "unist-util-visit": "^4.1.2"
36
+ },
37
+ "peerDependencies": {
38
+ "@nuxt/content": "latest"
39
+ },
40
+ "devDependencies": {
41
+ "@nuxt/content": "latest",
42
+ "@nuxt/eslint-config": "^0.1.1",
43
+ "@nuxt/module-builder": "^0.2.1",
44
+ "@nuxt/schema": "^3.3.2",
45
+ "@nuxt/test-utils": "^3.3.2",
46
+ "changelogen": "^0.5.1",
47
+ "eslint": "^8.36.0",
48
+ "nuxt": "^3.3.2",
49
+ "vitest": "^0.29.7"
50
+ }
51
+ }