mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-29 09:02:03 +00:00
feat: @nuxt/meta
module for head rendering (#179)
Co-authored-by: Anthony Fu <hi@antfu.me> Co-authored-by: Pooya Parsa <pyapar@gmail.com>
This commit is contained in:
parent
0bbbddecba
commit
b263b4f930
@ -11,6 +11,7 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
"vue/one-component-per-file": "off",
|
"vue/one-component-per-file": "off",
|
||||||
|
"vue/require-default-prop": "off",
|
||||||
"jsdoc/require-jsdoc": "off",
|
"jsdoc/require-jsdoc": "off",
|
||||||
"jsdoc/require-param": "off",
|
"jsdoc/require-param": "off",
|
||||||
"jsdoc/require-returns": "off",
|
"jsdoc/require-returns": "off",
|
||||||
|
@ -1 +1,75 @@
|
|||||||
# Meta Tags
|
# 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 `<Title>`, `<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</Title>
|
||||||
|
<Meta name="description" :content="`My page's ${dynamic} description`" />
|
||||||
|
<Link rel="preload" href="/test.txt" as="script" />
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<button class="blue" @click="dynamic = Math.random() * 100">
|
||||||
|
Click me
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data: () => ({ dynamic: 49 })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
34
examples/meta/app.vue
Normal file
34
examples/meta/app.vue
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
Hello World
|
||||||
|
|
||||||
|
<Head :lang="'' + dynamic">
|
||||||
|
<Title>{{ dynamic }} title</Title>
|
||||||
|
<Meta name="description" :content="`My page's ${dynamic} description`" />
|
||||||
|
<Link rel="preload" href="/test.txt" as="script" />
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<button class="blue" @click="dynamic = Math.random() * 100">
|
||||||
|
Clickme
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useMeta } from '@nuxt/app'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
setup () {
|
||||||
|
useMeta({
|
||||||
|
bodyAttrs: {
|
||||||
|
class: 'test'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return { dynamic: ref(49) }
|
||||||
|
},
|
||||||
|
head: {
|
||||||
|
title: 'Another title'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
4
examples/meta/nuxt.config.ts
Normal file
4
examples/meta/nuxt.config.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { defineNuxtConfig } from '@nuxt/kit'
|
||||||
|
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
})
|
12
examples/meta/package.json
Normal file
12
examples/meta/package.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "example-meta",
|
||||||
|
"private": true,
|
||||||
|
"devDependencies": {
|
||||||
|
"nuxt3": "latest"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nu dev",
|
||||||
|
"build": "nu build",
|
||||||
|
"start": "node .output/server"
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,6 @@ export default defineBuildConfig({
|
|||||||
{ input: 'src/', name: 'app' }
|
{ input: 'src/', name: 'app' }
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
'@vueuse/head',
|
|
||||||
'ohmyfetch',
|
'ohmyfetch',
|
||||||
'vue-router',
|
'vue-router',
|
||||||
'vuex5'
|
'vuex5'
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
"prepack": "unbuild"
|
"prepack": "unbuild"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vueuse/head": "^0.6.0",
|
|
||||||
"hookable": "^4.4.1",
|
"hookable": "^4.4.1",
|
||||||
"ohmyfetch": "^0.2.0",
|
"ohmyfetch": "^0.2.0",
|
||||||
"upath": "^2.0.1",
|
"upath": "^2.0.1",
|
||||||
@ -28,6 +27,9 @@
|
|||||||
"vue-router": "^4.0.10",
|
"vue-router": "^4.0.10",
|
||||||
"vuex5": "^0.5.0-testing.3"
|
"vuex5": "^0.5.0-testing.3"
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nuxt/meta": "^0.1.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"unbuild": "^0.3.2"
|
"unbuild": "^0.3.2"
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,5 @@
|
|||||||
export * from './nuxt'
|
export * from './nuxt'
|
||||||
export * from './composables'
|
export * from './composables'
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
export * from '@nuxt/meta'
|
||||||
|
@ -3,6 +3,15 @@ import Hookable from 'hookable'
|
|||||||
import { defineGetter } from './utils'
|
import { defineGetter } from './utils'
|
||||||
import { legacyPlugin, LegacyContext } from './legacy'
|
import { legacyPlugin, LegacyContext } from './legacy'
|
||||||
|
|
||||||
|
type NuxtMeta = {
|
||||||
|
htmlAttrs?: string
|
||||||
|
headAttrs?: string
|
||||||
|
bodyAttrs?: string
|
||||||
|
headTags?: string
|
||||||
|
bodyPrepend?: string
|
||||||
|
bodyScripts?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface Nuxt {
|
export interface Nuxt {
|
||||||
app: App
|
app: App
|
||||||
globalName: string
|
globalName: string
|
||||||
@ -16,7 +25,9 @@ export interface Nuxt {
|
|||||||
_asyncDataPromises?: Record<string, Promise<any>>
|
_asyncDataPromises?: Record<string, Promise<any>>
|
||||||
_legacyContext?: LegacyContext
|
_legacyContext?: LegacyContext
|
||||||
|
|
||||||
ssrContext?: Record<string, any>
|
ssrContext?: Record<string, any> & {
|
||||||
|
renderMeta: () => Promise<NuxtMeta> | NuxtMeta
|
||||||
|
}
|
||||||
payload: {
|
payload: {
|
||||||
serverRendered?: true
|
serverRendered?: true
|
||||||
data?: Record<string, any>
|
data?: Record<string, any>
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
import { defineComponent } from '@vue/runtime-core'
|
|
||||||
import { useHead, HeadObject } from '@vueuse/head'
|
|
||||||
|
|
||||||
type MappedProps<T extends Record<string, any>> = {
|
|
||||||
[P in keyof T]: { type: () => T[P] }
|
|
||||||
}
|
|
||||||
|
|
||||||
const props: MappedProps<HeadObject> = {
|
|
||||||
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)
|
|
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
14
packages/meta/build.config.ts
Normal file
14
packages/meta/build.config.ts
Normal file
@ -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'
|
||||||
|
]
|
||||||
|
})
|
1
packages/meta/module.js
Normal file
1
packages/meta/module.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/module')
|
34
packages/meta/package.json
Normal file
34
packages/meta/package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
17
packages/meta/src/module.ts
Normal file
17
packages/meta/src/module.ts
Normal file
@ -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') })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
209
packages/meta/src/runtime/components.ts
Normal file
209
packages/meta/src/runtime/components.ts
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
import { defineComponent, SetupContext } from 'vue'
|
||||||
|
import { useMeta } from './index'
|
||||||
|
|
||||||
|
type Props = Readonly<Record<string, any>>
|
||||||
|
|
||||||
|
const removeUndefinedProps = (props: Props) =>
|
||||||
|
Object.fromEntries(Object.entries(props).filter(([_key, value]) => value !== undefined))
|
||||||
|
|
||||||
|
const setupForUseMeta = (metaFactory: (props: Props, ctx: SetupContext) => Record<string, any>, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// <script>
|
||||||
|
export const Script = defineComponent({
|
||||||
|
name: 'Script',
|
||||||
|
props: {
|
||||||
|
...globalProps,
|
||||||
|
async: Boolean,
|
||||||
|
crossorigin: {
|
||||||
|
type: [Boolean, String],
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
defer: Boolean,
|
||||||
|
integrity: String,
|
||||||
|
nomodule: Boolean,
|
||||||
|
nonce: String,
|
||||||
|
referrerpolicy: String,
|
||||||
|
src: String,
|
||||||
|
type: String,
|
||||||
|
/** @deprecated **/
|
||||||
|
charset: String,
|
||||||
|
/** @deprecated **/
|
||||||
|
language: String
|
||||||
|
},
|
||||||
|
setup: setupForUseMeta(script => ({
|
||||||
|
script: [script]
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// <link>
|
||||||
|
export const Link = defineComponent({
|
||||||
|
name: 'Link',
|
||||||
|
props: {
|
||||||
|
...globalProps,
|
||||||
|
as: String,
|
||||||
|
crossorigin: String,
|
||||||
|
disabled: Boolean,
|
||||||
|
href: String,
|
||||||
|
hreflang: String,
|
||||||
|
imagesizes: String,
|
||||||
|
imagesrcset: String,
|
||||||
|
integrity: String,
|
||||||
|
media: String,
|
||||||
|
prefetch: {
|
||||||
|
type: Boolean,
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
referrerpolicy: String,
|
||||||
|
rel: String,
|
||||||
|
sizes: String,
|
||||||
|
title: String,
|
||||||
|
type: String,
|
||||||
|
/** @deprecated **/
|
||||||
|
methods: String,
|
||||||
|
/** @deprecated **/
|
||||||
|
target: String
|
||||||
|
},
|
||||||
|
setup: setupForUseMeta(link => ({
|
||||||
|
link: [link]
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// <base>
|
||||||
|
export const Base = defineComponent({
|
||||||
|
name: 'Base',
|
||||||
|
props: {
|
||||||
|
...globalProps,
|
||||||
|
href: String,
|
||||||
|
target: String
|
||||||
|
},
|
||||||
|
setup: setupForUseMeta(base => ({
|
||||||
|
base
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// <title>
|
||||||
|
export const Title = defineComponent({
|
||||||
|
name: 'Title',
|
||||||
|
setup: setupForUseMeta((_, { slots }) => {
|
||||||
|
const title = slots.default()?.[0]?.children || null
|
||||||
|
if (process.dev && title && typeof title !== 'string') {
|
||||||
|
console.error('<Title> can only take a string in its default slot.')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// <meta>
|
||||||
|
export const Meta = defineComponent({
|
||||||
|
name: 'Meta',
|
||||||
|
props: {
|
||||||
|
...globalProps,
|
||||||
|
charset: String,
|
||||||
|
content: String,
|
||||||
|
httpEquiv: String,
|
||||||
|
name: String
|
||||||
|
},
|
||||||
|
setup: setupForUseMeta(meta => ({
|
||||||
|
meta: [meta]
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// <style>
|
||||||
|
export const Style = defineComponent({
|
||||||
|
name: 'Style',
|
||||||
|
props: {
|
||||||
|
...globalProps,
|
||||||
|
type: String,
|
||||||
|
media: String,
|
||||||
|
nonce: String,
|
||||||
|
title: String,
|
||||||
|
/** @deprecated **/
|
||||||
|
scoped: {
|
||||||
|
type: Boolean,
|
||||||
|
default: undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup: setupForUseMeta((props, { slots }) => {
|
||||||
|
const style = { ...props }
|
||||||
|
const textContent = slots.default?.()?.[0]?.children
|
||||||
|
if (textContent) {
|
||||||
|
if (process.dev && typeof textContent !== 'string') {
|
||||||
|
console.error('<Style> can only take a string in its default slot.')
|
||||||
|
}
|
||||||
|
style.content = textContent
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
style: [style]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// <head>
|
||||||
|
export const Head = defineComponent({
|
||||||
|
name: 'Head',
|
||||||
|
props: {
|
||||||
|
...globalProps,
|
||||||
|
manifest: String,
|
||||||
|
version: String,
|
||||||
|
xmlns: String
|
||||||
|
},
|
||||||
|
setup: setupForUseMeta(headAttrs => ({ headAttrs }), true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// <body>
|
||||||
|
export const Body = defineComponent({
|
||||||
|
name: 'Body',
|
||||||
|
props: globalProps,
|
||||||
|
setup: setupForUseMeta(bodyAttrs => ({ bodyAttrs }), true)
|
||||||
|
})
|
25
packages/meta/src/runtime/composables.ts
Normal file
25
packages/meta/src/runtime/composables.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// import { useMeta as useVueMeta } from 'vue-meta'
|
||||||
|
import { isFunction } from '@vue/shared'
|
||||||
|
import { computed, ComputedGetter } from '@vue/reactivity'
|
||||||
|
import { useHead } from '@vueuse/head'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You can pass in a meta object, which has keys corresponding to meta tags:
|
||||||
|
* `title`, `base`, `script`, `style`, `meta` and `link`, as well as `htmlAttrs` and `bodyAttrs`.
|
||||||
|
*
|
||||||
|
* Alternatively, for reactive meta state, you can pass in a function
|
||||||
|
* that returns a meta object.
|
||||||
|
*/
|
||||||
|
export function useMeta (meta: Record<string, any> | ComputedGetter<any>) {
|
||||||
|
// TODO: refine @nuxt/meta API
|
||||||
|
|
||||||
|
// At the moment we force all interaction to happen through passing in
|
||||||
|
// the meta object or function that returns a meta object.
|
||||||
|
const source = isFunction(meta) ? computed(meta) : meta
|
||||||
|
|
||||||
|
// `vue-meta`
|
||||||
|
// useVueMeta(source)
|
||||||
|
|
||||||
|
// `@vueuse/head`
|
||||||
|
useHead(source)
|
||||||
|
}
|
1
packages/meta/src/runtime/index.ts
Normal file
1
packages/meta/src/runtime/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './composables'
|
19
packages/meta/src/runtime/meta.ts
Normal file
19
packages/meta/src/runtime/meta.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { getCurrentInstance } from 'vue'
|
||||||
|
import { defineNuxtPlugin } from '@nuxt/app'
|
||||||
|
import * as Components from './components'
|
||||||
|
import { useMeta } from './index'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxt) => {
|
||||||
|
nuxt.app.mixin({
|
||||||
|
created () {
|
||||||
|
const instance = getCurrentInstance()
|
||||||
|
if (!instance?.type || !('head' in instance.type)) { return }
|
||||||
|
|
||||||
|
useMeta((instance.type as any).head)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const name in Components) {
|
||||||
|
nuxt.app.component(name, Components[name])
|
||||||
|
}
|
||||||
|
})
|
35
packages/meta/src/runtime/vue-meta.ts
Normal file
35
packages/meta/src/runtime/vue-meta.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { defineNuxtPlugin } from '@nuxt/app'
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createMetaManager } from 'vue-meta'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxt) => {
|
||||||
|
const manager = createMetaManager(process.server)
|
||||||
|
|
||||||
|
nuxt.app.use(manager)
|
||||||
|
|
||||||
|
if (process.client) {
|
||||||
|
const teleportTarget = document.createElement('div')
|
||||||
|
teleportTarget.id = 'head-target'
|
||||||
|
document.body.appendChild(teleportTarget)
|
||||||
|
|
||||||
|
createApp({ render: () => manager.render({}) }).mount('#head-target')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.server) {
|
||||||
|
nuxt.ssrContext.renderMeta = async () => {
|
||||||
|
const { renderMetaToString } = await import('vue-meta/ssr')
|
||||||
|
nuxt.ssrContext.teleports = nuxt.ssrContext.teleports || {}
|
||||||
|
|
||||||
|
await renderMetaToString(nuxt.app, nuxt.ssrContext)
|
||||||
|
|
||||||
|
return {
|
||||||
|
htmlAttrs: nuxt.ssrContext.teleports.htmlAttrs || '',
|
||||||
|
headAttrs: nuxt.ssrContext.teleports.headAttrs || '',
|
||||||
|
bodyAttrs: nuxt.ssrContext.teleports.bodyAttrs || '',
|
||||||
|
headTags: nuxt.ssrContext.teleports.head || '',
|
||||||
|
bodyPrepend: nuxt.ssrContext.teleports['body-prepend'] || '',
|
||||||
|
bodyScripts: nuxt.ssrContext.teleports.body || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
12
packages/meta/src/runtime/vueuse-head.ts
Normal file
12
packages/meta/src/runtime/vueuse-head.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { createHead, renderHeadToString } from '@vueuse/head'
|
||||||
|
import { defineNuxtPlugin } from '@nuxt/app'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxt) => {
|
||||||
|
const head = createHead()
|
||||||
|
|
||||||
|
nuxt.app.use(head)
|
||||||
|
|
||||||
|
if (process.server) {
|
||||||
|
nuxt.ssrContext.renderMeta = () => renderHeadToString(head)
|
||||||
|
}
|
||||||
|
})
|
@ -59,6 +59,12 @@ export default function nuxt2CompatModule () {
|
|||||||
src: resolve(nitroContext._internal.runtimeDir, 'app/nitro.client.mjs')
|
src: resolve(nitroContext._internal.runtimeDir, 'app/nitro.client.mjs')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Nitro server plugin (for vue-meta)
|
||||||
|
this.addPlugin({
|
||||||
|
fileName: 'nitro-compat.server.js',
|
||||||
|
src: resolve(nitroContext._internal.runtimeDir, 'app/nitro-compat.server.js')
|
||||||
|
})
|
||||||
|
|
||||||
// Fix module resolution
|
// Fix module resolution
|
||||||
nuxt.hook('webpack:config', (configs) => {
|
nuxt.hook('webpack:config', (configs) => {
|
||||||
for (const config of configs) {
|
for (const config of configs) {
|
||||||
|
27
packages/nitro/src/runtime/app/nitro-compat.server.js
Normal file
27
packages/nitro/src/runtime/app/nitro-compat.server.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
export default ({ ssrContext }) => {
|
||||||
|
ssrContext.renderMeta = () => {
|
||||||
|
const meta = ssrContext.meta.inject({
|
||||||
|
isSSR: ssrContext.nuxt.serverRendered,
|
||||||
|
ln: process.env.NODE_ENV === 'development'
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
htmlAttrs: meta.htmlAttrs.text(),
|
||||||
|
headAttrs: meta.headAttrs.text(),
|
||||||
|
headTags:
|
||||||
|
meta.title.text() + meta.base.text() +
|
||||||
|
meta.meta.text() + meta.link.text() +
|
||||||
|
meta.style.text() + meta.script.text() +
|
||||||
|
meta.noscript.text(),
|
||||||
|
bodyAttrs: meta.bodyAttrs.text(),
|
||||||
|
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 }),
|
||||||
|
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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -68,7 +68,7 @@ export async function renderMiddleware (req, res) {
|
|||||||
data = renderPayload(payload, url)
|
data = renderPayload(payload, url)
|
||||||
res.setHeader('Content-Type', 'text/javascript;charset=UTF-8')
|
res.setHeader('Content-Type', 'text/javascript;charset=UTF-8')
|
||||||
} else {
|
} else {
|
||||||
data = renderHTML(payload, rendered, ssrContext)
|
data = await renderHTML(payload, rendered, ssrContext)
|
||||||
res.setHeader('Content-Type', 'text/html;charset=UTF-8')
|
res.setHeader('Content-Type', 'text/html;charset=UTF-8')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,56 +77,30 @@ export async function renderMiddleware (req, res) {
|
|||||||
res.end(data, 'utf-8')
|
res.end(data, 'utf-8')
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderHTML (payload, rendered, ssrContext) {
|
async function renderHTML (payload, rendered, ssrContext) {
|
||||||
const state = `<script>window.__NUXT__=${devalue(payload)}</script>`
|
const state = `<script>window.__NUXT__=${devalue(payload)}</script>`
|
||||||
const _html = rendered.html
|
const html = rendered.html
|
||||||
|
|
||||||
const meta = {
|
if ('renderMeta' in ssrContext) {
|
||||||
htmlAttrs: '',
|
rendered.meta = await ssrContext.renderMeta()
|
||||||
bodyAttrs: '',
|
|
||||||
headAttrs: '',
|
|
||||||
headTags: '',
|
|
||||||
bodyTags: '',
|
|
||||||
bodyScriptsPrepend: '',
|
|
||||||
bodyScripts: ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @vueuse/head
|
const {
|
||||||
if (typeof ssrContext.head === 'function') {
|
htmlAttrs = '',
|
||||||
Object.assign(meta, ssrContext.head())
|
bodyAttrs = '',
|
||||||
}
|
headAttrs = '',
|
||||||
|
headTags = '',
|
||||||
// vue-meta
|
bodyScriptsPrepend = '',
|
||||||
if (ssrContext.meta && typeof ssrContext.meta.inject === 'function') {
|
bodyScripts = ''
|
||||||
const _meta = ssrContext.meta.inject({
|
} = rendered.meta || {}
|
||||||
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 })
|
|
||||||
}
|
|
||||||
|
|
||||||
return htmlTemplate({
|
return htmlTemplate({
|
||||||
HTML_ATTRS: meta.htmlAttrs,
|
HTML_ATTRS: htmlAttrs,
|
||||||
HEAD_ATTRS: meta.headAttrs,
|
HEAD_ATTRS: headAttrs,
|
||||||
HEAD: meta.headTags +
|
HEAD: headTags +
|
||||||
rendered.renderResourceHints() + rendered.renderStyles() + (ssrContext.styles || ''),
|
rendered.renderResourceHints() + rendered.renderStyles() + (ssrContext.styles || ''),
|
||||||
BODY_ATTRS: meta.bodyAttrs,
|
BODY_ATTRS: bodyAttrs,
|
||||||
APP: meta.bodyScriptsPrepend + _html + state + rendered.renderScripts() + meta.bodyScripts
|
APP: bodyScriptsPrepend + html + state + rendered.renderScripts() + bodyScripts
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,11 @@
|
|||||||
"repository": "nuxt/framework",
|
"repository": "nuxt/framework",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
"bin": {
|
||||||
|
"nu": "./bin/nuxt.js",
|
||||||
|
"nuxt": "./bin/nuxt.js",
|
||||||
|
"nuxt-cli": "./bin/nuxt.js"
|
||||||
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"bin",
|
"bin",
|
||||||
"dist"
|
"dist"
|
||||||
@ -11,15 +16,11 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"prepack": "unbuild $@ || true"
|
"prepack": "unbuild $@ || true"
|
||||||
},
|
},
|
||||||
"bin": {
|
|
||||||
"nu": "./bin/nuxt.js",
|
|
||||||
"nuxt": "./bin/nuxt.js",
|
|
||||||
"nuxt-cli": "./bin/nuxt.js"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/app": "^0.5.0",
|
"@nuxt/app": "^0.5.0",
|
||||||
"@nuxt/component-discovery": "^0.2.0",
|
"@nuxt/component-discovery": "^0.2.0",
|
||||||
"@nuxt/kit": "^0.6.4",
|
"@nuxt/kit": "^0.6.4",
|
||||||
|
"@nuxt/meta": "^0.1.0",
|
||||||
"@nuxt/nitro": "^0.9.1",
|
"@nuxt/nitro": "^0.9.1",
|
||||||
"@nuxt/pages": "^0.3.0",
|
"@nuxt/pages": "^0.3.0",
|
||||||
"@nuxt/vite-builder": "^0.5.0",
|
"@nuxt/vite-builder": "^0.5.0",
|
||||||
|
@ -56,6 +56,7 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
|
|||||||
options._majorVersion = 3
|
options._majorVersion = 3
|
||||||
options.alias.vue = normalize(require.resolve('vue/dist/vue.esm-bundler.js'))
|
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/pages/module')))
|
||||||
|
options.buildModules.push(normalize(require.resolve('@nuxt/meta/module')))
|
||||||
options.buildModules.push(normalize(require.resolve('@nuxt/component-discovery/module')))
|
options.buildModules.push(normalize(require.resolve('@nuxt/component-discovery/module')))
|
||||||
|
|
||||||
const nuxt = createNuxt(options)
|
const nuxt = createNuxt(options)
|
||||||
|
39
yarn.lock
39
yarn.lock
@ -1279,7 +1279,6 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@nuxt/app@workspace:packages/app"
|
resolution: "@nuxt/app@workspace:packages/app"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vueuse/head": ^0.6.0
|
|
||||||
hookable: ^4.4.1
|
hookable: ^4.4.1
|
||||||
ohmyfetch: ^0.2.0
|
ohmyfetch: ^0.2.0
|
||||||
unbuild: ^0.3.2
|
unbuild: ^0.3.2
|
||||||
@ -1287,6 +1286,8 @@ __metadata:
|
|||||||
vue: ^3.1.4
|
vue: ^3.1.4
|
||||||
vue-router: ^4.0.10
|
vue-router: ^4.0.10
|
||||||
vuex5: ^0.5.0-testing.3
|
vuex5: ^0.5.0-testing.3
|
||||||
|
peerDependencies:
|
||||||
|
"@nuxt/meta": ^0.1.0
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
@ -1355,6 +1356,22 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
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":
|
"@nuxt/nitro@^0.9.1, @nuxt/nitro@workspace:packages/nitro":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@nuxt/nitro@workspace:packages/nitro"
|
resolution: "@nuxt/nitro@workspace:packages/nitro"
|
||||||
@ -5416,6 +5433,14 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
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":
|
"example-pages@workspace:examples/pages":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "example-pages@workspace:examples/pages"
|
resolution: "example-pages@workspace:examples/pages"
|
||||||
@ -8621,6 +8646,7 @@ fsevents@~2.3.2:
|
|||||||
"@nuxt/app": ^0.5.0
|
"@nuxt/app": ^0.5.0
|
||||||
"@nuxt/component-discovery": ^0.2.0
|
"@nuxt/component-discovery": ^0.2.0
|
||||||
"@nuxt/kit": ^0.6.4
|
"@nuxt/kit": ^0.6.4
|
||||||
|
"@nuxt/meta": ^0.1.0
|
||||||
"@nuxt/nitro": ^0.9.1
|
"@nuxt/nitro": ^0.9.1
|
||||||
"@nuxt/pages": ^0.3.0
|
"@nuxt/pages": ^0.3.0
|
||||||
"@nuxt/vite-builder": ^0.5.0
|
"@nuxt/vite-builder": ^0.5.0
|
||||||
@ -11667,7 +11693,7 @@ fsevents@~2.3.2:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"unbuild@npm:^0.3.2":
|
"unbuild@npm:^0.3.1, unbuild@npm:^0.3.2":
|
||||||
version: 0.3.2
|
version: 0.3.2
|
||||||
resolution: "unbuild@npm:0.3.2"
|
resolution: "unbuild@npm:0.3.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -11987,6 +12013,15 @@ fsevents@~2.3.2:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"vue-router@npm:^4.0.10":
|
||||||
version: 4.0.10
|
version: 4.0.10
|
||||||
resolution: "vue-router@npm:4.0.10"
|
resolution: "vue-router@npm:4.0.10"
|
||||||
|
Loading…
Reference in New Issue
Block a user