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 ``, `<Base>`, `<Script>`, `<Style>`, `<Meta>`, `<Link>`, `<Body>` and `<Head>` components so that you can interact directly with your metadata within your component template. + +Because these component names match native HTML elements, it is very important that they are capitalized in the template. + +`<Head>` and `<Body>` can accept nested meta tags (for aesthetic reasons) but this has no effect on _where_ the nested meta tags are rendered in the final HTML. + +For example: + +```vue +<template> + <div> + Hello World + <Head :lang="dynamic > 50 ? 'en-GB' : 'en-US'"> + <Title>{{ dynamic }} title + + + + + + + + + +``` 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 @@ + + + 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"