Skip to content

Commit

Permalink
feat: Nuxt Content v3 (#398)
Browse files Browse the repository at this point in the history
  • Loading branch information
harlan-zw authored Jan 17, 2025
1 parent c37be55 commit 579f70e
Show file tree
Hide file tree
Showing 32 changed files with 1,789 additions and 112 deletions.
86 changes: 58 additions & 28 deletions docs/content/2.guides/5.content.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,37 @@ title: Nuxt Content
description: How to use the Nuxt Sitemap module with Nuxt Content.
---

Nuxt Sitemap integrates with Nuxt Content out of the box.
## Introduction

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

## Setup
## Setup Nuxt Content v3

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

When using `documentDriven` mode, all paths will be automatically added to the sitemap.
```ts [content.config.ts]
import { defineCollection, defineContentConfig, z } from '@nuxt/content'
import { asSitemapCollection } from '@nuxtjs/sitemap'

export default defineContentConfig({
collections: {
content: defineCollection(
// adds the robots frontmatter key to the collection
asSitemapCollection({
type: 'page',
source: '**/*.md',
}),
),
},
})
```


## Setup Nuxt Content v2

In Nuxt Content v2 markdown files require either [Document Driven Mode](https://content.nuxt.com/document-driven/introduction), a `path` key to be set
in the frontmatter or the `strictNuxtContentPaths` option to be enabled.

```ts [nuxt.config.ts]
export default defineNuxtConfig({
Expand All @@ -34,26 +55,9 @@ export default defineNuxtConfig({
})
```

### Markdown opt in

If you want to add markdown pages to your sitemap without any extra config, you can use the `sitemap` key on
your frontmatter. You must provide a `loc` value, or the page must have a `path`.

```md
---
sitemap:
loc: /my-page
lastmod: 2021-01-01
changefreq: monthly
priority: 0.8
---

# My Page
```

### Nuxt Content App Source
### Advanced: Nuxt Content App Source

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

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

Expand Down Expand Up @@ -86,8 +90,34 @@ export default defineNuxtConfig({
})
```

## Guides
## Usage

### Opt out from Sitemap
### Frontmatter `sitemap`

You can also disable the content from being used by passing in `sitemap: false` or `robots: false`.
Use the `sitemap` key in your frontmatter to add a page to your sitemap.

You can provide any data that you would normally provide in the sitemap configuration.

```md
---
sitemap:
loc: /my-page
lastmod: 2021-01-01
changefreq: monthly
priority: 0.8
---

# My Page
```

### Exclude from Sitemap

If you'd like to exclude a page from the sitemap, you can set `sitemap: false` in the frontmatter or `robots: false`
if you'd like to exclude it from search engines.

```md
---
sitemap: false
robots: false
---
```
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@
"ufo": "^1.5.4"
},
"devDependencies": {
"@nuxt/content": "^2.13.4",
"@nuxt/content": "3.0.0-alpha.9",
"@nuxt/content-v2": "npm:@nuxt/[email protected]",
"@nuxt/eslint-config": "^0.7.5",
"@nuxt/module-builder": "0.8.4",
"@nuxt/test-utils": "^3.15.4",
Expand All @@ -89,7 +90,8 @@
"h3",
"std-env",
"nitropack",
"consola"
"consola",
"@nuxt/content"
]
}
}
1,385 changes: 1,336 additions & 49 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
packages:
- client
- test/fixtures/**
- playground
54 changes: 54 additions & 0 deletions src/content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { z } from '@nuxt/content'

const sitemap = z.object({
loc: z.string().optional(),
lastmod: z.date().optional(),
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(),
priority: z.number().optional(),
images: z.array(z.object({
loc: z.string(),
caption: z.string().optional(),
geo_location: z.string().optional(),
title: z.string().optional(),
license: z.string().optional(),
})).optional(),
videos: z.array(z.object({
content_loc: z.string(),
player_loc: z.string().optional(),
duration: z.string().optional(),
expiration_date: z.date().optional(),
rating: z.number().optional(),
view_count: z.number().optional(),
publication_date: z.date().optional(),
family_friendly: z.boolean().optional(),
tag: z.string().optional(),
category: z.string().optional(),
restriction: z.object({
relationship: z.literal('allow').optional(),
value: z.string().optional(),
}).optional(),
gallery_loc: z.string().optional(),
price: z.string().optional(),
requires_subscription: z.boolean().optional(),
uploader: z.string().optional(),
})).optional(),
}).optional()

export function asSitemapCollection(collection: any) {
if (collection.type !== 'page') {
return
}
if (!collection.schema) {
collection.schema = z.object({
sitemap,
})
}
else {
collection.schema = collection.schema.extend({
sitemap,
})
}
collection._integrations = collection._integrations || []
collection._integrations.push('sitemap')
return collection
}
143 changes: 113 additions & 30 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ import {
defineNuxtModule,
getNuxtModuleVersion,
hasNuxtModule,
hasNuxtModuleCompatibility,
hasNuxtModuleCompatibility, resolveModule,
useLogger,
} from '@nuxt/kit'
import { joinURL, withBase, withLeadingSlash, withoutLeadingSlash, withoutTrailingSlash } from 'ufo'
import { installNuxtSiteConfig } from 'nuxt-site-config/kit'
import { defu } from 'defu'
import type { NitroRouteConfig } from 'nitropack'
import { readPackageJSON } from 'pkg-types'
import { dirname } from 'pathe'
import type { FileAfterParseHook } from '@nuxt/content'
import type { UseSeoMetaInput } from '@unhead/schema'
import type {
AppSourceContext,
AutoI18nConfig,
Expand All @@ -24,7 +27,7 @@ import type {
SitemapSourceBase,
SitemapSourceInput,
SitemapSourceResolved,
ModuleOptions as _ModuleOptions, FilterInput, I18nIntegrationOptions,
ModuleOptions as _ModuleOptions, FilterInput, I18nIntegrationOptions, SitemapUrl,
} from './runtime/types'
import { convertNuxtPagesToSitemapEntries, generateExtraRoutesFromNuxtConfig, resolveUrls } from './util/nuxtSitemap'
import { createNitroPromise, createPagesPromise, extendTypes, getNuxtModuleOptions, resolveNitroPreset } from './util/kit'
Expand All @@ -41,6 +44,8 @@ import { normalizeFilters } from './util/filter'
// eslint-disable-next-line
export interface ModuleOptions extends _ModuleOptions {}

export * from './content'

export default defineNuxtModule<ModuleOptions>({
meta: {
name: '@nuxtjs/sitemap',
Expand Down Expand Up @@ -352,34 +357,112 @@ declare module 'vue-router' {

// @ts-expect-error untyped
const isNuxtContentDocumentDriven = (!!nuxt.options.content?.documentDriven || config.strictNuxtContentPaths)
if (hasNuxtModule('@nuxt/content')) {
if (await hasNuxtModuleCompatibility('@nuxt/content', '^3')) {
logger.warn('Nuxt Sitemap does not work with Nuxt Content v3 yet, the integration will be disabled.')
}
else {
addServerPlugin(resolve('./runtime/server/plugins/nuxt-content'))
addServerHandler({
route: '/__sitemap__/nuxt-content-urls.json',
handler: resolve('./runtime/server/routes/__sitemap__/nuxt-content-urls'),
})
const tips: string[] = []
// @ts-expect-error untyped
if (nuxt.options.content?.documentDriven)
tips.push('Enabled because you\'re using `@nuxt/content` with `documentDriven: true`.')
else if (config.strictNuxtContentPaths)
tips.push('Enabled because you\'ve set `config.strictNuxtContentPaths: true`.')
else
tips.push('You can provide a `sitemap` key in your markdown frontmatter to configure specific URLs. Make sure you include a `loc`.')
const usingNuxtContent = hasNuxtModule('@nuxt/content')
const isNuxtContentV3 = usingNuxtContent && await hasNuxtModuleCompatibility('@nuxt/content', '^3')
const nuxtV3Collections = new Set<string>()
const isNuxtContentV2 = usingNuxtContent && await hasNuxtModuleCompatibility('@nuxt/content', '^2')
if (isNuxtContentV3) {
// TODO this is a hack until content gives us an alias
nuxt.options.alias['#sitemap/content-v3-nitro-path'] = resolve(dirname(resolveModule('@nuxt/content')), 'runtime/nitro')
// @ts-expect-error runtime type
nuxt.hooks.hook('content:file:afterParse', (ctx: FileAfterParseHook) => {
const content = ctx.content as {
body: { value: [string, Record<string, any>][] }
sitemap?: Partial<SitemapUrl>
path: string
seo: UseSeoMetaInput
updatedAt?: string
}
nuxtV3Collections.add(ctx.collection.name)
if (!('sitemap' in ctx.collection.fields)) {
return
}
// add any top level images
const images: SitemapUrl['images'] = []
if (config.discoverImages) {
images.push(...(content.body.value
?.filter(c =>
['image', 'img', 'nuxtimg', 'nuxt-img'].includes(c[0]),
)
.map(c => ({ loc: c[1].src })) || []),
)
}

appGlobalSources.push({
context: {
name: '@nuxt/content:urls',
description: 'Generated from your markdown files.',
tips,
},
fetch: '/__sitemap__/nuxt-content-urls.json',
})
// add any top level videos
const videos: SitemapUrl['videos'] = []
if (config.discoverVideos) {
// TODO
// videos.push(...(content.body.value
// .filter(c => c[0] === 'video' && c[1]?.src)
// .map(c => ({
// content_loc: c[1].src
// })) || []),
// )
}

const sitemapConfig = typeof content.sitemap === 'object' ? content.sitemap : {}
const lastmod = content.seo?.articleModifiedTime || content.updatedAt
const defaults: Partial<SitemapUrl> = {
loc: content.path,
}
if (images.length > 0)
defaults.images = images
if (videos.length > 0)
defaults.videos = videos
if (lastmod)
defaults.lastmod = lastmod
const definition = defu(sitemapConfig, defaults) as Partial<SitemapUrl>
if (!definition.loc) {
// user hasn't provided a loc... lets fallback to a relative path
if (content.path && content.path && content.path.startsWith('/'))
definition.loc = content.path
}
content.sitemap = definition
// loc is required
if (!definition.loc)
delete content.sitemap
ctx.content = content
})

addServerHandler({
route: '/__sitemap__/nuxt-content-urls.json',
handler: resolve('./runtime/server/routes/__sitemap__/nuxt-content-urls-v3'),
})
if (config.strictNuxtContentPaths) {
logger.warn('You have set `strictNuxtContentPaths: true` but are using @nuxt/content v3. This is not required, please remove it.')
}
appGlobalSources.push({
context: {
name: '@nuxt/content@v3:urls',
description: 'Generated from your markdown files.',
tips: [`Parsing the following collections: ${Array.from(nuxtV3Collections).join(', ')}`],
},
fetch: '/__sitemap__/nuxt-content-urls.json',
})
}
else if (isNuxtContentV2) {
addServerPlugin(resolve('./runtime/server/plugins/nuxt-content-v2'))
addServerHandler({
route: '/__sitemap__/nuxt-content-urls.json',
handler: resolve('./runtime/server/routes/__sitemap__/nuxt-content-urls-v2'),
})
const tips: string[] = []
// @ts-expect-error untyped
if (nuxt.options.content?.documentDriven)
tips.push('Enabled because you\'re using `@nuxt/content` with `documentDriven: true`.')
else if (config.strictNuxtContentPaths)
tips.push('Enabled because you\'ve set `config.strictNuxtContentPaths: true`.')
else
tips.push('You can provide a `sitemap` key in your markdown frontmatter to configure specific URLs. Make sure you include a `loc`.')

appGlobalSources.push({
context: {
name: '@nuxt/content@v2:urls',
description: 'Generated from your markdown files.',
tips,
},
fetch: '/__sitemap__/nuxt-content-urls.json',
})
}

// config -> sitemaps
Expand Down Expand Up @@ -567,9 +650,9 @@ declare module 'vue-router' {
// check for file in lastSegment using regex
const isExplicitFile = !!(lastSegment?.match(/\.[0-9a-z]+$/i)?.[0])
// avoid adding fallback pages to sitemap
if (r.error || ['/200.html', '/404.html', '/index.html'].includes(r.route))
if (isExplicitFile || r.error || ['/200.html', '/404.html', '/index.html'].includes(r.route))
return false
return (r.contentType?.includes('text/html') || !isExplicitFile)
return r.contentType?.includes('text/html')
})
.map(r => r._sitemap),
]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defu } from 'defu'
import type { ParsedContent } from '@nuxt/content'
import type { NitroApp } from 'nitropack'
import type { NitroApp } from 'nitropack/types'
import type { SitemapUrl } from '../../types'
import { useSimpleSitemapRuntimeConfig } from '../utils'
import { defineNitroPlugin } from '#imports'
Expand Down
Loading

0 comments on commit 579f70e

Please sign in to comment.