mirror of
https://github.com/tenrok/vue-meta.git
synced 2026-06-24 13:20:33 +03:00
feat: add support for attributes (wip)
support slots for head/body make config fully external (while providing defaults users can use) type fixes
This commit is contained in:
+44
-15
@@ -9,7 +9,7 @@ import {
|
|||||||
watch
|
watch
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import { createManager, useMeta, useMetainfo } from 'vue-meta'
|
import { defaultConfig, createManager, useMeta, useMetainfo } from 'vue-meta'
|
||||||
// import About from './about.vue'
|
// import About from './about.vue'
|
||||||
|
|
||||||
const metaUpdated = 'no'
|
const metaUpdated = 'no'
|
||||||
@@ -76,22 +76,18 @@ const App = {
|
|||||||
title: 'Twitter Title'
|
title: 'Twitter Title'
|
||||||
},
|
},
|
||||||
noscript: [
|
noscript: [
|
||||||
//'<!-- // A code comment -->',
|
|
||||||
{ tag: 'link', rel: 'stylesheet', href: 'style.css' }
|
{ tag: 'link', rel: 'stylesheet', href: 'style.css' }
|
||||||
],
|
],
|
||||||
otherNoscript: {
|
otherNoscript: {
|
||||||
tag: 'noscript',
|
tag: 'noscript',
|
||||||
'data-test': 'hello',
|
'data-test': 'hello',
|
||||||
children: [
|
children: [
|
||||||
//'<!-- // Another code comment -->',
|
|
||||||
{ tag: 'link', rel: 'stylesheet', href: 'style2.css' }
|
{ tag: 'link', rel: 'stylesheet', href: 'style2.css' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
body: 'body-script1.js',
|
body: 'body-script1.js',
|
||||||
script: [
|
script: [
|
||||||
//'<!--[if IE]>',
|
|
||||||
{ src: 'head-script1.js' },
|
{ src: 'head-script1.js' },
|
||||||
//'<![endif]-->',
|
|
||||||
{ src: 'body-script2.js', to: 'body' },
|
{ src: 'body-script2.js', to: 'body' },
|
||||||
{ src: 'body-script3.js', to: '#put-it-here' }
|
{ src: 'body-script3.js', to: '#put-it-here' }
|
||||||
],
|
],
|
||||||
@@ -120,23 +116,58 @@ const App = {
|
|||||||
|
|
||||||
const metadata = useMetainfo()
|
const metadata = useMetainfo()
|
||||||
|
|
||||||
window.$metainfo = metadata
|
window.$metadata = metadata
|
||||||
|
|
||||||
watch(metadata, (newValue, oldValue) => {
|
watch(metadata, (newValue, oldValue) => {
|
||||||
console.log('UPDATE', newValue)
|
console.log('UPDATE', newValue)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/* let i = 0
|
||||||
|
const walk = (data, path = []) => {
|
||||||
|
for (const key in data) {
|
||||||
|
const newPath = [...path, key]
|
||||||
|
if (typeof data[key] === 'object') {
|
||||||
|
walk(data[key], newPath)
|
||||||
|
} else {
|
||||||
|
console.log(newPath.join('.'))
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
if (i > 50) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTimeout(() => walk(metadata), 1000) */
|
||||||
|
|
||||||
return {
|
return {
|
||||||
metadata
|
metadata
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<metainfo :metainfo="metadata">
|
<metainfo :metainfo="metadata" :body-class="'theme-dark'" :html-lang="['en','nl']" html-amp>
|
||||||
<template v-slot:base="{ content, metainfo }">http://nuxt.dev:3000{{ content }}</template>
|
<template v-slot:base="{ content, metainfo }">http://nuxt.dev:3000{{ content }}</template>
|
||||||
<template v-slot:title="{ content, metainfo }">{{ content }} - {{ metainfo.description }} - Hello</template>
|
<template v-slot:title="{ content, metainfo }">{{ content }} - {{ metainfo.description }} - Hello</template>
|
||||||
<template v-slot:og(title)="{ content, metainfo, og }">
|
<template v-slot:og(title)="{ content, metainfo, og }">
|
||||||
{{ content }} - {{ og.description }} - {{ metainfo.description }} - Hello Again
|
{{ content }} - {{ og.description }} - {{ metainfo.description }} - Hello Again
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script src="lalala1.js"></script>
|
||||||
|
<script src="lalala2.js"></script>
|
||||||
|
|
||||||
|
<template v-slot:head="{ metainfo }">
|
||||||
|
<!--[if IE]>
|
||||||
|
// -> Reactivity is not supported by Vue, all comments are ignored
|
||||||
|
<script :src="metainfo.script[0].src" ></script>
|
||||||
|
// -> but a static file should work
|
||||||
|
<script src="lalala3.js" ></script>
|
||||||
|
// -> altho Vue probably strips comments in production builds (but can be configged afaik)
|
||||||
|
<![endif]-->
|
||||||
|
<script :src="metainfo.script[0].src" ></script>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:body>
|
||||||
|
<script src="lalala4.js"></script>
|
||||||
|
</template>
|
||||||
</metainfo>
|
</metainfo>
|
||||||
|
|
||||||
<div id="app">
|
<div id="app">
|
||||||
@@ -172,15 +203,13 @@ function decisionMaker5000000 (key, pathSegments, getOptions, getCurrentValue) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const metaManager = createManager({
|
const metaManager = createManager({
|
||||||
resolver: decisionMaker5000000,
|
...defaultConfig,
|
||||||
config: {
|
esi: {
|
||||||
esi: {
|
group: true,
|
||||||
group: true,
|
namespaced: true,
|
||||||
namespaced: true,
|
attributes: ['src', 'test', 'text']
|
||||||
attributes: ['src', 'test', 'text']
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}, decisionMaker5000000)
|
||||||
|
|
||||||
/* useMeta(
|
/* useMeta(
|
||||||
{
|
{
|
||||||
|
|||||||
+48
-8
@@ -1,5 +1,5 @@
|
|||||||
import { h, defineComponent, Teleport, PropType, VNode, VNodeProps } from 'vue'
|
import { h, watchEffect, defineComponent, Teleport, PropType, VNode, VNodeProps } from 'vue'
|
||||||
import { isArray } from '@vue/shared'
|
import { isArray, isFunction } from '@vue/shared'
|
||||||
import { renderMeta } from './render'
|
import { renderMeta } from './render'
|
||||||
import { getCurrentManager } from './useApi'
|
import { getCurrentManager } from './useApi'
|
||||||
import { MetainfoActive } from './types'
|
import { MetainfoActive } from './types'
|
||||||
@@ -8,23 +8,59 @@ export interface MetainfoProps {
|
|||||||
metainfo: MetainfoActive
|
metainfo: MetainfoActive
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addVnode (teleports: any, to: string, vnode: VNode) {
|
export function addVnode (teleports: any, to: string, vnode: VNode | Array<VNode>) {
|
||||||
if (!teleports[to]) {
|
if (!teleports[to]) {
|
||||||
teleports[to] = []
|
teleports[to] = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isArray(vnode)) {
|
||||||
|
teleports[to].push(...vnode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
teleports[to].push(vnode)
|
teleports[to].push(vnode)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MetainfoImpl = defineComponent({
|
export const MetainfoImpl = defineComponent({
|
||||||
name: 'Metainfo',
|
name: 'Metainfo',
|
||||||
|
inheritAttrs: false,
|
||||||
props: {
|
props: {
|
||||||
metainfo: {
|
metainfo: {
|
||||||
type: Object as PropType<MetainfoActive>,
|
type: Object as PropType<MetainfoActive>,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setup ({ metainfo }, { slots }) {
|
setup ({ metainfo }, { attrs, slots }) {
|
||||||
|
const tags: { [key: string]: Element } = {}
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
const attributes = Object.keys(attrs)
|
||||||
|
|
||||||
|
for (const tagName of ['html', 'head', 'body']) {
|
||||||
|
const tagAttrs = attributes.filter(attr => attr.startsWith(tagName + '-'))
|
||||||
|
|
||||||
|
if (!tagAttrs.length) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tags[tagName]) {
|
||||||
|
const foundTag = document.querySelector(tagName)
|
||||||
|
|
||||||
|
if (foundTag) {
|
||||||
|
tags[tagName] = foundTag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tag: Element = tags[tagName]
|
||||||
|
|
||||||
|
for (const tagAttr of tagAttrs) {
|
||||||
|
const attr: string = tagAttr.slice(5)
|
||||||
|
|
||||||
|
tag.setAttribute(attr, `${attrs[tagAttr] || ''}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
const teleports: any = {}
|
const teleports: any = {}
|
||||||
|
|
||||||
@@ -39,24 +75,28 @@ export const MetainfoImpl = defineComponent({
|
|||||||
metainfo[key],
|
metainfo[key],
|
||||||
config
|
config
|
||||||
)
|
)
|
||||||
console.log('RENDERED VNODES', vnodes)
|
|
||||||
const defaultTo =
|
const defaultTo =
|
||||||
(key !== 'base' && metainfo[key].to) || config.to || 'head'
|
(key !== 'base' && metainfo[key].to) || config.to || 'head'
|
||||||
|
|
||||||
if (isArray(vnodes)) {
|
if (isArray(vnodes)) {
|
||||||
for (const { to, vnode } of vnodes) {
|
for (const { to, vnode } of vnodes) {
|
||||||
console.log('VNODE 1', vnode)
|
|
||||||
addVnode(teleports, to || defaultTo, vnode)
|
addVnode(teleports, to || defaultTo, vnode)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const { to, vnode } = vnodes
|
const { to, vnode } = vnodes
|
||||||
console.log('VNODE 2', vnode)
|
|
||||||
addVnode(teleports, to || defaultTo, vnode)
|
addVnode(teleports, to || defaultTo, vnode)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('TARGETS', teleports)
|
for (const tag of ['default', 'head', 'body']) {
|
||||||
|
const slotFn = slots[tag]
|
||||||
|
if (isFunction(slotFn)) {
|
||||||
|
addVnode(teleports, tag === 'default' ? 'head' : tag, slotFn({ metainfo }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Object.keys(teleports).map((to) => {
|
return Object.keys(teleports).map((to) => {
|
||||||
return h(Teleport, { to }, teleports[to])
|
return h(Teleport, { to }, teleports[to])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
import { isArray } from '@vue/shared'
|
|
||||||
import { tags } from './config/tags'
|
|
||||||
import { TODO } from './types'
|
|
||||||
|
|
||||||
export interface ConfigOption {
|
|
||||||
tag?: string
|
|
||||||
to?: string
|
|
||||||
group?: boolean
|
|
||||||
keyAttribute?: string
|
|
||||||
valueAttribute?: string
|
|
||||||
nameless?: boolean
|
|
||||||
namespaced?: boolean
|
|
||||||
namespacedAttribute?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultMapping: { [key: string]: ConfigOption } = {
|
|
||||||
body: {
|
|
||||||
tag: 'script',
|
|
||||||
to: 'body'
|
|
||||||
},
|
|
||||||
base: {
|
|
||||||
valueAttribute: 'href'
|
|
||||||
},
|
|
||||||
charset: {
|
|
||||||
tag: 'meta',
|
|
||||||
nameless: true,
|
|
||||||
valueAttribute: 'charset'
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
tag: 'meta'
|
|
||||||
},
|
|
||||||
og: {
|
|
||||||
group: true,
|
|
||||||
namespacedAttribute: true,
|
|
||||||
tag: 'meta',
|
|
||||||
keyAttribute: 'property'
|
|
||||||
},
|
|
||||||
twitter: {
|
|
||||||
group: true,
|
|
||||||
namespacedAttribute: true,
|
|
||||||
tag: 'meta'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { defaultMapping }
|
|
||||||
|
|
||||||
export function hasConfig (name: string): boolean {
|
|
||||||
return !!tags[name] || !!defaultMapping[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getConfigKey (
|
|
||||||
tagOrName: string | Array<string>,
|
|
||||||
key: string,
|
|
||||||
config: TODO
|
|
||||||
): any {
|
|
||||||
if (config && key in config) {
|
|
||||||
return config[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isArray(tagOrName)) {
|
|
||||||
for (const name of tagOrName) {
|
|
||||||
if (name && name in tags) {
|
|
||||||
return tags[name][key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tagOrName in tags) {
|
|
||||||
const tag = tags[tagOrName]
|
|
||||||
return tag[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
export interface ConfigOption {
|
||||||
|
tag?: string
|
||||||
|
to?: string
|
||||||
|
group?: boolean
|
||||||
|
keyAttribute?: string
|
||||||
|
valueAttribute?: string
|
||||||
|
nameless?: boolean
|
||||||
|
namespaced?: boolean
|
||||||
|
namespacedAttribute?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
[key: string]: ConfigOption
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultConfig: Config = {
|
||||||
|
body: {
|
||||||
|
tag: 'script',
|
||||||
|
to: 'body'
|
||||||
|
},
|
||||||
|
base: {
|
||||||
|
valueAttribute: 'href'
|
||||||
|
},
|
||||||
|
charset: {
|
||||||
|
tag: 'meta',
|
||||||
|
nameless: true,
|
||||||
|
valueAttribute: 'charset'
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
tag: 'meta'
|
||||||
|
},
|
||||||
|
og: {
|
||||||
|
group: true,
|
||||||
|
namespacedAttribute: true,
|
||||||
|
tag: 'meta',
|
||||||
|
keyAttribute: 'property'
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
group: true,
|
||||||
|
namespacedAttribute: true,
|
||||||
|
tag: 'meta'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { defaultConfig }
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { isArray } from '@vue/shared'
|
||||||
|
import { Config } from './default'
|
||||||
|
import { tags } from './tags'
|
||||||
|
|
||||||
|
export function hasConfig (name: string, config: Config): boolean {
|
||||||
|
return !!config[name] || !!tags[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConfigKey (
|
||||||
|
tagOrName: string | Array<string>,
|
||||||
|
key: string,
|
||||||
|
config: Config
|
||||||
|
): any {
|
||||||
|
if (config && key in config) {
|
||||||
|
return config[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArray(tagOrName)) {
|
||||||
|
for (const name of tagOrName) {
|
||||||
|
if (name && name in tags) {
|
||||||
|
return tags[name][key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagOrName in tags) {
|
||||||
|
const tag = tags[tagOrName]
|
||||||
|
return tag[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './default'
|
||||||
|
export * from './helpers'
|
||||||
|
export * from './tags'
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
|
export { defaultConfig } from './config'
|
||||||
export { createManager } from './manager'
|
export { createManager } from './manager'
|
||||||
export * from './useApi'
|
export * from './useApi'
|
||||||
|
|||||||
+7
-17
@@ -1,30 +1,23 @@
|
|||||||
import { App } from 'vue'
|
import { App } from 'vue'
|
||||||
import { isFunction } from '@vue/shared'
|
import { isFunction } from '@vue/shared'
|
||||||
import { defaultMapping } from './config'
|
import { Config } from './config'
|
||||||
import { applyMetaPlugin } from './install'
|
import { applyMetaPlugin } from './install'
|
||||||
import * as deepestResolver from './resolvers/deepest'
|
import * as deepestResolver from './resolvers/deepest'
|
||||||
import { TODO, ActiveResolverObject, MetaContext, PathSegments } from './types'
|
import { ManagerResolverObject, ActiveResolverObject, MetaContext, PathSegments } from './types'
|
||||||
|
|
||||||
export type ManagerOptions = {
|
|
||||||
install(app: App): void
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Manager = {
|
export type Manager = {
|
||||||
readonly config: TODO
|
readonly config: Config
|
||||||
|
|
||||||
resolver: ActiveResolverObject
|
resolver: ManagerResolverObject
|
||||||
install(app: App): void
|
install(app: App): void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createManager (options: TODO = {}): Manager {
|
export function createManager (config: Config, resolver: ActiveResolverObject = deepestResolver): Manager {
|
||||||
const { resolver = deepestResolver } = options
|
|
||||||
|
|
||||||
// TODO: validate resolver
|
// TODO: validate resolver
|
||||||
|
|
||||||
const manager: Manager = {
|
const manager: Manager = {
|
||||||
resolver: {
|
resolver: {
|
||||||
setup (context: MetaContext) {
|
setup (context: MetaContext) {
|
||||||
if (!resolver || isFunction(resolver)) {
|
if (!resolver || !resolver.setup || isFunction(resolver)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,10 +35,7 @@ export function createManager (options: TODO = {}): Manager {
|
|||||||
return resolver.resolve(key, pathSegments, getShadow, getActive)
|
return resolver.resolve(key, pathSegments, getShadow, getActive)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
config: {
|
config,
|
||||||
...defaultMapping,
|
|
||||||
...options.config
|
|
||||||
},
|
|
||||||
install (app: App) {
|
install (app: App) {
|
||||||
applyMetaPlugin(app, this)
|
applyMetaPlugin(app, this)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ export interface ActiveResolverObject {
|
|||||||
resolve: ActiveResolverMethod
|
resolve: ActiveResolverMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ManagerResolverObject {
|
||||||
|
setup: ActiveResolverSetup
|
||||||
|
resolve: ActiveResolverMethod
|
||||||
|
}
|
||||||
|
|
||||||
export interface ShadowNode {
|
export interface ShadowNode {
|
||||||
[key: string]: TODO
|
[key: string]: TODO
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-4
@@ -10,7 +10,6 @@ let contextCounter: number = 0
|
|||||||
export function useMeta (obj: MetainfoInput, manager?: Manager) {
|
export function useMeta (obj: MetainfoInput, manager?: Manager) {
|
||||||
const vm = getCurrentInstance()
|
const vm = getCurrentInstance()
|
||||||
if (vm) {
|
if (vm) {
|
||||||
console.log(vm)
|
|
||||||
manager = vm.appContext.config.globalProperties.$metaManager
|
manager = vm.appContext.config.globalProperties.$metaManager
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,9 +29,7 @@ export function useMeta (obj: MetainfoInput, manager?: Manager) {
|
|||||||
onUnmounted(unmount)
|
onUnmounted(unmount)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (manager.resolver.setup) {
|
manager.resolver.setup(context)
|
||||||
manager.resolver.setup(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
setByObject(context, obj)
|
setByObject(context, obj)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user