Skip to content

Commit 579f70e

Browse files
authored
feat: Nuxt Content v3 (nuxt-modules#398)
1 parent c37be55 commit 579f70e

32 files changed

+1789
-112
lines changed

docs/content/2.guides/5.content.md

+58-28
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,37 @@ title: Nuxt Content
33
description: How to use the Nuxt Sitemap module with Nuxt Content.
44
---
55

6-
Nuxt Sitemap integrates with Nuxt Content out of the box.
6+
## Introduction
77

8-
It comes with automatic configuration when using document driven mode.
9-
Otherwise, you can opt in on each markdown file or set up your own [app source](/docs/sitemap/getting-started/data-sources).
8+
Nuxt Sitemap comes with an integration for Nuxt Content that allows you to configure your sitemap entry straight from your markdown directly.
109

11-
## Setup
10+
## Setup Nuxt Content v3
1211

13-
### Document Driven Mode
12+
In Nuxt Content v3 we need to use the `asSitemapCollection()`{lang="ts"} function to augment any collections
13+
to be able to use the `sitemap` frontmatter key.
1414

15-
When using `documentDriven` mode, all paths will be automatically added to the sitemap.
15+
```ts [content.config.ts]
16+
import { defineCollection, defineContentConfig, z } from '@nuxt/content'
17+
import { asSitemapCollection } from '@nuxtjs/sitemap'
18+
19+
export default defineContentConfig({
20+
collections: {
21+
content: defineCollection(
22+
// adds the robots frontmatter key to the collection
23+
asSitemapCollection({
24+
type: 'page',
25+
source: '**/*.md',
26+
}),
27+
),
28+
},
29+
})
30+
```
31+
32+
33+
## Setup Nuxt Content v2
34+
35+
In Nuxt Content v2 markdown files require either [Document Driven Mode](https://content.nuxt.com/document-driven/introduction), a `path` key to be set
36+
in the frontmatter or the `strictNuxtContentPaths` option to be enabled.
1637

1738
```ts [nuxt.config.ts]
1839
export default defineNuxtConfig({
@@ -34,26 +55,9 @@ export default defineNuxtConfig({
3455
})
3556
```
3657

37-
### Markdown opt in
38-
39-
If you want to add markdown pages to your sitemap without any extra config, you can use the `sitemap` key on
40-
your frontmatter. You must provide a `loc` value, or the page must have a `path`.
41-
42-
```md
43-
---
44-
sitemap:
45-
loc: /my-page
46-
lastmod: 2021-01-01
47-
changefreq: monthly
48-
priority: 0.8
49-
---
50-
51-
# My Page
52-
```
53-
54-
### Nuxt Content App Source
58+
### Advanced: Nuxt Content App Source
5559

56-
If you'd like to set up a more automated Nuxt Content integration and your not using Document Driven mode, you can add content to the sitemap as you would with [Dynamic URLs](/docs/sitemap/guides/dynamic-urls).
60+
If you'd like to set up a more automated Nuxt Content integration and you're not using Document Driven mode, you can add content to the sitemap as you would with [Dynamic URLs](/docs/sitemap/guides/dynamic-urls).
5761

5862
An example of what this might look like is below, customize to your own needs.
5963

@@ -86,8 +90,34 @@ export default defineNuxtConfig({
8690
})
8791
```
8892

89-
## Guides
93+
## Usage
9094

91-
### Opt out from Sitemap
95+
### Frontmatter `sitemap`
9296

93-
You can also disable the content from being used by passing in `sitemap: false` or `robots: false`.
97+
Use the `sitemap` key in your frontmatter to add a page to your sitemap.
98+
99+
You can provide any data that you would normally provide in the sitemap configuration.
100+
101+
```md
102+
---
103+
sitemap:
104+
loc: /my-page
105+
lastmod: 2021-01-01
106+
changefreq: monthly
107+
priority: 0.8
108+
---
109+
110+
# My Page
111+
```
112+
113+
### Exclude from Sitemap
114+
115+
If you'd like to exclude a page from the sitemap, you can set `sitemap: false` in the frontmatter or `robots: false`
116+
if you'd like to exclude it from search engines.
117+
118+
```md
119+
---
120+
sitemap: false
121+
robots: false
122+
---
123+
```

package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@
6464
"ufo": "^1.5.4"
6565
},
6666
"devDependencies": {
67-
"@nuxt/content": "^2.13.4",
67+
"@nuxt/content": "3.0.0-alpha.9",
68+
"@nuxt/content-v2": "npm:@nuxt/[email protected]",
6869
"@nuxt/eslint-config": "^0.7.5",
6970
"@nuxt/module-builder": "0.8.4",
7071
"@nuxt/test-utils": "^3.15.4",
@@ -89,7 +90,8 @@
8990
"h3",
9091
"std-env",
9192
"nitropack",
92-
"consola"
93+
"consola",
94+
"@nuxt/content"
9395
]
9496
}
9597
}

pnpm-lock.yaml

+1,336-49
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
packages:
22
- client
3+
- test/fixtures/**
4+
- playground

src/content.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { z } from '@nuxt/content'
2+
3+
const sitemap = z.object({
4+
loc: z.string().optional(),
5+
lastmod: z.date().optional(),
6+
changefreq: z.union([z.literal('always'), z.literal('hourly'), z.literal('daily'), z.literal('weekly'), z.literal('monthly'), z.literal('yearly'), z.literal('never')]).optional(),
7+
priority: z.number().optional(),
8+
images: z.array(z.object({
9+
loc: z.string(),
10+
caption: z.string().optional(),
11+
geo_location: z.string().optional(),
12+
title: z.string().optional(),
13+
license: z.string().optional(),
14+
})).optional(),
15+
videos: z.array(z.object({
16+
content_loc: z.string(),
17+
player_loc: z.string().optional(),
18+
duration: z.string().optional(),
19+
expiration_date: z.date().optional(),
20+
rating: z.number().optional(),
21+
view_count: z.number().optional(),
22+
publication_date: z.date().optional(),
23+
family_friendly: z.boolean().optional(),
24+
tag: z.string().optional(),
25+
category: z.string().optional(),
26+
restriction: z.object({
27+
relationship: z.literal('allow').optional(),
28+
value: z.string().optional(),
29+
}).optional(),
30+
gallery_loc: z.string().optional(),
31+
price: z.string().optional(),
32+
requires_subscription: z.boolean().optional(),
33+
uploader: z.string().optional(),
34+
})).optional(),
35+
}).optional()
36+
37+
export function asSitemapCollection(collection: any) {
38+
if (collection.type !== 'page') {
39+
return
40+
}
41+
if (!collection.schema) {
42+
collection.schema = z.object({
43+
sitemap,
44+
})
45+
}
46+
else {
47+
collection.schema = collection.schema.extend({
48+
sitemap,
49+
})
50+
}
51+
collection._integrations = collection._integrations || []
52+
collection._integrations.push('sitemap')
53+
return collection
54+
}

src/module.ts

+113-30
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ import {
77
defineNuxtModule,
88
getNuxtModuleVersion,
99
hasNuxtModule,
10-
hasNuxtModuleCompatibility,
10+
hasNuxtModuleCompatibility, resolveModule,
1111
useLogger,
1212
} from '@nuxt/kit'
1313
import { joinURL, withBase, withLeadingSlash, withoutLeadingSlash, withoutTrailingSlash } from 'ufo'
1414
import { installNuxtSiteConfig } from 'nuxt-site-config/kit'
1515
import { defu } from 'defu'
1616
import type { NitroRouteConfig } from 'nitropack'
1717
import { readPackageJSON } from 'pkg-types'
18+
import { dirname } from 'pathe'
19+
import type { FileAfterParseHook } from '@nuxt/content'
20+
import type { UseSeoMetaInput } from '@unhead/schema'
1821
import type {
1922
AppSourceContext,
2023
AutoI18nConfig,
@@ -24,7 +27,7 @@ import type {
2427
SitemapSourceBase,
2528
SitemapSourceInput,
2629
SitemapSourceResolved,
27-
ModuleOptions as _ModuleOptions, FilterInput, I18nIntegrationOptions,
30+
ModuleOptions as _ModuleOptions, FilterInput, I18nIntegrationOptions, SitemapUrl,
2831
} from './runtime/types'
2932
import { convertNuxtPagesToSitemapEntries, generateExtraRoutesFromNuxtConfig, resolveUrls } from './util/nuxtSitemap'
3033
import { createNitroPromise, createPagesPromise, extendTypes, getNuxtModuleOptions, resolveNitroPreset } from './util/kit'
@@ -41,6 +44,8 @@ import { normalizeFilters } from './util/filter'
4144
// eslint-disable-next-line
4245
export interface ModuleOptions extends _ModuleOptions {}
4346

47+
export * from './content'
48+
4449
export default defineNuxtModule<ModuleOptions>({
4550
meta: {
4651
name: '@nuxtjs/sitemap',
@@ -352,34 +357,112 @@ declare module 'vue-router' {
352357

353358
// @ts-expect-error untyped
354359
const isNuxtContentDocumentDriven = (!!nuxt.options.content?.documentDriven || config.strictNuxtContentPaths)
355-
if (hasNuxtModule('@nuxt/content')) {
356-
if (await hasNuxtModuleCompatibility('@nuxt/content', '^3')) {
357-
logger.warn('Nuxt Sitemap does not work with Nuxt Content v3 yet, the integration will be disabled.')
358-
}
359-
else {
360-
addServerPlugin(resolve('./runtime/server/plugins/nuxt-content'))
361-
addServerHandler({
362-
route: '/__sitemap__/nuxt-content-urls.json',
363-
handler: resolve('./runtime/server/routes/__sitemap__/nuxt-content-urls'),
364-
})
365-
const tips: string[] = []
366-
// @ts-expect-error untyped
367-
if (nuxt.options.content?.documentDriven)
368-
tips.push('Enabled because you\'re using `@nuxt/content` with `documentDriven: true`.')
369-
else if (config.strictNuxtContentPaths)
370-
tips.push('Enabled because you\'ve set `config.strictNuxtContentPaths: true`.')
371-
else
372-
tips.push('You can provide a `sitemap` key in your markdown frontmatter to configure specific URLs. Make sure you include a `loc`.')
360+
const usingNuxtContent = hasNuxtModule('@nuxt/content')
361+
const isNuxtContentV3 = usingNuxtContent && await hasNuxtModuleCompatibility('@nuxt/content', '^3')
362+
const nuxtV3Collections = new Set<string>()
363+
const isNuxtContentV2 = usingNuxtContent && await hasNuxtModuleCompatibility('@nuxt/content', '^2')
364+
if (isNuxtContentV3) {
365+
// TODO this is a hack until content gives us an alias
366+
nuxt.options.alias['#sitemap/content-v3-nitro-path'] = resolve(dirname(resolveModule('@nuxt/content')), 'runtime/nitro')
367+
// @ts-expect-error runtime type
368+
nuxt.hooks.hook('content:file:afterParse', (ctx: FileAfterParseHook) => {
369+
const content = ctx.content as {
370+
body: { value: [string, Record<string, any>][] }
371+
sitemap?: Partial<SitemapUrl>
372+
path: string
373+
seo: UseSeoMetaInput
374+
updatedAt?: string
375+
}
376+
nuxtV3Collections.add(ctx.collection.name)
377+
if (!('sitemap' in ctx.collection.fields)) {
378+
return
379+
}
380+
// add any top level images
381+
const images: SitemapUrl['images'] = []
382+
if (config.discoverImages) {
383+
images.push(...(content.body.value
384+
?.filter(c =>
385+
['image', 'img', 'nuxtimg', 'nuxt-img'].includes(c[0]),
386+
)
387+
.map(c => ({ loc: c[1].src })) || []),
388+
)
389+
}
373390

374-
appGlobalSources.push({
375-
context: {
376-
name: '@nuxt/content:urls',
377-
description: 'Generated from your markdown files.',
378-
tips,
379-
},
380-
fetch: '/__sitemap__/nuxt-content-urls.json',
381-
})
391+
// add any top level videos
392+
const videos: SitemapUrl['videos'] = []
393+
if (config.discoverVideos) {
394+
// TODO
395+
// videos.push(...(content.body.value
396+
// .filter(c => c[0] === 'video' && c[1]?.src)
397+
// .map(c => ({
398+
// content_loc: c[1].src
399+
// })) || []),
400+
// )
401+
}
402+
403+
const sitemapConfig = typeof content.sitemap === 'object' ? content.sitemap : {}
404+
const lastmod = content.seo?.articleModifiedTime || content.updatedAt
405+
const defaults: Partial<SitemapUrl> = {
406+
loc: content.path,
407+
}
408+
if (images.length > 0)
409+
defaults.images = images
410+
if (videos.length > 0)
411+
defaults.videos = videos
412+
if (lastmod)
413+
defaults.lastmod = lastmod
414+
const definition = defu(sitemapConfig, defaults) as Partial<SitemapUrl>
415+
if (!definition.loc) {
416+
// user hasn't provided a loc... lets fallback to a relative path
417+
if (content.path && content.path && content.path.startsWith('/'))
418+
definition.loc = content.path
419+
}
420+
content.sitemap = definition
421+
// loc is required
422+
if (!definition.loc)
423+
delete content.sitemap
424+
ctx.content = content
425+
})
426+
427+
addServerHandler({
428+
route: '/__sitemap__/nuxt-content-urls.json',
429+
handler: resolve('./runtime/server/routes/__sitemap__/nuxt-content-urls-v3'),
430+
})
431+
if (config.strictNuxtContentPaths) {
432+
logger.warn('You have set `strictNuxtContentPaths: true` but are using @nuxt/content v3. This is not required, please remove it.')
382433
}
434+
appGlobalSources.push({
435+
context: {
436+
name: '@nuxt/content@v3:urls',
437+
description: 'Generated from your markdown files.',
438+
tips: [`Parsing the following collections: ${Array.from(nuxtV3Collections).join(', ')}`],
439+
},
440+
fetch: '/__sitemap__/nuxt-content-urls.json',
441+
})
442+
}
443+
else if (isNuxtContentV2) {
444+
addServerPlugin(resolve('./runtime/server/plugins/nuxt-content-v2'))
445+
addServerHandler({
446+
route: '/__sitemap__/nuxt-content-urls.json',
447+
handler: resolve('./runtime/server/routes/__sitemap__/nuxt-content-urls-v2'),
448+
})
449+
const tips: string[] = []
450+
// @ts-expect-error untyped
451+
if (nuxt.options.content?.documentDriven)
452+
tips.push('Enabled because you\'re using `@nuxt/content` with `documentDriven: true`.')
453+
else if (config.strictNuxtContentPaths)
454+
tips.push('Enabled because you\'ve set `config.strictNuxtContentPaths: true`.')
455+
else
456+
tips.push('You can provide a `sitemap` key in your markdown frontmatter to configure specific URLs. Make sure you include a `loc`.')
457+
458+
appGlobalSources.push({
459+
context: {
460+
name: '@nuxt/content@v2:urls',
461+
description: 'Generated from your markdown files.',
462+
tips,
463+
},
464+
fetch: '/__sitemap__/nuxt-content-urls.json',
465+
})
383466
}
384467

385468
// config -> sitemaps
@@ -567,9 +650,9 @@ declare module 'vue-router' {
567650
// check for file in lastSegment using regex
568651
const isExplicitFile = !!(lastSegment?.match(/\.[0-9a-z]+$/i)?.[0])
569652
// avoid adding fallback pages to sitemap
570-
if (r.error || ['/200.html', '/404.html', '/index.html'].includes(r.route))
653+
if (isExplicitFile || r.error || ['/200.html', '/404.html', '/index.html'].includes(r.route))
571654
return false
572-
return (r.contentType?.includes('text/html') || !isExplicitFile)
655+
return r.contentType?.includes('text/html')
573656
})
574657
.map(r => r._sitemap),
575658
]

src/runtime/server/plugins/nuxt-content.ts src/runtime/server/plugins/nuxt-content-v2.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { defu } from 'defu'
22
import type { ParsedContent } from '@nuxt/content'
3-
import type { NitroApp } from 'nitropack'
3+
import type { NitroApp } from 'nitropack/types'
44
import type { SitemapUrl } from '../../types'
55
import { useSimpleSitemapRuntimeConfig } from '../utils'
66
import { defineNitroPlugin } from '#imports'

0 commit comments

Comments
 (0)