Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Nuxt Content v3 #398

Merged
merged 2 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading