diff --git a/.eslintrc b/.eslintrc
index 6208ca34b6..36f4a83e78 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -11,6 +11,7 @@
"rules": {
"no-console": "off",
"vue/one-component-per-file": "off",
+ "vue/require-default-prop": "off",
"jsdoc/require-jsdoc": "off",
"jsdoc/require-param": "off",
"jsdoc/require-returns": "off",
diff --git a/docs/content/2.app/4.meta-tags.md b/docs/content/2.app/4.meta-tags.md
index b1ed0582c0..27417b3078 100644
--- a/docs/content/2.app/4.meta-tags.md
+++ b/docs/content/2.app/4.meta-tags.md
@@ -1 +1,75 @@
# Meta Tags
+
+You can customize the meta tags for your site through several different ways:
+
+## `useMeta` Composable
+
+Within your `setup()` function you can call `useMeta` with an object of meta properties with keys corresponding to meta tags: `title`, `base`, `script`, `style`, `meta` and `link`, as well as `htmlAttrs` and `bodyAttrs`. Alternatively, you can pass a function returning the object for reactive metadata.
+
+For example:
+```ts
+import { ref } from 'vue'
+import { useMeta } from '@nuxt/app'
+
+export default {
+ setup () {
+ useMeta({
+ bodyAttrs: {
+ class: 'test'
+ }
+ })
+ }
+}
+```
+
+## `head` Component Property
+
+If you would prefer to use the options syntax, you can do exactly the same thing within your component options with an object or function called `head`. For example:
+
+```js
+export default {
+ head: {
+ script: [
+ {
+ async: true,
+ src: 'https://myscript.com/script.js'
+ }
+ ]
+ }
+}
+```
+
+**Note that `head()` does not have access to the component instance.**
+
+## Meta Components
+
+Nuxt provides `
`, ` `, `
+```
diff --git a/examples/meta/app.vue b/examples/meta/app.vue
new file mode 100644
index 0000000000..aee1dd85e0
--- /dev/null
+++ b/examples/meta/app.vue
@@ -0,0 +1,34 @@
+
+
+ Hello World
+
+
+
{{ dynamic }} title
+
+
+
+
+
+ Clickme
+
+
+
+
+
diff --git a/examples/meta/nuxt.config.ts b/examples/meta/nuxt.config.ts
new file mode 100644
index 0000000000..854fc6bfc5
--- /dev/null
+++ b/examples/meta/nuxt.config.ts
@@ -0,0 +1,4 @@
+import { defineNuxtConfig } from '@nuxt/kit'
+
+export default defineNuxtConfig({
+})
diff --git a/examples/meta/package.json b/examples/meta/package.json
new file mode 100644
index 0000000000..9cd6967f0d
--- /dev/null
+++ b/examples/meta/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "example-meta",
+ "private": true,
+ "devDependencies": {
+ "nuxt3": "latest"
+ },
+ "scripts": {
+ "dev": "nu dev",
+ "build": "nu build",
+ "start": "node .output/server"
+ }
+}
diff --git a/packages/app/build.config.ts b/packages/app/build.config.ts
index 735bcd21b3..d405d23f39 100644
--- a/packages/app/build.config.ts
+++ b/packages/app/build.config.ts
@@ -6,7 +6,6 @@ export default defineBuildConfig({
{ input: 'src/', name: 'app' }
],
dependencies: [
- '@vueuse/head',
'ohmyfetch',
'vue-router',
'vuex5'
diff --git a/packages/app/package.json b/packages/app/package.json
index 19e0106d72..fdddb994e7 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -20,7 +20,6 @@
"prepack": "unbuild"
},
"dependencies": {
- "@vueuse/head": "^0.6.0",
"hookable": "^4.4.1",
"ohmyfetch": "^0.2.0",
"upath": "^2.0.1",
@@ -28,6 +27,9 @@
"vue-router": "^4.0.10",
"vuex5": "^0.5.0-testing.3"
},
+ "peerDependencies": {
+ "@nuxt/meta": "^0.1.0"
+ },
"devDependencies": {
"unbuild": "^0.3.2"
}
diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts
index c4b91a3b54..e72eef67e4 100644
--- a/packages/app/src/index.ts
+++ b/packages/app/src/index.ts
@@ -1,2 +1,5 @@
export * from './nuxt'
export * from './composables'
+
+// @ts-ignore
+export * from '@nuxt/meta'
diff --git a/packages/app/src/nuxt.ts b/packages/app/src/nuxt.ts
index 191fa6cdbe..23f9ed1ab1 100644
--- a/packages/app/src/nuxt.ts
+++ b/packages/app/src/nuxt.ts
@@ -3,6 +3,15 @@ import Hookable from 'hookable'
import { defineGetter } from './utils'
import { legacyPlugin, LegacyContext } from './legacy'
+type NuxtMeta = {
+ htmlAttrs?: string
+ headAttrs?: string
+ bodyAttrs?: string
+ headTags?: string
+ bodyPrepend?: string
+ bodyScripts?: string
+}
+
export interface Nuxt {
app: App
globalName: string
@@ -16,7 +25,9 @@ export interface Nuxt {
_asyncDataPromises?: Record>
_legacyContext?: LegacyContext
- ssrContext?: Record
+ ssrContext?: Record & {
+ renderMeta: () => Promise | NuxtMeta
+ }
payload: {
serverRendered?: true
data?: Record
diff --git a/packages/app/src/plugins/head/head.ts b/packages/app/src/plugins/head/head.ts
deleted file mode 100644
index 22ba9249e7..0000000000
--- a/packages/app/src/plugins/head/head.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { defineComponent } from '@vue/runtime-core'
-import { useHead, HeadObject } from '@vueuse/head'
-
-type MappedProps> = {
- [P in keyof T]: { type: () => T[P] }
-}
-
-const props: MappedProps = {
- base: { type: Object },
- bodyAttrs: { type: Object },
- htmlAttrs: { type: Object },
- link: { type: Array },
- meta: { type: Array },
- script: { type: Array },
- style: { type: Array },
- title: { type: String }
-}
-
-export const Head = defineComponent({
- props,
- setup (props, { slots }) {
- useHead(() => props)
-
- return () => slots.default?.()
- }
-})
-
-const createHeadComponent = (prop: keyof typeof props, isArray = false) =>
- defineComponent({
- setup (_props, { attrs, slots }) {
- useHead(() => ({
- [prop]: isArray ? [attrs] : attrs
- }))
-
- return () => slots.default?.()
- }
- })
-
-const createHeadComponentFromSlot = (prop: keyof typeof props) =>
- defineComponent({
- setup (_props, { slots }) {
- useHead(() => ({
- [prop]: slots.default?.()[0]?.children
- }))
-
- return () => null
- }
- })
-
-export const Html = createHeadComponent('htmlAttrs')
-export const Body = createHeadComponent('bodyAttrs')
-export const Title = createHeadComponentFromSlot('title')
-export const Meta = createHeadComponent('meta', true)
-export const Link = createHeadComponent('link', true)
-export const Script = createHeadComponent('script', true)
-export const Style = createHeadComponent('style', true)
diff --git a/packages/app/src/plugins/head/index.ts b/packages/app/src/plugins/head/index.ts
deleted file mode 100644
index e8cec35722..0000000000
--- a/packages/app/src/plugins/head/index.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { createHead, renderHeadToString } from '@vueuse/head'
-import { defineNuxtPlugin } from '@nuxt/app'
-import { Head, Html, Body, Title, Meta, Link, Script, Style } from './head'
-
-export default defineNuxtPlugin((nuxt) => {
- const { app, ssrContext } = nuxt
- const head = createHead()
-
- app.use(head)
-
- app.component('NuxtHead', Head)
- app.component('NuxtHtml', Html)
- app.component('NuxtBody', Body)
- app.component('NuxtTitle', Title)
- app.component('NuxtMeta', Meta)
- app.component('NuxtHeadLink', Link)
- app.component('NuxtScript', Script)
- app.component('NuxtStyle', Style)
-
- if (process.server) {
- ssrContext.head = () => renderHeadToString(head)
- }
-})
diff --git a/packages/meta/build.config.ts b/packages/meta/build.config.ts
new file mode 100644
index 0000000000..259ec1c972
--- /dev/null
+++ b/packages/meta/build.config.ts
@@ -0,0 +1,14 @@
+import { defineBuildConfig } from 'unbuild'
+
+export default defineBuildConfig({
+ declaration: true,
+ entries: [
+ 'src/module',
+ { input: 'src/runtime/', outDir: 'dist/runtime', format: 'esm' }
+ ],
+ externals: [
+ '@vue/reactivity',
+ '@vue/shared',
+ '@vueuse/head'
+ ]
+})
diff --git a/packages/meta/module.js b/packages/meta/module.js
new file mode 100644
index 0000000000..d6b0665c07
--- /dev/null
+++ b/packages/meta/module.js
@@ -0,0 +1 @@
+module.exports = require('./dist/module')
diff --git a/packages/meta/package.json b/packages/meta/package.json
new file mode 100644
index 0000000000..ad613a9de4
--- /dev/null
+++ b/packages/meta/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "@nuxt/meta",
+ "version": "0.1.0",
+ "repository": "nuxt/framework",
+ "license": "MIT",
+ "exports": {
+ ".": "./dist/runtime/index.mjs",
+ "./module": {
+ "import": "./dist/module.mjs",
+ "require": "./dist/module.js"
+ }
+ },
+ "types": "./dist/runtime/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "prepack": "unbuild"
+ },
+ "dependencies": {
+ "@nuxt/kit": "^0.6.4",
+ "@vueuse/head": "^0.6.0",
+ "upath": "^2.0.1"
+ },
+ "devDependencies": {
+ "unbuild": "^0.3.1",
+ "vue-meta": "3.0.0-alpha.9"
+ },
+ "peerDependencies": {
+ "@vue/reactivity": "3.1.1",
+ "@vue/shared": "3.1.1",
+ "vue": "3.1.1"
+ }
+}
diff --git a/packages/meta/src/module.ts b/packages/meta/src/module.ts
new file mode 100644
index 0000000000..f0639481e0
--- /dev/null
+++ b/packages/meta/src/module.ts
@@ -0,0 +1,17 @@
+import { resolve } from 'upath'
+import { defineNuxtModule } from '@nuxt/kit'
+
+export default defineNuxtModule({
+ name: 'meta',
+ setup (_options, nuxt) {
+ const runtimeDir = resolve(__dirname, 'runtime')
+
+ nuxt.options.build.transpile.push('@nuxt/meta', runtimeDir)
+ nuxt.options.alias['@nuxt/meta'] = resolve(runtimeDir, 'index')
+
+ nuxt.hook('app:resolve', (app) => {
+ app.plugins.push({ src: resolve(runtimeDir, 'vueuse-head') })
+ app.plugins.push({ src: resolve(runtimeDir, 'meta') })
+ })
+ }
+})
diff --git a/packages/meta/src/runtime/components.ts b/packages/meta/src/runtime/components.ts
new file mode 100644
index 0000000000..07568a04ab
--- /dev/null
+++ b/packages/meta/src/runtime/components.ts
@@ -0,0 +1,209 @@
+import { defineComponent, SetupContext } from 'vue'
+import { useMeta } from './index'
+
+type Props = Readonly>
+
+const removeUndefinedProps = (props: Props) =>
+ Object.fromEntries(Object.entries(props).filter(([_key, value]) => value !== undefined))
+
+const setupForUseMeta = (metaFactory: (props: Props, ctx: SetupContext) => Record, renderChild?: boolean) => (props: Props, ctx: SetupContext) => {
+ useMeta(() => metaFactory(removeUndefinedProps(props), ctx))
+ return () => renderChild ? ctx.slots.default?.() : null
+}
+
+const globalProps = {
+ accesskey: String,
+ autocapitalize: String,
+ autofocus: {
+ type: Boolean,
+ default: undefined
+ },
+ class: String,
+ contenteditable: {
+ type: Boolean,
+ default: undefined
+ },
+ contextmenu: String,
+ dir: String,
+ draggable: {
+ type: Boolean,
+ default: undefined
+ },
+ enterkeyhint: String,
+ exportparts: String,
+ hidden: {
+ type: Boolean,
+ default: undefined
+ },
+ id: String,
+ inputmode: String,
+ is: String,
+ itemid: String,
+ itemprop: String,
+ itemref: String,
+ itemscope: String,
+ itemtype: String,
+ lang: String,
+ nonce: String,
+ part: String,
+ slot: String,
+ spellcheck: {
+ type: Boolean,
+ default: undefined
+ },
+ style: String,
+ tabindex: String,
+ title: String,
+ translate: String
+}
+
+// `
- const _html = rendered.html
+ const html = rendered.html
- const meta = {
- htmlAttrs: '',
- bodyAttrs: '',
- headAttrs: '',
- headTags: '',
- bodyTags: '',
- bodyScriptsPrepend: '',
- bodyScripts: ''
+ if ('renderMeta' in ssrContext) {
+ rendered.meta = await ssrContext.renderMeta()
}
- // @vueuse/head
- if (typeof ssrContext.head === 'function') {
- Object.assign(meta, ssrContext.head())
- }
-
- // vue-meta
- if (ssrContext.meta && typeof ssrContext.meta.inject === 'function') {
- const _meta = ssrContext.meta.inject({
- isSSR: ssrContext.nuxt.serverRendered,
- ln: process.env.NODE_ENV === 'development'
- })
- meta.htmlAttrs += _meta.htmlAttrs.text()
- meta.headAttrs += _meta.headAttrs.text()
- meta.headTags +=
- _meta.title.text() + _meta.base.text() +
- _meta.meta.text() + _meta.link.text() +
- _meta.style.text() + _meta.script.text() +
- _meta.noscript.text()
- meta.bodyAttrs += _meta.bodyAttrs.text()
- meta.bodyScriptsPrepend =
- _meta.meta.text({ pbody: true }) + _meta.link.text({ pbody: true }) +
- _meta.style.text({ pbody: true }) + _meta.script.text({ pbody: true }) +
- _meta.noscript.text({ pbody: true })
- meta.bodyScripts =
- _meta.meta.text({ body: true }) + _meta.link.text({ body: true }) +
- _meta.style.text({ body: true }) + _meta.script.text({ body: true }) +
- _meta.noscript.text({ body: true })
- }
+ const {
+ htmlAttrs = '',
+ bodyAttrs = '',
+ headAttrs = '',
+ headTags = '',
+ bodyScriptsPrepend = '',
+ bodyScripts = ''
+ } = rendered.meta || {}
return htmlTemplate({
- HTML_ATTRS: meta.htmlAttrs,
- HEAD_ATTRS: meta.headAttrs,
- HEAD: meta.headTags +
+ HTML_ATTRS: htmlAttrs,
+ HEAD_ATTRS: headAttrs,
+ HEAD: headTags +
rendered.renderResourceHints() + rendered.renderStyles() + (ssrContext.styles || ''),
- BODY_ATTRS: meta.bodyAttrs,
- APP: meta.bodyScriptsPrepend + _html + state + rendered.renderScripts() + meta.bodyScripts
+ BODY_ATTRS: bodyAttrs,
+ APP: bodyScriptsPrepend + html + state + rendered.renderScripts() + bodyScripts
})
}
diff --git a/packages/nuxt3/package.json b/packages/nuxt3/package.json
index 53435a9631..19c707a233 100644
--- a/packages/nuxt3/package.json
+++ b/packages/nuxt3/package.json
@@ -4,6 +4,11 @@
"repository": "nuxt/framework",
"license": "MIT",
"main": "./dist/index.js",
+ "bin": {
+ "nu": "./bin/nuxt.js",
+ "nuxt": "./bin/nuxt.js",
+ "nuxt-cli": "./bin/nuxt.js"
+ },
"files": [
"bin",
"dist"
@@ -11,15 +16,11 @@
"scripts": {
"prepack": "unbuild $@ || true"
},
- "bin": {
- "nu": "./bin/nuxt.js",
- "nuxt": "./bin/nuxt.js",
- "nuxt-cli": "./bin/nuxt.js"
- },
"dependencies": {
"@nuxt/app": "^0.5.0",
"@nuxt/component-discovery": "^0.2.0",
"@nuxt/kit": "^0.6.4",
+ "@nuxt/meta": "^0.1.0",
"@nuxt/nitro": "^0.9.1",
"@nuxt/pages": "^0.3.0",
"@nuxt/vite-builder": "^0.5.0",
diff --git a/packages/nuxt3/src/nuxt.ts b/packages/nuxt3/src/nuxt.ts
index 81b93c8b00..0031c264a0 100644
--- a/packages/nuxt3/src/nuxt.ts
+++ b/packages/nuxt3/src/nuxt.ts
@@ -56,6 +56,7 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise {
options._majorVersion = 3
options.alias.vue = normalize(require.resolve('vue/dist/vue.esm-bundler.js'))
options.buildModules.push(normalize(require.resolve('@nuxt/pages/module')))
+ options.buildModules.push(normalize(require.resolve('@nuxt/meta/module')))
options.buildModules.push(normalize(require.resolve('@nuxt/component-discovery/module')))
const nuxt = createNuxt(options)
diff --git a/yarn.lock b/yarn.lock
index 380c483672..82ae2b33e5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1279,7 +1279,6 @@ __metadata:
version: 0.0.0-use.local
resolution: "@nuxt/app@workspace:packages/app"
dependencies:
- "@vueuse/head": ^0.6.0
hookable: ^4.4.1
ohmyfetch: ^0.2.0
unbuild: ^0.3.2
@@ -1287,6 +1286,8 @@ __metadata:
vue: ^3.1.4
vue-router: ^4.0.10
vuex5: ^0.5.0-testing.3
+ peerDependencies:
+ "@nuxt/meta": ^0.1.0
languageName: unknown
linkType: soft
@@ -1355,6 +1356,22 @@ __metadata:
languageName: unknown
linkType: soft
+"@nuxt/meta@^0.1.0, @nuxt/meta@workspace:packages/meta":
+ version: 0.0.0-use.local
+ resolution: "@nuxt/meta@workspace:packages/meta"
+ dependencies:
+ "@nuxt/kit": ^0.6.4
+ "@vueuse/head": ^0.6.0
+ unbuild: ^0.3.1
+ upath: ^2.0.1
+ vue-meta: 3.0.0-alpha.9
+ peerDependencies:
+ "@vue/reactivity": 3.1.1
+ "@vue/shared": 3.1.1
+ vue: 3.1.1
+ languageName: unknown
+ linkType: soft
+
"@nuxt/nitro@^0.9.1, @nuxt/nitro@workspace:packages/nitro":
version: 0.0.0-use.local
resolution: "@nuxt/nitro@workspace:packages/nitro"
@@ -5416,6 +5433,14 @@ __metadata:
languageName: unknown
linkType: soft
+"example-meta@workspace:examples/meta":
+ version: 0.0.0-use.local
+ resolution: "example-meta@workspace:examples/meta"
+ dependencies:
+ nuxt3: latest
+ languageName: unknown
+ linkType: soft
+
"example-pages@workspace:examples/pages":
version: 0.0.0-use.local
resolution: "example-pages@workspace:examples/pages"
@@ -8621,6 +8646,7 @@ fsevents@~2.3.2:
"@nuxt/app": ^0.5.0
"@nuxt/component-discovery": ^0.2.0
"@nuxt/kit": ^0.6.4
+ "@nuxt/meta": ^0.1.0
"@nuxt/nitro": ^0.9.1
"@nuxt/pages": ^0.3.0
"@nuxt/vite-builder": ^0.5.0
@@ -11667,7 +11693,7 @@ fsevents@~2.3.2:
languageName: node
linkType: hard
-"unbuild@npm:^0.3.2":
+"unbuild@npm:^0.3.1, unbuild@npm:^0.3.2":
version: 0.3.2
resolution: "unbuild@npm:0.3.2"
dependencies:
@@ -11987,6 +12013,15 @@ fsevents@~2.3.2:
languageName: node
linkType: hard
+"vue-meta@npm:3.0.0-alpha.9":
+ version: 3.0.0-alpha.9
+ resolution: "vue-meta@npm:3.0.0-alpha.9"
+ peerDependencies:
+ vue: ^3.0.0
+ checksum: bc0ccfee2b2fa9bdbf7a382a4af671def09c79ba2bbf3c89e54d642ec2de18cc1ad91f3ecd266e3cc8eb01547cef743648fd63b4392dd1c4b5663c6f7b94351f
+ languageName: node
+ linkType: hard
+
"vue-router@npm:^4.0.10":
version: 4.0.10
resolution: "vue-router@npm:4.0.10"