2
0
mirror of https://github.com/tenrok/vue-meta.git synced 2026-06-18 04:00:34 +03:00

Merge branch 'next'

This commit is contained in:
pimlie
2019-04-20 12:49:06 +02:00
129 changed files with 11261 additions and 5145 deletions
-8
View File
@@ -1,8 +0,0 @@
{
"presets": ["@babel/preset-env"],
"env": {
"test": {
"plugins": ["istanbul"]
}
}
}
+111 -20
View File
@@ -1,38 +1,129 @@
version: 2
jobs:
build:
docker:
- image: circleci/node
steps:
# Checkout repository
- checkout
version: 2.1
# Restore cache
executors:
node:
parameters:
browsers:
type: boolean
default: false
docker:
- image: circleci/node:latest<<# parameters.browsers >>-browsers<</ parameters.browsers >>
working_directory: ~/project
environment:
NODE_ENV: test
commands:
attach-project:
steps:
- checkout
- attach_workspace:
at: ~/project
jobs:
setup:
executor: node
steps:
- checkout
- restore_cache:
key: yarn-{{ checksum "yarn.lock" }}
# Install dependencies
- run:
name: Install Dependencies
command: NODE_ENV=dev yarn
# Keep cache
- save_cache:
key: yarn-{{ checksum "yarn.lock" }}
paths:
- "node_modules"
- persist_to_workspace:
root: ~/project
paths:
- node_modules
# Lint
lint:
executor: node
steps:
- attach-project
- run:
name: Lint
command: yarn lint
# Test
audit:
executor: node
steps:
- attach-project
- run:
name: Test
command: yarn test
name: Security Audit
command: yarn audit
# Coverage
test-unit:
executor: node
steps:
- attach-project
- run:
name: Coverage
command: yarn codecov
name: Unit Tests
command: yarn test:unit --coverage && yarn coverage
test-e2e-ssr:
executor: node
steps:
- attach-project
- run:
name: E2E SSR Tests
command: yarn test:e2e-ssr
- persist_to_workspace:
root: ~/project
paths:
- test/fixtures
test-e2e-browser:
parameters:
browserString:
type: string
executor:
name: node
browsers: true
steps:
- attach-project
- run:
name: E2E Browser Tests
command: yarn test:e2e-browser
environment:
BROWSER_STRING: << parameters.browserString >>
workflows:
version : 2
commit:
jobs:
- setup
- lint: { requires: [setup] }
- audit: { requires: [setup] }
- test-unit: { requires: [lint] }
- test-e2e-ssr: { requires: [lint] }
- test-e2e-browser:
name: test-e2e-firefox
browserString: firefox/headless
requires: [test-e2e-ssr]
- test-e2e-browser:
name: test-e2e-chrome
browserString: chrome/selenium
requires: [test-e2e-ssr]
- test-e2e-browser:
name: test-e2e-ie
browserString: browserstack/local/windows 7/ie:9
requires: [test-e2e-ssr]
filters:
branches: { ignore: /^pull\/.*/ }
- test-e2e-browser:
name: test-e2e-edge
browserString: browserstack/local/edge:15
requires: [test-e2e-ssr]
filters:
branches: { ignore: /^pull\/.*/ }
- test-e2e-browser:
name: test-e2e-safari
browserString: browserstack/local/os x=snow leopard/safari:5.1
requires: [test-e2e-ssr]
filters:
branches: { ignore: /^pull\/.*/ }
+13
View File
@@ -0,0 +1,13 @@
{
"root": true,
"parserOptions": {
"parser": "babel-eslint",
"sourceType": "module"
},
"extends": [
"@nuxtjs"
],
"globals": {
"Vue": "readable"
}
}
+10
View File
@@ -1,6 +1,7 @@
# Logs
logs
*.log
*.err
npm-debug.log*
# Runtime data
@@ -32,9 +33,18 @@ jspm_packages
# Optional npm cache directory
.npm
package-lock.json
# Optional REPL history
.node_repl_history
# built code
lib
es
.vue-meta
# examples yarn lock
examples/yarn.lock
# env vars
.env*
+4 -1
View File
@@ -1,6 +1,9 @@
MIT License
Copyright (c) 2016-2018 Declan de Wet & Sébastien Chopin
Copyright (c) 2016-2019
- Declan de Wet
- Sébastien Chopin
- All the amazing contributors (https://github.com/nuxt/vue-meta/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+49 -790
View File
@@ -1,21 +1,18 @@
<p align="center">
<img src="http://imgur.com/258WtHI.png" alt="vue-meta">
<img src="./docs/.vuepress/public/vue-meta.png" alt="vue-meta" />
</p>
<h5 align="center">
Manage page meta info in Vue 2.0 components. SSR + Streaming supported. Inspired by <a href="https://github.com/nfl/react-helmet">react-helmet</a>.
Manage page metadata in Vue.js components with SSR support
</h5>
<p align="center">
<a href="https://github.com/feross/standard">
<img src="https://cdn.rawgit.com/feross/standard/master/badge.svg" alt="Standard - JavaScript Style">
</a>
</p>
<p align="center">
<a href="https://github.com/nuxt/vue-meta/releases/latest"><img src="https://img.shields.io/github/release/nuxt/vue-meta.svg" alt="github release"></a> <a href="http://npmjs.org/package/vue-meta"><img src="https://img.shields.io/npm/v/vue-meta.svg" alt="npm version"></a> <a href="https://circleci.com/gh/nuxt/vue-meta/"><img src="https://badgen.net/circleci/github/nuxt/vue-meta" alt="Build Status"></a> <a href="https://codecov.io/gh/nuxt/vue-meta"><img src="https://codecov.io/gh/nuxt/vue-meta/branch/master/graph/badge.svg" alt="codecov"></a><br>
<a href="https://david-dm.org/nuxt/vue-meta"><img src="https://david-dm.org/nuxt/vue-meta/status.svg" alt="dependencies Status"></a> <a href="https://david-dm.org/nuxt/vue-meta?type=dev"><img src="https://david-dm.org/nuxt/vue-meta/dev-status.svg" alt="devDependencies Status"></a><br>
<a href="http://npm-stat.com/charts.html?package=vue-meta"><img src="https://img.shields.io/npm/dm/vue-meta.svg" alt="npm downloads"></a> <a href="https://gitter.im/nuxt/vue-meta"><img src="https://badges.gitter.im/nuxt/vue-meta.svg" alt="Gitter"></a>
<a href="http://npm-stat.com/charts.html?package=vue-meta"><img src="https://img.shields.io/npm/dm/vue-meta.svg" alt="npm downloads"></a>
<a href="http://npmjs.org/package/vue-meta"><img src="https://img.shields.io/npm/v/vue-meta.svg" alt="npm version"></a>
<a href="https://codecov.io/gh/nuxt/vue-meta"><img src="https://badgen.net/codecov/c/github/nuxt/vue-meta/master" alt="Coverage Status"></a>
<a href="https://circleci.com/gh/nuxt/vue-meta/"><img src="https://badgen.net/circleci/github/nuxt/vue-meta" alt="Build Status"></a>
<a href="https://david-dm.org/nuxt/vue-meta"><img src="https://david-dm.org/nuxt/vue-meta/status.svg" alt="dependencies Status"></a>
<a href="https://discord.nuxtjs.org/"><img src="https://badgen.net/badge/Discord/join-us/7289DA" alt="Discord"></a>
</p>
```html
@@ -26,825 +23,87 @@
<script>
export default {
metaInfo: {
title: 'My Example App', // set a title
titleTemplate: '%s - Yay!', // title is now "My Example App - Yay!"
title: 'My Example App',
titleTemplate: '%s - Yay!',
htmlAttrs: {
lang: 'en',
amp: undefined // "amp" has no value
amp: true
}
}
}
</script>
```
```html
<html lang="en" amp>
<head>
<title>My Example App - Yay!</title>
...
</head>
```
# Introduction
Vue Meta is a [Vue.js](https://vuejs.org) plugin that allows you to manage your app's metadata. It is inspired by and works similar as [`react-helmet`](https://github.com/nfl/react-helmet) for react. However, instead of setting your data as props passed to a proprietary component, you simply export it as part of your component's data using the `metaInfo` property.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
# Table of Contents
These properties, when set on a deeply nested component, will cleverly overwrite their parent components' `metaInfo`, thereby enabling custom info for each top-level view as well as coupling metadata directly to deeply nested subcomponents for more maintainable code.
- [Description](#description)
- [Installation](#installation)
- [Yarn](#yarn)
- [NPM](#npm)
- [CDN](#cdn)
- [Usage](#usage)
- [Step 1: Preparing the plugin](#step-1-preparing-the-plugin)
- [Options](#options)
- [Step 2: Server Rendering (Optional)](#step-2-server-rendering-optional)
- [Step 2.1: Exposing `$meta` to `bundleRenderer`](#step-21-exposing-meta-to-bundlerenderer)
- [Step 2.2: Populating the document meta info with `inject()`](#step-22-populating-the-document-meta-info-with-inject)
- [Simple Rendering with `renderToString()`](#simple-rendering-with-rendertostring)
- [Streaming Rendering with `renderToStream()`](#streaming-rendering-with-rendertostream)
- [Step 3: Start defining `metaInfo`](#step-3-start-defining-metainfo)
- [Recognized `metaInfo` Properties](#recognized-metainfo-properties)
- [`title` (String)](#title-string)
- [`titleTemplate` (String | Function)](#titletemplate-string--function)
- [`htmlAttrs` (Object)](#htmlattrs-object)
- [`headAttrs` (Object)](#headattrs-object)
- [`bodyAttrs` (Object)](#bodyattrs-object)
- [`base` (Object)](#base-object)
- [`meta` ([Object])](#meta-object)
- [`link` ([Object])](#link-object)
- [`style` ([Object])](#style-object)
- [`script` ([Object])](#script-object)
- [`noscript` ([Object])](#noscript-object)
- [`__dangerouslyDisableSanitizers` ([String])](#__dangerouslydisablesanitizers-string)
- [`__dangerouslyDisableSanitizersByTagID` ({[String]})](#__dangerouslydisablesanitizersbytagid-string)
- [`changed` (Function)](#changed-function)
- [How `metaInfo` is Resolved](#how-metainfo-is-resolved)
- [Lists of Tags](#lists-of-tags)
- [Performance](#performance)
- [How to prevent the update on the initial page render](#how-to-prevent-the-update-on-the-initial-page-render)
- [FAQ](#faq)
- [How do I use component props and/or component data in `metaInfo`?](#how-do-i-use-component-props-andor-component-data-in-metainfo)
- [How do I populate `metaInfo` from the result of an asynchronous action?](#how-do-i-populate-metainfo-from-the-result-of-an-asynchronous-action)
- [Why doesn't `vue-meta` support `jsnext:main`?](#why-doesnt-vue-meta-support-jsnextmain)
- [Examples](#examples)
## Documentation
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
Please find the documention on https://vue-meta.nuxtjs.org
> :globe_with_meridians: Please help us translate the documentation into your language
# Description
`vue-meta` is a [Vue 2.0](https://vuejs.org) plugin that allows you to manage your app's meta information, much like [`react-helmet`](https://github.com/nfl/react-helmet) does for React. However, instead of setting your data as props passed to a proprietary component, you simply export it as part of your component's data using the `metaInfo` property.
## Examples
These properties, when set on a deeply nested component, will cleverly overwrite their parent components' `metaInfo`, thereby enabling custom info for each top-level view as well as coupling meta info directly to deeply nested subcomponents for more maintainable code.
Looking for more examples what vue-meta can do for you? Have a look at the [examples](https://github.com/nuxt/vue-meta/tree/master/examples)
# Installation
## Installation
### Yarn
##### Yarn
```sh
$ yarn add vue-meta
```
### NPM
##### npm
```sh
$ npm install vue-meta --save
```
### CDN
##### Download / CDN
Use the links below - if you want a previous version, check the instructions at https://unpkg.com.
Use the download links below - if you want a previous version, check the instructions at https://unpkg.com.
Latest version: https://unpkg.com/vue-meta/lib/vue-meta.min.js
Latest v1.x version: https://unpkg.com/vue-meta@1/lib/vue-meta.min.js
<!-- start CDN generator - do **NOT** remove this comment -->
**Uncompressed:**
```html
<script src="https://unpkg.com/vue-meta@1.6.0/lib/vue-meta.js"></script>
<script src="https://unpkg.com/vue-meta/lib/vue-meta.js"></script>
```
**Minified:**
```html
<script src="https://unpkg.com/vue-meta@1.6.0/lib/vue-meta.min.js"></script>
<script src="https://unpkg.com/vue-meta/lib/vue-meta.min.js"></script>
```
<!-- end CDN generator - do **NOT** remove this comment -->
# Usage
## Quick Usage
## Step 1: Preparing the plugin
> This step is optional if you don't need SSR and `Vue` is available as a global variable. `vue-meta` will install itself in this case.
In order to use this plugin, you first need to pass it to `Vue.use` - if you're not rendering on the server-side, your JS entry file will suffice. If you are rendering on the server, then place it in a file that runs both on the server and on the client before your root instance is mounted. If you're using [`vue-router`](https://github.com/vuejs/vue-router), then your main `router.js` file is a good place:
**router.js:**
See the [documentation](https://vue-meta.nuxtjs.org) for more information
```js
import Vue from 'vue'
import Router from 'vue-router'
import Meta from 'vue-meta'
import VueMeta from 'vue-meta'
Vue.use(Router)
Vue.use(Meta)
export default new Router({
...
Vue.use(VueMeta, {
// optional pluginOptions
refreshOnceOnNavigation: true
})
```
#### Options
## Higher level frameworks using vue-meta
If you wish to create your app even more quickly, take a look at the following frameworks which use vue-meta
`vue-meta` allows a few custom options:
- [Nuxt.js](https://github.com/nuxt/nuxt.js) - The Vue.js Meta framework
- [Gridsome](https://github.com/gridsome/gridsome) - The Vue.js JAMstack framework
```js
Vue.use(Meta, {
keyName: 'metaInfo', // the component option name that vue-meta looks for meta info on.
attribute: 'data-vue-meta', // the attribute name vue-meta adds to the tags it observes
ssrAttribute: 'data-vue-meta-server-rendered', // the attribute name that lets vue-meta know that meta info has already been server-rendered
tagIDKeyName: 'vmid' // the property name that vue-meta uses to determine whether to overwrite or append a tag
})
```
# License
If you don't care about server-side rendering, you can skip straight to [step 3](#step-3-start-defining-metainfo). Otherwise, continue. :smile:
## Step 2: Server Rendering (Optional)
If you have an isomorphic/universal webapp, you'll likely want to render your metadata on the server side as well. Here's how.
### Step 2.1: Exposing `$meta` to `bundleRenderer`
You'll need to expose the results of the `$meta` method that `vue-meta` adds to the Vue instance to the bundle render context before you can begin injecting your meta information. You'll need to do this in your server entry file:
**server-entry.js:**
```js
import app from './app'
const router = app.$router
const meta = app.$meta() // here
export default (context) => {
router.push(context.url)
context.meta = meta // and here
return app
}
```
### Step 2.2: Populating the document meta info with `inject()`
All that's left for you to do now before you can begin using `metaInfo` options in your components is to make sure they work on the server by `inject`-ing them so you can call `text()` on each item to render out the necessary info. You have two methods at your disposal:
#### Simple Rendering with `renderToString()`
Considerably the easiest method to wrap your head around is if your Vue server markup is rendered out as a string:
**server.js:**
```js
app.get('*', (req, res) => {
const context = { url: req.url }
renderer.renderToString(context, (error, html) => {
if (error) return res.send(error.stack)
const bodyOpt = { body: true }
const {
title, htmlAttrs, headAttrs, bodyAttrs, link, style, script, noscript, meta
} = context.meta.inject()
return res.send(`
<!doctype html>
<html data-vue-meta-server-rendered ${htmlAttrs.text()}>
<head ${headAttrs.text()}>
${meta.text()}
${title.text()}
${link.text()}
${style.text()}
${script.text()}
${noscript.text()}
</head>
<body ${bodyAttrs.text()}>
${html}
<script src="/assets/vendor.bundle.js"></script>
<script src="/assets/client.bundle.js"></script>
${script.text(bodyOpt)}
</body>
</html>
`)
})
})
```
If you are using a separate template file, edit your head tag with
```html
<head>
{{{ meta.inject().title.text() }}}
{{{ meta.inject().meta.text() }}}
</head>
```
Notice the use of `{{{` to avoid double escaping. Be extremely cautious when you use `{{{` with `__dangerouslyDisableSanitizers`.
#### Streaming Rendering with `renderToStream()`
A little more complex, but well worth it, is to instead stream your response. `vue-meta` supports streaming with no effort (on it's part :stuck_out_tongue_winking_eye:) thanks to Vue's clever `bundleRenderer` context injection:
**server.js**
```js
app.get('*', (req, res) => {
const context = { url: req.url }
const renderStream = renderer.renderToStream(context)
renderStream.once('data', () => {
const bodyOpt = { body: true }
const {
title, htmlAttrs, headAttrs, bodyAttrs, link, style, script, noscript, meta
} = context.meta.inject()
res.write(`
<!doctype html>
<html data-vue-meta-server-rendered ${htmlAttrs.text()}>
<head ${headAttrs.text()}>
${meta.text()}
${title.text()}
${link.text()}
${style.text()}
${script.text()}
${noscript.text()}
</head>
<body ${bodyAttrs.text()}>
`)
})
renderStream.on('data', (chunk) => {
res.write(chunk)
})
renderStream.on('end', () => {
res.end(`
<script src="/assets/vendor.bundle.js"></script>
<script src="/assets/client.bundle.js"></script>
${script.text(bodyOpt)}
</body>
</html>
`)
})
renderStream.on('error', (error) => res.status(500).end(`<pre>${error.stack}</pre>`))
})
```
## Step 3: Start defining `metaInfo`
In any of your components, define a `metaInfo` property:
**App.vue:**
```html
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
metaInfo: {
// if no subcomponents specify a metaInfo.title, this title will be used
title: 'Default Title',
// all titles will be injected into this template
titleTemplate: '%s | My Awesome Webapp'
}
}
</script>
```
**Home.vue**
```html
<template>
<div id="page">
<h1>Home Page</h1>
</div>
</template>
<script>
export default {
name: 'Home',
metaInfo: {
title: 'My Awesome Webapp',
// override the parent template and just use the above title only
titleTemplate: null
}
}
</script>
```
**About.vue**
```html
<template>
<div id="page">
<h1>About Page</h1>
</div>
</template>
<script>
export default {
name: 'About',
metaInfo: {
// title will be injected into parent titleTemplate
title: 'About Us'
}
}
</script>
```
### Recognized `metaInfo` Properties
#### `title` (String)
Maps to the inner-text value of the `<title>` element.
```js
{
metaInfo: {
title: 'Foo Bar'
}
}
```
```html
<title>Foo Bar</title>
```
#### `titleTemplate` (String | Function)
The value of `title` will be injected into the `%s` placeholder in `titleTemplate` before being rendered. The original title will be available on `metaInfo.titleChunk`.
```js
{
metaInfo: {
title: 'Foo Bar',
titleTemplate: '%s - Baz'
}
}
```
```html
<title>Foo Bar - Baz</title>
```
The property can also be a function (from [v1.2.0](https://github.com/nuxt/vue-meta/releases/tag/v1.2.0)):
```js
titleTemplate: (titleChunk) => {
// If undefined or blank then we don't need the hyphen
return titleChunk ? `${titleChunk} - Site Title` : 'Site Title';
}
```
#### `htmlAttrs` (Object)
Each **key:value** maps to the equivalent **attribute:value** of the `<html>` element.
```js
{
metaInfo: {
htmlAttrs: {
foo: 'bar',
amp: undefined
}
}
}
```
```html
<html foo="bar" amp></html>
```
#### `headAttrs` (Object)
Each **key:value** maps to the equivalent **attribute:value** of the `<head>` element.
```js
{
metaInfo: {
headAttrs: {
foo: 'bar'
}
}
}
```
```html
<head foo="bar"></head>
```
#### `bodyAttrs` (Object)
Each **key:value** maps to the equivalent **attribute:value** of the `<body>` element.
```js
{
metaInfo: {
bodyAttrs: {
bar: 'baz'
}
}
}
```
```html
<body bar="baz">Foo Bar</body>
```
#### `base` (Object)
Maps to a newly-created `<base>` element, where object properties map to attributes.
```js
{
metaInfo: {
base: { target: '_blank', href: '/' }
}
}
```
```html
<base target="_blank" href="/">
```
#### `meta` ([Object])
Each item in the array maps to a newly-created `<meta>` element, where object properties map to attributes.
```js
{
metaInfo: {
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
]
}
}
```
```html
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
```
Since v1.5.0, you can now set up meta templates that work similar to the titleTemplate:
```js
{
metaInfo: {
meta: [
{ charset: 'utf-8' },
{
'property': 'og:title',
'content': 'Test title',
'template': chunk => `${chunk} - My page`, //or as string template: '%s - My page',
'vmid': 'og:title'
}
]
}
}
```
```html
<meta charset="utf-8">
<meta name="og:title" property="og:title" content="Test title - My page">
```
#### `link` ([Object])
Each item in the array maps to a newly-created `<link>` element, where object properties map to attributes.
```js
{
metaInfo: {
link: [
{ rel: 'stylesheet', href: '/css/index.css' },
{ rel: 'favicon', href: 'favicon.ico' }
]
}
}
```
```html
<link rel="stylesheet" href="/css/index.css">
<link rel="favicon" href="favicon.ico">
```
#### `style` ([Object])
Each item in the array maps to a newly-created `<style>` element, where object properties map to attributes.
```js
{
metaInfo: {
style: [
{ cssText: '.foo { color: red }', type: 'text/css' }
]
}
}
```
```html
<style type="text/css">.foo { color: red }</style>
```
#### `script` ([Object])
Each item in the array maps to a newly-created `<script>` element, where object properties map to attributes.
```js
{
metaInfo: {
script: [
{ src: 'https://cdn.jsdelivr.net/npm/vue/dist/vue.js', async: true, defer: true }
],
}
}
```
```html
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js" async="true" defer="true"></script>
```
:warning: You have to disable sanitizers so the content of `innerHTML` won't be escaped. Please refer to [\__dangerouslyDisableSanitizers](#__dangerouslydisablesanitizers-string) section below for more info on related risks.
```js
{
metaInfo: {
script: [
{ innerHTML: '{ "@context": "http://schema.org" }', type: 'application/ld+json' }
],
__dangerouslyDisableSanitizers: ['script'],
}
}
```
```html
<script type="application/ld+json">{ "@context": "http://schema.org" }</script>
```
If your browser doesn't support `defer` or any other reason, you want to put `<script>` before `</body>`, use `body`.
```js
{
metaInfo: {
script: [
{ innerHTML: 'console.log("I am in body");', type: 'text/javascript', body: true }
]
}
}
```
#### `noscript` ([Object])
Each item in the array maps to a newly-created `<noscript>` element, where object properties map to attributes.
```js
{
metaInfo: {
noscript: [
{ innerHTML: 'This website requires JavaScript.' }
]
}
}
```
```html
<noscript>This website requires JavaScript.</noscript>
```
#### `__dangerouslyDisableSanitizers` ([String])
By default, `vue-meta` sanitizes HTML entities in _every_ property. You can disable this behaviour on a per-property basis using `__dangerouslyDisableSantizers`. Just pass it a list of properties you want sanitization to be disabled on:
```js
{
metaInfo: {
title: '<I will be sanitized>',
meta: [{ vmid: 'description', name: 'description', content: '& I will not be <sanitized>'}],
__dangerouslyDisableSanitizers: ['meta']
}
}
```
```html
<title>&lt;I will be sanitized&gt;</title>
<meta vmid="description" name="description" content="& I will not be <sanitized>">
```
:warning: **Using this option is not recommended unless you know exactly what you are doing.** By disabling sanitization, you are opening potential vectors for attacks such as SQL injection & Cross-Site Scripting (XSS). Be very careful to not compromise your application.
#### `__dangerouslyDisableSanitizersByTagID` ({[String]})
Provides same functionality as `__dangerouslyDisableSanitizers` but you can specify which property for which `tagIDKeyName`'s sanitization should be disabled. It expects an object with the vmid's as key and an array with property names value:
```js
{
metaInfo: {
title: '<I will be sanitized>',
meta: [{ vmid: 'description', name: 'still-&-sanitized', content: '& I will not be <sanitized>'}],
__dangerouslyDisableSanitizersByTagID: { description: ['content'] }
}
}
```
```html
<title>&lt;I will be sanitized&gt;</title>
<meta vmid="description" name="still-&amp;-sanitized" content="& I will not be <sanitized>">
```
:warning: **Using this option is not recommended unless you know exactly what you are doing.** By disabling sanitization, you are opening potential vectors for attacks such as SQL injection & Cross-Site Scripting (XSS). Be very careful to not compromise your application.
#### `changed` (Function)
Will be called when the client `metaInfo` updates/changes. Receives the following parameters:
- `newInfo` (Object) - The new state of the `metaInfo` object.
- `addedTags` ([HTMLElement]) - a list of elements that were added.
- `removedTags` ([HTMLElement]) - a list of elements that were removed.
`this` context is the component instance `changed` is defined on.
```js
{
metaInfo: {
changed (newInfo, addedTags, removedTags) {
console.log('Meta info was updated!')
}
}
}
```
### How `metaInfo` is Resolved
You can define a `metaInfo` property on any component in the tree. Child components that have `metaInfo` will recursively merge their `metaInfo` into the parent context, overwriting any duplicate properties. To better illustrate, consider this component heirarchy:
```html
<parent>
<child></child>
</parent>
```
If both `<parent>` _and_ `<child>` define a `title` property inside `metaInfo`, then the `title` that gets rendered will resolve to the `title` defined inside `<child>`.
#### Lists of Tags
When specifying an array in `metaInfo`, like in the below examples, the default behaviour is to simply concatenate the lists.
**Input:**
```js
// parent component
{
metaInfo: {
meta: [
{ charset: 'utf-8' },
{ name: 'description', content: 'foo' }
]
}
}
// child component
{
metaInfo: {
meta: [
{ name: 'description', content: 'bar' }
]
}
}
```
**Output:**
```html
<meta charset="utf-8">
<meta name="description" content="foo">
<meta name="description" content="bar">
```
This is not what we want, since the meta `description` needs to be unique for every page. If you want to change this behaviour such that `description` is instead replaced, then give it a `vmid`:
**Input:**
```js
// parent component
{
metaInfo: {
meta: [
{ charset: 'utf-8' },
{ vmid: 'description', name: 'description', content: 'foo' }
]
}
}
// child component
{
metaInfo: {
meta: [
{ vmid: 'description', name: 'description', content: 'bar' }
]
}
}
```
**Output:**
```html
<meta charset="utf-8">
<meta vmid="description" name="description" content="bar">
```
While solutions like `react-helmet` manage the occurrence order and merge behaviour for you automatically, it involves a lot more code and is therefore prone to failure in some edge-cases, whereas this method is _almost_ bulletproof because of its versatility; _at the expense of one tradeoff:_ these `vmid` properties will be rendered out in the final markup (`vue-meta` uses these client-side to prevent duplicating or overriding markup). If you are serving your content GZIP'ped, then the slight increase in HTTP payload size is negligible.
# Performance
On the client, `vue-meta` batches DOM updates using [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame). It needs to do this because it registers a Vue mixin that subscribes to the [`beforeMount`](https://vuejs.org/api/#beforeMount) lifecycle hook on all components in order to be notified that renders have occurred and data is ready. If `vue-meta` did not batch updates, the DOM meta info would be re-calculated and re-updated for every component on the page in quick-succession.
Thanks to batch updating, the update will only occurr once - even if the correct meta info has already been compiled by the server. If you don't want this behaviour, see below.
### How to prevent the update on the initial page render
Add the `data-vue-meta-server-rendered` attribute to the `<html>` tag on the server-side:
```html
<html data-vue-meta-server-rendered>
...
```
`vue-meta` will check for this attribute whenever it attempts to update the DOM - if it exists, `vue-meta` will just remove it and perform no updates. If it does not exist, `vue-meta` will perform updates as usual.
> **Note:** While this may seem verbose, it _is_ intentional. Having `vue-meta` handle this for you automatically would limit interoperability with other server-side programming languages. If you use PHP to power your server, for example, you might also have meta info handled on the server already and want to prevent this extraneous update.
# FAQ
Here are some answers to some frequently asked questions.
## How do I use component props and/or component data in `metaInfo`?
Easy. Instead of defining `metaInfo` as an object, define it as a function and access `this` as usual:
**Post.vue:**
```html
<template>
<div>
<h1>{{{ title }}}</h1>
</div>
</template>
<script>
export default {
name: 'post',
props: ['title'],
data () {
return {
description: 'A blog post about some stuff'
}
},
metaInfo () {
return {
title: this.title,
meta: [
{ vmid: 'description', name: 'description', content: this.description }
]
}
}
}
</script>
```
**PostContainer.vue:**
```html
<template>
<div>
<post :title="title"></post>
</div>
</template>
<script>
import Post from './Post.vue'
export default {
name: 'post-container',
components: { Post },
data () {
return {
title: 'Example blog post'
}
}
}
</script>
```
## How do I populate `metaInfo` from the result of an asynchronous action?
`vue-meta` will do this for you automatically when your component state changes.
Just make sure that you're using the function form of `metaInfo`:
```js
{
data () {
return {
title: 'Foo Bar Baz'
}
},
metaInfo () {
return {
title: this.title
}
}
}
```
Check out the [vuex-async](https://github.com/nuxt/vue-meta/tree/master/examples/vuex-async) example for a far more detailed demonstration if you have doubts.
Credit & Thanks for this feature goes to [Sébastien Chopin](https://github.com/Atinux).
## Why doesn't `vue-meta` support `jsnext:main`?
Originally, it did - however, it caused [problems](https://github.com/nuxt/vue-meta/issues/25). Essentially, Vue [does not support](https://github.com/vuejs/vue/issues/2880) `jsnext:main`, and does not introspect for the `default` property that is transpiled from the ES2015 source, thus breaking module resolution.
Given that `jsnext:main` is a non-standard property that won't stick around for long, and `vue-meta` is bundled into one file with no dynamic module internals as well as the fact that if you're using `vue-meta`, you're 99.9% likely to not be using it conditionally - the decision has been made to drop support for it entirely.
If this were not the case, you would have to instruct Babel to convert `default` imports to the proper commonjs module syntax via a plugin, which is not ideal since many users in the Vue landscape write their code in TypeScript, not Babel.
# Examples
To run the examples; clone this repository & run `npm install` in the root directory, and then run `npm run dev`. Head to http://localhost:8080.
[MIT](./LICENSE.md)
+15
View File
@@ -0,0 +1,15 @@
module.exports = {
"plugins": ["@babel/plugin-syntax-dynamic-import"],
"env": {
"test": {
"plugins": ["dynamic-import-node"],
"presets": [
[ "@babel/env", {
targets: {
node: "current"
}
}]
]
}
},
}
+65
View File
@@ -0,0 +1,65 @@
module.exports = {
locales: {
'/': {
lang: 'en-US',
title: 'Vue Meta',
description: 'Metadata manager for Vue.js'
},
},
ga: 'UA-88662854-1',
serviceWorker: true,
themeConfig: {
repo: 'nuxt/vue-meta',
docsDir: 'docs',
locales: {
'/': {
label: 'English',
selectText: 'Languages',
editLinkText: 'Edit this page on GitHub',
nav: [{
text: 'Guide',
link: '/guide/'
}, {
text: 'API',
link: '/api/'
}, {
text: 'Release Notes',
link: 'https://github.com/nuxt/vue-meta/releases'
}],
sidebar: [
'/',
{
title: 'Getting started',
collapsable: false,
children: [
'/guide/',
'/guide/preparing',
'/guide/ssr',
'/guide/frameworks'
]
},
{
title: 'Usage',
collapsable: false,
children: [
'/guide/metainfo',
'/guide/special',
'/guide/caveats',
]
},
{
title: 'FAQ',
collapsable: false,
children: [
'/faq/',
'/faq/performance.md',
'/faq/prevent-initial.md',
'/faq/component-props.md',
'/faq/async-action.md',
]
}
]
},
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+14
View File
@@ -0,0 +1,14 @@
<img src="vue-meta.png" alt="vue-meta"/>
::: tip We need your help
We are working on defining the RFC for Vue Meta v3.0. It will be a ground-breaking release built from the ground up.
We would like your help with this! Please visit the [Vue Meta v3.0 rfc](https://github.com/nuxt/rfcs/issues/19) and let us know your thoughts.
:::
# Introduction
`vue-meta` is a [Vue.js](https://vuejs.org) plugin that allows you to manage your app's metadata, much like [`react-helmet`](https://github.com/nfl/react-helmet) does for React. However, instead of setting your data as props passed to a proprietary component, you simply export it as part of your component's data using the `metaInfo` property.
These properties, when set on a deeply nested component, will cleverly overwrite their parent components' `metaInfo`, thereby enabling custom info for each top-level view as well as coupling metadata directly to deeply nested sub components for more maintainable code.
[Get started](/guide) or play with the [examples](https://github.com/nuxt/vue-meta/tree/master/examples)
+470
View File
@@ -0,0 +1,470 @@
---
sidebar: auto
---
# API Reference
## Plugin options
### keyName
- type `string`
- default `metaInfo`
The name of the component option that contains all the information that gets converted to the various meta tags & attributes for the page
### attribute
- type `string`
- default `data-vue-meta`
The name of the attribute vue-meta arguments on elements to know which it should manage and which it should ignore.
### ssrAttribute
- type `string`
- default `data-vue-meta-server-rendered`
The name of the attribute that is added to the `html` tag to inform `vue-meta` that the server has already generated the meta tags for the initial render
See [How to prevent update on page load](/faq/prevent-initial)
### tagIDKeyName
- type `string`
- default `vmid`
The property that tells `vue-meta` to overwrite (instead of append) an item in a tag list.
For example, if you have two `meta` tag list items that both have `vmid` of 'description',
then vue-meta will overwrite the shallowest one with the deepest one.
### contentKeyName
- type `string`
- default `content`
The key name for the content-holding property
### metaTemplateKeyName
- type `string`
- default `template`
The key name for possible meta templates
### refreshOnceOnNavigation
- type `boolean`
- default `false`
When `true` then `vue-meta` will pause updates once page navigation starts and resumes updates when navigation finishes (resuming also triggers an update).
This could both be a performance improvement as a possible fix for 'flickering' when you are e.g. replacing stylesheets
## Plugin methods
The `vue-meta` plugin injects a `$meta()` function in the Vue prototype which provides the following methods
:::tip Note
`$meta()` is a function so we only need to insert it once in the `Vue.prototype`, but still use `this` to reference the component it was called from
:::
### $meta().getOptions
- returns [`pluginOptions`](/api/#plugin-options)
Could be used by third-party libraries who wish to interact with `vue-meta`
### $meta().refresh
- returns [`metaInfo`](/api/#metaInfo-properties)
Updates the current metadata with new metadata.
Useful when updating metadata as the result of an asynchronous action that resolves after the initial render takes place.
### $meta().inject
- returns [`metaInfo`](/api/#metaInfo-properties)
:::tip SSR only
`inject` is available in the server plugin only and is not available on the client
:::
It returns a special `metaInfo` object where all keys have an object as value which contains a `text()` method for returning html code
See [Rendering with renderToString](/guide/ssr.html#simple-rendering-with-rendertostring) for an example
### $meta().pause
- arguments:
- refresh (type `boolean`, default `false`)
- returns `resume()`
Pauses global metadata updates until either the returned resume method is called or [resume](/api/#meta-resume)
### $meta().resume
- arguments:
- refresh (type `boolean`, default `false`)
- returns [`metaInfo`](/api/#metaInfo-properties) (optional)
Resumes metadata updates after they have been paused. If `refresh` is `true` it immediately initiates a metadata update by calling [refresh](/api/#meta-refresh)
## metaInfo properties
::: tip Note
The documentation below uses `metaInfo` as `keyName` in the examples, please note that this is [configurable](/api/#keyname) and could be different in your case
:::
### title
- type `string`
Maps to the inner-text value of the `<title>` element.
```js
{
metaInfo: {
title: 'Foo Bar'
}
}
```
```html
<title>Foo Bar</title>
```
### titleTemplate
- type `string | Function`
The value of `title` will be injected into the `%s` placeholder in `titleTemplate` before being rendered. The original title will be available on `metaInfo.titleChunk`.
```js
{
metaInfo: {
title: 'Foo Bar',
titleTemplate: '%s - Baz'
}
}
```
```html
<title>Foo Bar - Baz</title>
```
The property can also be a function:
```js
titleTemplate: (titleChunk) => {
// If undefined or blank then we don't need the hyphen
return titleChunk ? `${titleChunk} - Site Title` : 'Site Title';
}
```
### htmlAttrs
### headAttrs
### bodyAttrs
- type `object`
Each **key:value** maps to the equivalent **attribute:value** of the `<body>` element.
Since `v2.0` value can also be an `Array<string>`
```js
{
metaInfo: {
htmlAttrs: {
lang: 'en',
amp: true
},
bodyAttrs: {
class: ['dark-mode', 'mobile']
}
}
}
```
```html
<html lang="en" amp>
<body class="dark-mode mobile">Foo Bar</body>
```
### base
- type `object`
Maps to a newly-created `<base>` element, where object properties map to attributes.
```js
{
metaInfo: {
base: { target: '_blank', href: '/' }
}
}
```
```html
<base target="_blank" href="/">
```
### meta
- type `collection`
Each item in the array maps to a newly-created `<meta>` element, where object properties map to attributes.
```js
{
metaInfo: {
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
]
}
}
```
```html
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
```
#### Content templates
Since `v1.5.0`, you can now set up meta templates that work similar to the titleTemplate:
```js
{
metaInfo: {
meta: [
{ charset: 'utf-8' },
{
'property': 'og:title',
'content': 'Test title',
'template': chunk => `${chunk} - My page`, //or as string template: '%s - My page',
'vmid': 'og:title'
}
]
}
}
```
```html
<meta charset="utf-8">
<meta name="og:title" property="og:title" content="Test title - My page">
```
### link
- type `collection`
Each item in the array maps to a newly-created `<link>` element, where object properties map to attributes.
```js
{
metaInfo: {
link: [
{ rel: 'stylesheet', href: '/css/index.css' },
{ rel: 'favicon', href: 'favicon.ico' }
]
}
}
```
```html
<link rel="stylesheet" href="/css/index.css">
<link rel="favicon" href="favicon.ico">
```
### style
- type `object`
Each item in the array maps to a newly-created `<style>` element, where object properties map to attributes.
```js
{
metaInfo: {
style: [
{ cssText: '.foo { color: red }', type: 'text/css' }
]
}
}
```
```html
<style type="text/css">.foo { color: red }</style>
```
### script
- type `collection`
Each item in the array maps to a newly-created `<script>` element, where object properties map to attributes.
```js
{
metaInfo: {
script: [
{ src: 'https://cdn.jsdelivr.net/npm/vue/dist/vue.js', async: true, defer: true }
],
}
}
```
```html
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js" async defer></script>
```
#### Add json or other raw data
::: warning
You have to disable sanitizers so the content of `innerHTML` won't be escaped. Please see [__dangerouslyDisableSanitizersByTagID](/api/#dangerouslydisablesanitizersbytagid) for more info on related risks
:::
```js
{
metaInfo: {
script: [{
vmid: 'ldjson-schema',
innerHTML: '{ "@context": "http://schema.org" }',
type: 'application/ld+json'
}],
__dangerouslyDisableSanitizersByTagID: {
'ldjson-schema': ['innerHTML']
},
}
}
```
```html
<script type="application/ld+json">{ "@context": "http://schema.org" }</script>
```
#### Special attribute: `body: true`
If you e.g. wish to force delayed execution of a script or just want the script to be included in the `<body>` of the page, add `body: true`.
Script tags with `body: true` are rendered just before `</body>`
```js
{
metaInfo: {
script: [{
innerHTML: 'console.log("I am in body");',
type: 'text/javascript',
body: true
}]
}
}
```
### noscript
- type `collection`
Each item in the array maps to a newly-created `<noscript>` element, where object properties map to attributes.
```js
{
metaInfo: {
noscript: [
{ innerHTML: 'This website requires JavaScript.' }
]
}
}
```
```html
<noscript>This website requires JavaScript.</noscript>
```
### __dangerouslyDisableSanitizers
- type `Array<string>`
::: danger
If you need to disable sanitation, please always use [__dangerouslyDisableSanitizersByTagID](/api/#dangerouslydisablesanitizers) when possible
By disabling sanitization, you are opening potential vectors for attacks such as SQL injection & Cross-Site Scripting (XSS). Be very careful to not compromise your application.
:::
By default, `vue-meta` sanitizes HTML entities in _every_ property. You can disable this behaviour on a per-property basis using `__dangerouslyDisableSantizers`. Just pass it a list of properties you want sanitization to be disabled on:
```js
{
metaInfo: {
title: '<I will be sanitized>',
meta: [{
vmid: 'description',
name: 'description',
content: '& I will not be <sanitized>'
}],
__dangerouslyDisableSanitizers: ['meta']
}
}
```
```html
<title>&lt;I will be sanitized&gt;</title>
<meta vmid="description" name="description" content="& I will not be <sanitized>">
```
### __dangerouslyDisableSanitizersByTagID
- type `object`
::: warning
By disabling sanitation, you are opening potential vectors for attacks such as SQL injection & Cross-Site Scripting (XSS). Be very careful to not compromise your application.
:::
Provides same functionality as `__dangerouslyDisableSanitizers` but you can specify which property for which `tagIDKeyName` sanitation should be disabled. It expects an object with the vmid as key and an array with property keys as value:
```js
{
metaInfo: {
title: '<I will be sanitized>',
meta: [{
vmid: 'description',
name: 'still-&-sanitized',
content: '& I will not be <sanitized>'
}],
__dangerouslyDisableSanitizersByTagID: {
description: ['content']
}
}
}
```
```html
<title>&lt;I will be sanitized&gt;</title>
<meta vmid="description" name="still-&amp;-sanitized" content="& I will not be <sanitized>">
```
### changed
- type `Function`
A callback function which is called whenever the `metaInfo` updates / changes.
The callback receives the following arguments:
- **newInfo**
- type `object`<br/>
The updated [`metaInfo`](/api/#metaInfo-properties) object
- **addedTags**
- type `Array<HTMLElement>`<br/>
List of elements that were added
- **removedTags**
- type `Array<HTMLElement>`<br/>
List of elements that were removed
```js
{
metaInfo: {
changed (newInfo, addedTags, removedTags) {
console.log('Metadata was updated!')
}
}
}
```
### afterNavigation
- type `Function`
A callback function which is called when `vue-meta` has updated the metadata after navigation occurred.
This can be used to track page views with the updated document title etc.
Adding a `afterNavigation` callback behaves the same as when [refreshOnceOnNavigation](/api/#refreshonceonnavigation) is `true`
The callback receives the following arguments:
- **newInfo**
- type `object`<br/>
The updated [`metaInfo`](/api/#metaInfo-properties) object
```js
{
metaInfo: {
afterNavigation(metaInfo) {
trackPageView(document.title)
// is the same as
trackPageView(metaInfo.title)
}
}
}
```
+76
View File
@@ -0,0 +1,76 @@
# How is `metaInfo` resolved?
You can define a `metaInfo` property on any component in the tree. Child components that have `metaInfo` will recursively merge their `metaInfo` into the parent context, overwriting any duplicate properties. To better illustrate, consider this component hierarchy:
```html
<parent>
<child></child>
</parent>
```
If both `<parent>` _and_ `<child>` define a `title` property inside `metaInfo`, then the `title` that gets rendered will resolve to the `title` defined inside `<child>`.
## Concatenate metadata
When specifying an array in `metaInfo`, like in the below examples, the default behaviour is to simply concatenate the lists.
**Input:**
```js
// parent component
{
metaInfo: {
meta: [
{ charset: 'utf-8' },
{ name: 'description', content: 'foo' }
]
}
}
// child component
{
metaInfo: {
meta: [
{ name: 'description', content: 'bar' }
]
}
}
```
**Output:**
```html
<meta charset="utf-8">
<meta name="description" content="foo">
<meta name="description" content="bar">
```
## Unique metadata
This is not what we want, since the meta `description` needs to be unique for every page. If you want to change this behaviour such that `description` is instead replaced, then give it a `vmid`:
**Input:**
```js
// parent component
{
metaInfo: {
meta: [
{ charset: 'utf-8' },
{ vmid: 'description', name: 'description', content: 'foo' }
]
}
}
// child component
{
metaInfo: {
meta: [
{ vmid: 'description', name: 'description', content: 'bar' }
]
}
}
```
**Output:**
```html
<meta charset="utf-8">
<meta vmid="description" name="description" content="bar">
```
While solutions like `react-helmet` manage the occurrence order and merge behaviour for you automatically, it involves a lot more code and is therefore prone to failure in some edge-cases, whereas this method is _almost_ bulletproof because of its versatility; _at the expense of one tradeoff:_ these `vmid` properties will be rendered out in the final markup (`vue-meta` uses these client-side to prevent duplicating or overriding markup). If you are serving your content gzipped, then the slight increase in HTTP payload size is negligible.
+22
View File
@@ -0,0 +1,22 @@
# How to use async data in metaInfo?
`vue-meta` will do this for you automatically when your component state changes.
Just make sure that you're using the function form of `metaInfo`:
```js
{
data () {
return {
title: 'Foo Bar Baz'
}
},
metaInfo () {
return {
title: this.title
}
}
}
```
Check out the [vuex-async](https://github.com/nuxt/vue-meta/tree/master/examples/vuex-async) example for a more detailed demonstration
+55
View File
@@ -0,0 +1,55 @@
# How to use component props or data
Easy. Instead of defining `metaInfo` as an object, define it as a function and access `this` as usual:
**Post.vue:**
```html
<template>
<div>
<h1>{{{ title }}}</h1>
</div>
</template>
<script>
export default {
name: 'post',
props: ['title'],
data () {
return {
description: 'A blog post about some stuff'
}
},
metaInfo () {
return {
title: this.title,
meta: [
{ vmid: 'description', name: 'description', content: this.description }
]
}
}
}
</script>
```
**PostContainer.vue:**
```html
<template>
<div>
<post :title="title"></post>
</div>
</template>
<script>
import Post from './Post.vue'
export default {
name: 'post-container',
components: { Post },
data () {
return {
title: 'Example blog post'
}
}
}
</script>
```
+16
View File
@@ -0,0 +1,16 @@
# Any performance considerations?
Short answer, no
On the client, `vue-meta` batches DOM updates using [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame). It needs to do this because it registers a Vue mixin that subscribes to the [`beforeMount`](https://vuejs.org/api/#beforeMount) lifecycle hook on all components in order to be notified that renders have occurred and data is ready. If `vue-meta` did not batch updates, the DOM metadata would be re-calculated and re-updated for every component on the page in quick-succession.
Thanks to batch updating, the update will only occurr once - even if the correct metadata has already been compiled by the server. If you don't want this behaviour, see below.
:::tip Improvements since v2.0
Previous versions of vue-meta injected lifecycle hooks from the global mixin on all components on the page. Also when refreshing metadata it checked all components on the page
Since v2.0 runtime performance should be improved due to:
- the global mixin injects just a `beforeCreate` lifecycle hook, other hooks are only added for components which define `metaInfo`
- we track component branches with `vue-meta` components which means that when refreshing metadata we can skip branches without `metaInfo`
:::
+12
View File
@@ -0,0 +1,12 @@
# How to prevent update on page load
Add the `data-vue-meta-server-rendered` attribute to the `<html>` tag on the server-side:
```html
<html data-vue-meta-server-rendered>
...
```
`vue-meta` will check for this attribute whenever it attempts to update the DOM - if it exists, `vue-meta` will just remove it and perform no updates. If it does not exist, `vue-meta` will perform updates as usual.
While this may seem verbose, it _is_ intentional. Having `vue-meta` handle this for you automatically would limit interoperability with other server-side programming languages. If you use PHP to power your server, for example, you might also have metadata handled on the server already and want to prevent this extraneous update.
+54
View File
@@ -0,0 +1,54 @@
# Installation
:::tip Using a framework?
Are you using a framework like Nuxt.js, Gridsome or another one which uses vue-meta? Then `vue-meta` should already be installed and you can skip to [Usage](/guide/metainfo.html) or consult the [documentation](/guide/frameworks.html) of your framework for more information.
:::
## Download / CDN
[https://unpkg.com/vue-meta/lib/vue-meta.js](https://unpkg.com/vue-meta/lib/vue-meta.js)
For the latest version in the v1.x branch you can use:<br/>
[https://unpkg.com/vue-meta@1/lib/vue-meta.js](https://unpkg.com/vue-meta@1/lib/vue-meta.js)
Or you can replace `1` with the full version number you wish to use.
If you include vue-meta after Vue it will install automatically
**Unminified (suggested only for dev):**
```html
<script src="https://unpkg.com/vue-meta/lib/vue-meta.js"></script>
```
**Minified:**
```html
<script src="https://unpkg.com/vue-meta/lib/vue-meta.min.js"></script>
```
## Package manager
**Yarn**
```sh
$ yarn add vue-meta
```
**npm**
```sh
$ npm i vue-meta
```
### Install
:::warning Using a framework?
If you use a framework like Nuxt.js or Gridsome, vue-meta comes pre-installed and this step is most likely **not** required. Consult the [documentation](/guide/frameworks.html) of your framework for more information
:::
If you add `vue-meta` with a package manager, you will need to install the `vue-meta` plugin manually:
```js
import Vue from 'vue'
import VueMeta from 'vue-meta'
Vue.use(VueMeta)
```
+39
View File
@@ -0,0 +1,39 @@
# Caveats
## Reactive variables in template functions
Both [title](/api/#titletemplate) as [meta](/api/#content-templates) support using template function.
Due to how Vue determines reactivity it is not possible to use reactive variables directly in template functions
```js
{
// this wont work
metaInfo() {
return {
titleTemplate: chunk => (
this.locale === 'nl-NL'
? `${chunk} - Welkom`
: `${chunk} - Welcome`
)
}
}
}
```
You need to assign the reactive variable to a local variable first for this to work:
```js
{
// this will work
metaInfo() {
const locale = this.locale
return {
titleTemplate: chunk => (
locale === 'nl-NL'
? `${chunk} - Welkom`
: `${chunk} - Welcome`
)
}
}
}
```
+11
View File
@@ -0,0 +1,11 @@
# Frameworks
If you are using a framework which uses `vue-meta`, please make sure to consult their documentation first
## Gridsome
Proceed to the [Gridsome documentation](https://gridsome.org/docs)
## Nuxt.js
Proceed to the [Nuxt.js documentation](https://nuxtjs.org/api)
+69
View File
@@ -0,0 +1,69 @@
# Defining `metaInfo`
You can define a `[keyName]` property in any of your components, by default this is `metaInfo`.
See the [API](/api) for a list of recognised `metaInfo` properties
::: tip Note
Altough we talk about the `metaInfo` variable on this page, please note that the keyName is [configurable](/api/#keyname) and could be different in your case. E.g. [Nuxt.js](https://nuxtjs.org/api/pages-head#the-head-method) uses `head` as keyName
:::
**App.vue:**
```html
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
metaInfo: {
// if no subcomponents specify a metaInfo.title, this title will be used
title: 'Default Title',
// all titles will be injected into this template
titleTemplate: '%s | My Awesome Webapp'
}
}
</script>
```
**Home.vue**
```html
<template>
<div id="page">
<h1>Home Page</h1>
</div>
</template>
<script>
export default {
name: 'Home',
metaInfo: {
title: 'My Awesome Webapp',
// override the parent template and just use the above title only
titleTemplate: null
}
}
</script>
```
**About.vue**
```html
<template>
<div id="page">
<h1>About Page</h1>
</div>
</template>
<script>
export default {
name: 'About',
metaInfo: {
// title will be injected into parent titleTemplate
title: 'About Us'
}
}
</script>
```
+36
View File
@@ -0,0 +1,36 @@
# Preparing the plugin
:::tip Note
This step is optional if you don't need SSR and `Vue` is available as a global variable. `vue-meta` will install itself in this case.
:::
In order to use this plugin, you first need to pass it to `Vue.use` - if you're not rendering on the server-side, your entry file will suffice. If you are rendering on the server, then place it in a file that runs both on the server and on the client before your root instance is mounted. If you're using [`vue-router`](https://github.com/vuejs/vue-router), then your main `router.js` file is a good place:
**router.js:**
```js
import Vue from 'vue'
import Router from 'vue-router'
import Meta from 'vue-meta'
Vue.use(Router)
Vue.use(Meta)
export default new Router({
...
})
```
## Options
`vue-meta` allows a few custom options:
```js
Vue.use(Meta, {
keyName: 'metaInfo',
attribute: 'data-vue-meta',
ssrAttribute: 'data-vue-meta-server-rendered',
tagIDKeyName: 'vmid',
refreshOnceOnNavigation: true
})
```
See the [API](/api/#plugin-options) for a description of the available plugin options
+108
View File
@@ -0,0 +1,108 @@
# Special cases
::: tip Read first
Understanding [How is metaInfo resolved?](/faq/#concatenate-metadata) is probably a prerequisite for these cases
:::
## Remove parent property by child
If a child returns `null` as content value then the parent metaInfo property with the same `vmid` will be ignored
:::tip Content value
With content value we mean the following value of a `metaInfo` property:
- the value of a key for `object` types as [`htmlAttrs`](/api/#htmlattrs)
- the value of `[contentKeyName]` or `innerHTML` keys for `collection` types as [`meta`](/api/#meta)
:::
The following might be a bit far-fetched, but its just an example
```js
// parent
metaInfo: {
style: [{
vmid: 'page-load-overlay',
innerHTML: `
body div.loading {
z-index: 999;
background-color: #0f0f0f;
opacity: 0.9;
}
`,
}]
}
// dynamically loaded child
metaInfo() {
const style = this.cssTexts
return { style }
},
data() {
return {
this.cssTexts: []
}
},
mounted() {
this.cssTexts.push({
vmid: 'page-load-overlay',
innerHTML: null
})
}
```
## Use child property conditionally
If you wish to use a child property conditionally and use the parents' property as default value, make sure the child returns `undefined` as content value
:::tip Content value
With content value we mean the following value of a `metaInfo` property:
- the value of a key for `object` types as [`htmlAttrs`](/api/#htmlattrs)
- the value of `[contentKeyName]` or `innerHTML` keys for `collection` types as [`meta`](/api/#meta)
:::
The below example will still show a description when the GET in the child fails
```js
// parent
metaInfo: {
meta: [{
vmid: 'description',
name: 'description',
content: 'my standard description',
}]
}
// child
metaInfo() {
return {
meta: [{
vmid: 'description',
name: 'description',
content: this.description,
}]
}
},
data() {
return {
description: undefined
}
},
methods: {
getDescription() {
this.description = this.$axios.get()
if (!this.description) {
// if GET request failed or returned empty,
// explicitly set to undefined so the parents'
// default description is used
this.description = undefined
}
}
}
```
## Boolean attributes
`vue-meta` maintains a [list](https://github.com/nuxt/vue-meta/blob/master/src/shared/constants.js) of attributes which are Boolean attributes according to the HTML specs (and some extra). Whatever value you will pass to these attributes, they will be rendered as a Boolean attribute.<sup>*</sup>
<sup>*</sup><small>Except for the special values `undefined` and `null`, see above</small>
:::tip Note
Prior to `v2.0` any attribute key with `undefined` as value was rendered as Boolean attribute. This has been removed as bundlers often remove object properties with an `undefined` value as given `a = {}` then `a.a === undefined`
:::
+117
View File
@@ -0,0 +1,117 @@
# Server Side Rendering
If you have an isomorphic/universal web application, you'll likely want to render your metadata on the server side as well. Here's how.
## Add `vue-meta` to the context
You'll need to expose the results of the `$meta` method that `vue-meta` adds to the Vue instance to the bundle render context before you can begin injecting your metadata. You'll need to do this in your server entry file:
**server-entry.js:**
```js
import app from './app'
const router = app.$router
const meta = app.$meta() // here
export default (context) => {
router.push(context.url)
context.meta = meta // and here
return app
}
```
## Inject metadata into page string
Probably the easiest method to wrap your head around is if your Vue server markup is rendered out as a string using `renderToString`:
**server.js:**
```js
app.get('*', (req, res) => {
const context = { url: req.url }
renderer.renderToString(context, (error, html) => {
if (error) return res.send(error.stack)
const {
title, htmlAttrs, headAttrs, bodyAttrs, link,
style, script, noscript, meta
} = context.meta.inject()
return res.send(`
<!doctype html>
<html data-vue-meta-server-rendered ${htmlAttrs.text()}>
<head ${headAttrs.text()}>
${meta.text()}
${title.text()}
${link.text()}
${style.text()}
${script.text()}
${noscript.text()}
</head>
<body ${bodyAttrs.text()}>
${html}
<script src="/assets/vendor.bundle.js"></script>
<script src="/assets/client.bundle.js"></script>
${script.text({ body: true })}
</body>
</html>
`)
})
})
```
If you are using a separate template file, edit your head tag with
```html
<head>
{{{ meta.inject().title.text() }}}
{{{ meta.inject().meta.text() }}}
</head>
```
Notice the use of `{{{` to avoid double escaping. Be extremely cautious when you use `{{{` with [`__dangerouslyDisableSanitizersByTagID`](/api/#dangerouslydisablesanitizersbytagid).
## Inject metadata into page stream
A little more complex, but well worth it, is to instead stream your response. `vue-meta` supports streaming with no effort (on it's part :stuck_out_tongue_winking_eye:) thanks to Vue's clever `bundleRenderer` context injection:
**server.js**
```js
app.get('*', (req, res) => {
const context = { url: req.url }
const renderStream = renderer.renderToStream(context)
renderStream.once('data', () => {
const {
title, htmlAttrs, headAttrs, bodyAttrs, link,
style, script, noscript, meta
} = context.meta.inject()
res.write(`
<!doctype html>
<html data-vue-meta-server-rendered ${htmlAttrs.text()}>
<head ${headAttrs.text()}>
${meta.text()}
${title.text()}
${link.text()}
${style.text()}
${script.text()}
${noscript.text()}
</head>
<body ${bodyAttrs.text()}>
`)
})
renderStream.on('data', (chunk) => {
res.write(chunk)
})
renderStream.on('end', () => {
res.end(`
<script src="/assets/vendor.bundle.js"></script>
<script src="/assets/client.bundle.js"></script>
${script.text({ body: true })}
</body>
</html>
`)
})
renderStream.on('error', (error) => {
res.status(500).end(`<pre>${error.stack}</pre>`)
})
})
```
+4
View File
@@ -0,0 +1,4 @@
{
"presets": [["@babel/preset-env", { targets: { node: "current" } }]],
"plugins": ["dynamic-import-node"]
}
+42
View File
@@ -0,0 +1,42 @@
# Vue Meta Examples
## Prepare examples
To prepare the examples to run locally, please follow these steps:
```bash
git clone https://github.com/nuxt/vue-meta
cd examples
yarn install
```
## Run the examples
When the examples are installed locally, start the example server as follows
```js
yarn start
// or
HOST=0.0.0.0 PORT=8080 yarn start
```
and browse to `http://localhost:3000` or whatever you changed the host and port to
### SSR Example
The server side rendering example is available on the cli only, to run the SSR example just run
```bash
yarn ssr
```
## Developing
If you would like to use the examples while developing or debugging `vue-meta` features or issues, please do as follows
```js
git clone https://github.com/nuxt/vue-meta
yarn install
cd examples
yarn install
yarn dev
```
+8 -3
View File
@@ -5,11 +5,16 @@ Vue.use(VueMeta)
Vue.component('child', {
name: 'Child',
props: ['page'],
render (h) {
props: {
page: {
type: String,
default: ''
}
},
render(h) {
return h('h3', null, this.page)
},
metaInfo () {
metaInfo() {
return {
title: this.page
}
+8 -8
View File
@@ -11,6 +11,14 @@ Vue.component('foo', {
})
new Vue({
data() {
return { showFoo: false }
},
methods: {
show() {
this.showFoo = !this.showFoo
}
},
template: `
<div id="app">
<h1>Kept alive foo</h1>
@@ -20,14 +28,6 @@ new Vue({
</keep-alive>
</div>
`,
data () {
return { showFoo: false }
},
methods: {
show () {
this.showFoo = !this.showFoo
}
},
metaInfo: () => ({
title: 'Keep-alive'
})
+46
View File
@@ -0,0 +1,46 @@
{
"name": "vue-meta-examples",
"version": "1.0.0",
"description": "Examples for vue-meta",
"main": "server.js",
"private": true,
"scripts": {
"dev": "cross-env NODE_ENV=development babel-node server.js",
"start": "babel-node server.js",
"ssr": "babel-node ssr"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nuxt/vue-meta.git"
},
"author": "",
"license": "MIT",
"bugs": {
"url": "https://github.com/nuxt/vue-meta/issues"
},
"homepage": "https://github.com/nuxt/vue-meta#readme",
"devDependencies": {
"@babel/core": "^7.3.3",
"@babel/node": "^7.2.2",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-env": "^7.3.1",
"babel-loader": "^8.0.5",
"babel-plugin-dynamic-import-node": "^2.2.0",
"consola": "^2.5.6",
"cross-env": "^5.2.0",
"express": "^4.16.4",
"express-urlrewrite": "^1.2.0",
"fs-extra": "^7.0.1",
"lodash": "^4.17.11",
"vue": "^2.6.6",
"vue-loader": "^15.6.4",
"vue-meta": "^1.5.8",
"vue-router": "^3.0.2",
"vue-server-renderer": "^2.6.8",
"vue-template-compiler": "^2.6.6",
"vuex": "^3.1.0",
"webpack": "^4.29.5",
"webpack-dev-server": "^3.2.0",
"webpackbar": "^3.1.5"
}
}
+13 -8
View File
@@ -1,5 +1,6 @@
import fs from 'fs'
import path from 'path'
import consola from 'consola'
import express from 'express'
import rewrite from 'express-urlrewrite'
import webpack from 'webpack'
@@ -16,15 +17,19 @@ app.use(webpackDevMiddleware(webpack(WebpackConfig), {
}
}))
fs.readdirSync(__dirname).forEach(file => {
if (fs.statSync(path.join(__dirname, file)).isDirectory()) {
app.use(rewrite('/' + file + '/*', '/' + file + '/index.html'))
}
})
fs.readdirSync(__dirname)
.filter(file => file !== 'ssr')
.forEach((file) => {
if (fs.statSync(path.join(__dirname, file)).isDirectory()) {
app.use(rewrite('/' + file + '/*', '/' + file + '/index.html'))
}
})
app.use(express.static(__dirname))
const port = process.env.PORT || 8080
module.exports = app.listen(port, () => {
console.log(`Server listening on http://localhost:${port}, Ctrl+C to stop`)
const host = process.env.HOST || 'localhost'
const port = process.env.PORT || 3000
module.exports = app.listen(port, host, () => {
consola.info(`Server listening on http://${host}:${port}, Ctrl+C to stop`)
})
-52
View File
@@ -1,52 +0,0 @@
const Vue = require('vue')
const renderer = require('vue-server-renderer').createRenderer()
const VueMeta = require('../')
Vue.use(VueMeta, {
tagIDKeyName: 'hid'
})
const vm = new Vue({
template: '<hello/>',
metaInfo: {
title: 'Hello',
htmlAttrs: { amp: undefined },
meta: [
{ hid: 'description', name: 'description', content: 'Hello World' }
],
script: [
{ innerHTML: '{ "@context": "http://www.schema.org", "@type": "Organization" }', type: 'application/ld+json' },
{ innerHTML: '{ "body": "yes" }', body: true, type: 'application/ld+json' }
],
__dangerouslyDisableSanitizers: ['script']
},
components: {
Hello: {
template: '<p>Hello</p>',
data () {
return { msg: 'Hello' }
},
metaInfo () {
return {
title: `<b>${this.msg}</b>`,
meta: [
{ hid: 'description', name: 'description', content: this.msg }
]
}
},
created () {
this.msg = 'Hi!'
}
}
}
})
renderer.renderToString(vm, function (err, html) {
if (err) throw err
const $meta = vm.$meta().inject()
console.log('Title:\n' + $meta.title.text())
console.log('\nHTML attrs:\n' + $meta.htmlAttrs.text())
console.log('\nMeta:\n' + $meta.meta.text())
console.log('\nHead Script:\n' + $meta.script.text())
console.log('\nBody Script:\n' + $meta.script.text({ body: true }))
})
-44
View File
@@ -1,44 +0,0 @@
const Vue = require('vue')
const renderer = require('vue-server-renderer').createRenderer()
const VueMeta = require('../')
Vue.use(VueMeta, {
tagIDKeyName: 'hid'
})
const vm = new Vue({
template: '<hello/>',
metaInfo: {
title: 'Hello',
htmlAttrs: { amp: undefined },
meta: [
{ hid: 'description', name: 'description', content: 'Hello World' }
],
script: [
{ hid: 'schema', innerHTML: '{ "@context": "http://www.schema.org", "@type": "Organization" }', type: 'application/ld+json' },
{ innerHTML: '{ "body": "yes" }', body: true, type: 'application/ld+json' }
],
__dangerouslyDisableSanitizersByTagID: { schema: ['innerHTML'] }
},
components: {
Hello: {
template: '<p>Hello</p>',
metaInfo: {
title: 'Coucou',
meta: [
{ hid: 'description', name: 'description', content: 'Coucou' }
]
}
}
}
})
renderer.renderToString(vm, function (err, html) {
if (err) throw err
const $meta = vm.$meta().inject()
console.log('Title:\n' + $meta.title.text())
console.log('\nHTML attrs:\n' + $meta.htmlAttrs.text())
console.log('\nMeta:\n' + $meta.meta.text())
console.log('\nHead Script:\n' + $meta.script.text())
console.log('\nBody Script:\n' + $meta.script.text({ body: true }))
})
+56
View File
@@ -0,0 +1,56 @@
import Vue from 'vue'
// import VueMeta from 'vue-meta'
export default async function createApp() {
// the dynamic import is for this example only
const vueMetaModule = process.env.NODE_ENV === 'development' ? '../../' : 'vue-meta'
const VueMeta = await import(vueMetaModule).then(m => m.default || m)
Vue.use(VueMeta, {
tagIDKeyName: 'hid'
})
return new Vue({
components: {
Hello: {
template: '<p>Hello</p>',
metaInfo: {
title: 'Coucou',
meta: [
{
hid: 'description',
name: 'description',
content: 'Coucou'
}
]
}
}
},
template: '<hello/>',
metaInfo: {
title: 'Hello',
htmlAttrs: { amp: true },
meta: [
{
hid: 'description',
name: 'description',
content: 'Hello World'
}
],
script: [
{
hid: 'ldjson-schema',
type: 'application/ld+json',
innerHTML: '{ "@context": "http://www.schema.org", "@type": "Organization" }'
}, {
type: 'application/ld+json',
innerHTML: '{ "body": "yes" }',
body: true
}
],
__dangerouslyDisableSanitizersByTagID: {
'ldjson-schema': ['innerHTML']
}
}
})
}
+17
View File
@@ -0,0 +1,17 @@
<!doctype html>
<html data-vue-meta-server-rendered {{ htmlAttrs.text() }}>
<head {{ headAttrs.text() }}>
{{ meta.text() }}
{{ title.text() }}
{{ link.text() }}
{{ style.text() }}
{{ webpackAssets }}
{{ script.text() }}
{{ noscript.text() }}
</head>
<body {{ bodyAttrs.text() }}>
{{ app }}
{{ script.text({ body: true }) }}
{{ noscript.text({ body: true }) }}
</body>
</html>
+3
View File
@@ -0,0 +1,3 @@
import createApp from './app'
createApp().$mount('#app')
+36
View File
@@ -0,0 +1,36 @@
import path from 'path'
import fs from 'fs-extra'
import template from 'lodash/template'
import { createRenderer } from 'vue-server-renderer'
import consola from 'consola'
import createApp from './server-entry'
const renderer = createRenderer()
async function createPage() {
const templateFile = path.resolve(__dirname, 'app.template.html')
const templateContent = await fs.readFile(templateFile, { encoding: 'utf8' })
// see: https://lodash.com/docs#template
const compiled = template(templateContent, { interpolate: /{{([\s\S]+?)}}/g })
const webpackAssets = '<link rel="stylesheet" href="../global.css">'
const serverApp = await createApp()
const appHtml = await renderer.renderToString(serverApp)
const pageHtml = compiled({
app: appHtml,
webpackAssets,
...serverApp.$meta().inject()
})
return pageHtml
}
consola.info(`Creating ssr page`)
createPage()
.then((pageHtml) => {
consola.info(`Done, page:`)
consola.log(pageHtml)
})
.catch(e => consola.error(e))
+3
View File
@@ -0,0 +1,3 @@
import createApp from './app'
export default createApp
+29 -9
View File
@@ -1,29 +1,48 @@
import assign from 'object-assign'
import Vue from 'vue'
import VueMeta from 'vue-meta'
import Router from 'vue-router'
Vue.use(Router)
Vue.use(VueMeta)
Vue.use(VueMeta, {
refreshOnceOnNavigation: true
})
let metaUpdated = 'no'
const ChildComponent = {
name: `child-component`,
props: ['page'],
template: `<h3>You're looking at the <strong>{{ page }}</strong> page</h3>`,
metaInfo () {
template: `<div>
<h3>You're looking at the <strong>{{ page }}</strong> page</h3>
<p>Has metaInfo been updated? {{ metaUpdated }}</p>
</div>`,
metaInfo() {
return {
title: this.page
title: `${this.page} - ${this.date && this.date.toTimeString()}`,
afterNavigation() {
metaUpdated = 'yes'
}
}
},
data() {
return {
date: null,
metaUpdated
}
},
mounted() {
setInterval(() => {
this.date = new Date()
}, 1000)
}
}
// this wrapper function is not a requirement for vue-router,
// just a demonstration that render-function style components also work.
// See https://github.com/declandewet/vue-meta/issues/9 for more info.
function view (page) {
// See https://github.com/nuxt/vue-meta/issues/9 for more info.
function view(page) {
return {
name: `section-${page}`,
render (h) {
render(h) {
return h(ChildComponent, {
props: { page }
})
@@ -41,6 +60,7 @@ const router = new Router({
})
const App = {
router,
template: `
<div id="app">
<h1>vue-router</h1>
@@ -54,6 +74,6 @@ const App = {
`
}
const app = new Vue(assign(App, { router }))
const app = new Vue(App)
app.$mount('#app')
+3 -3
View File
@@ -1,9 +1,9 @@
import assign from 'object-assign'
import Vue from 'vue'
import store from './store'
import router from './router'
import App from './App.vue'
const app = new Vue(assign(App, { router, store }))
App.router = router
App.store = store
app.$mount('#app')
new Vue(App).$mount('#app')
+9 -9
View File
@@ -35,33 +35,33 @@ export default new Vuex.Store({
// GETTERS
getters: {
isLoading (state) {
isLoading(state) {
return state.isLoading
},
post (state) {
post(state) {
return state.post
},
publishedPosts (state) {
return state.posts.filter((post) => post.published)
publishedPosts(state) {
return state.posts.filter(post => post.published)
},
publishedPostsCount (state, getters) {
publishedPostsCount(state, getters) {
return getters.publishedPosts.length
}
},
// MUTATIONS
mutations: {
loadingState (state, { isLoading }) {
loadingState(state, { isLoading }) {
state.isLoading = isLoading
},
getPost (state, { slug }) {
state.post = state.posts.find((post) => post.slug === slug)
getPost(state, { slug }) {
state.post = state.posts.find(post => post.slug === slug)
}
},
// ACTIONS
actions: {
getPost ({ commit }, payload) {
getPost({ commit }, payload) {
commit('loadingState', { isLoading: true })
setTimeout(() => {
commit('getPost', payload)
+3 -3
View File
@@ -1,9 +1,9 @@
import assign from 'object-assign'
import Vue from 'vue'
import store from './store'
import router from './router'
import App from './App.vue'
const app = new Vue(assign(App, { router, store }))
App.router = router
App.store = store
app.$mount('#app')
new Vue(App).$mount('#app')
+3 -2
View File
@@ -1,12 +1,13 @@
import Vue from 'vue'
import Router from 'vue-router'
import Meta from 'vue-meta'
import Home from './views/Home.vue'
import Post from './views/Post.vue'
Vue.use(Router)
Vue.use(Meta)
const Home = () => import('./views/Home.vue')
const Post = () => import('./views/Post.vue')
export default new Router({
mode: 'history',
base: '/vuex',
+7 -7
View File
@@ -35,27 +35,27 @@ export default new Vuex.Store({
// GETTERS
getters: {
post (state) {
post(state) {
return state.post
},
publishedPosts (state) {
return state.posts.filter((post) => post.published)
publishedPosts(state) {
return state.posts.filter(post => post.published)
},
publishedPostsCount (state, getters) {
publishedPostsCount(state, getters) {
return getters.publishedPosts.length
}
},
// MUTATIONS
mutations: {
getPost (state, { slug }) {
state.post = state.posts.find((post) => post.slug === slug)
getPost(state, { slug }) {
state.post = state.posts.find(post => post.slug === slug)
}
},
// ACTIONS
actions: {
getPost ({ commit }, payload) {
getPost({ commit }, payload) {
commit('getPost', payload)
}
}
+15 -10
View File
@@ -1,18 +1,22 @@
import fs from 'fs'
import path from 'path'
import webpack from 'webpack'
import WebpackBar from 'webpackbar'
import VueLoaderPlugin from 'vue-loader/lib/plugin'
export default {
devtool: 'inline-source-map',
mode: 'development',
entry: fs.readdirSync(__dirname).reduce((entries, dir) => {
const fullDir = path.join(__dirname, dir)
const entry = path.join(fullDir, 'app.js')
if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) {
entries[dir] = entry
}
return entries
}, {}),
entry: fs.readdirSync(__dirname)
.filter(entry => entry !== 'ssr')
.reduce((entries, dir) => {
const fullDir = path.join(__dirname, dir)
const entry = path.join(fullDir, 'app.js')
if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) {
entries[dir] = entry
}
return entries
}, {}),
output: {
path: path.join(__dirname, '__build__'),
filename: '[name].js',
@@ -28,7 +32,7 @@ export default {
resolve: {
alias: {
'vue': 'vue/dist/vue.js',
'vue-meta': path.join(__dirname, '..', 'src')
'vue-meta': process.env.NODE_ENV === 'development' ? path.join(__dirname, '..', 'src') : 'vue-meta'
}
},
// Expose __dirname to allow automatically setting basename.
@@ -37,7 +41,8 @@ export default {
__dirname: true
},
plugins: [
// new webpack.optimize.CommonsChunkPlugin('shared.js'),
new WebpackBar(),
new VueLoaderPlugin(),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
})
+42
View File
@@ -0,0 +1,42 @@
module.exports = {
testEnvironment: 'jest-environment-jsdom-global',
expand: true,
forceExit: false,
// https://github.com/facebook/jest/pull/6747 fix warning here
// But its performance overhead is pretty bad (30+%).
// detectOpenHandles: true
setupFilesAfterEnv: ['./test/utils/setup'],
coverageDirectory: './coverage',
collectCoverageFrom: [
'**/src/**/*.js'
],
coveragePathIgnorePatterns: [
'node_modules'
],
testPathIgnorePatterns: [
'node_modules'
],
transformIgnorePatterns: [
'node_modules'
],
transform: {
'^.+\\.js$': 'babel-jest',
'.*\\.(vue)$': 'vue-jest'
},
moduleFileExtensions: [
'ts',
'js',
'json'
]
}
-33
View File
@@ -1,33 +0,0 @@
import webpackConfig from './examples/webpack.config.babel'
delete webpackConfig.entry
export default (config) => {
config.set({
browsers: ['PhantomJS'],
frameworks: ['mocha', 'chai'],
reporters: ['mocha', 'coverage'],
files: ['test/index.js'],
preprocessors: {
'test/index.js': ['webpack', 'sourcemap']
},
coverageReporter: {
reporters: [
{ type: 'lcov' },
{ type: 'text' }
],
includeAllSources: true,
dir: 'coverage',
subdir: '.'
},
webpack: webpackConfig,
webpackMiddleware: {
noInfo: true
},
mochaReporter: {
showDiff: true,
output: 'full'
},
singleRun: true
})
}
-2
View File
@@ -1,2 +0,0 @@
require('@babel/register')
module.exports = require('./karma.conf.babel').default
+106 -103
View File
@@ -1,93 +1,14 @@
{
"name": "vue-meta",
"description": "manage page meta info in Vue 2.0 server-rendered apps",
"version": "1.6.0",
"author": "Declan de Wet <declandewet@me.com>",
"bugs": "https://github.com/declandewet/vue-meta/issues",
"scripts": {
"build": "rollup -c",
"codecov": "codecov",
"deploy": "npm version",
"dev": "babel-node examples/server.js",
"lint": "standard --verbose | snazzy",
"minify": "uglifyjs lib/vue-meta.js -cm --comments -o lib/vue-meta.min.js",
"postbuild": "npm run minify",
"postdeploy": "git push origin master --follow-tags && npm run release",
"postversion": "npm run update-cdn && git add . && git commit -m \":ship: CDN update\"",
"prebuild": "rimraf lib",
"predeploy": "git checkout master && git pull -r",
"prerelease": "npm run build",
"pretest": "npm run lint",
"preversion": "npm run toc",
"release": "npm publish",
"test": "cross-env NODE_ENV=test karma start karma.conf.js",
"toc": "doctoc README.md --title '# Table of Contents'",
"update-cdn": "babel-node scripts/update-cdn.js"
},
"dependencies": {
"deepmerge": "^3.2.0",
"lodash.isplainobject": "^4.0.6",
"lodash.uniqueid": "^4.0.1",
"object-assign": "^4.1.1"
},
"devDependencies": {
"@babel/cli": "^7.2.3",
"@babel/core": "^7.4.0",
"@babel/node": "^7.2.2",
"@babel/preset-env": "^7.4.2",
"@babel/register": "^7.4.0",
"babel-loader": "^8.0.5",
"babel-plugin-istanbul": "^5.1.1",
"chai": "^4.2.0",
"codecov": "^3.2.0",
"cross-env": "^5.2.0",
"css-loader": "^2.1.1",
"doctoc": "^1.4.0",
"es6-promise": "^4.2.6",
"express": "^4.16.4",
"express-urlrewrite": "^1.2.0",
"file-loader": "^3.0.1",
"karma": "^4.0.1",
"karma-chai": "^0.1.0",
"karma-coverage": "^1.1.2",
"karma-mocha": "^1.3.0",
"karma-mocha-reporter": "^2.2.5",
"karma-phantomjs-launcher": "^1.0.4",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^3.0.5",
"mocha": "^6.0.2",
"phantomjs-prebuilt": "^2.1.16",
"rimraf": "^2.6.3",
"rollup": "^1.7.4",
"rollup-plugin-buble": "^0.19.6",
"rollup-plugin-commonjs": "^9.2.2",
"rollup-plugin-json": "^4.0.0",
"rollup-plugin-node-resolve": "^4.0.1",
"snazzy": "^8.0.0",
"standard": "^12.0.1",
"uglify-js": "^3.5.2",
"update-section": "^0.3.3",
"vue": "^2.6.10",
"vue-loader": "^15.7.0",
"vue-router": "^3.0.2",
"vue-server-renderer": "^2.6.10",
"vue-template-compiler": "^2.6.10",
"vuex": "^3.1.0",
"webpack": "^4.29.6",
"webpack-dev-server": "^3.2.1"
},
"files": [
"lib",
"types/index.d.ts",
"types/vue.d.ts"
],
"homepage": "https://github.com/declandewet/vue-meta",
"description": "Manage page metadata in Vue.js components with ssr support",
"keywords": [
"attribute",
"google",
"head",
"helmet",
"info",
"metadata",
"meta",
"seo",
"server",
@@ -96,29 +17,111 @@
"universal",
"vue"
],
"license": "MIT",
"main": "lib/vue-meta.js",
"typings": "types/index.d.ts",
"nyc": {
"exclude": [
"test/**/*.js"
]
},
"homepage": "https://github.com/nuxt/vue-meta",
"bugs": "https://github.com/nuxt/vue-meta/issues",
"repository": {
"url": "git@github.com:declandewet/vue-meta.git",
"type": "git"
"type": "git",
"url": "git@github.com/nuxt/vue-meta.git"
},
"standard": {
"globals": [
"Vue",
"define",
"describe",
"it",
"expect",
"before",
"beforeEach",
"after",
"afterEach"
]
"license": "MIT",
"contributors": [
{
"name": "Declan de Wet (@declandewet)"
},
{
"name": "Sebastien Chopin (@Atinux)"
}
],
"files": [
"lib",
"es",
"types/*.d.ts"
],
"main": "lib/vue-meta.common.js",
"web": "lib/vue-meta.js",
"module": "es/index.js",
"typings": "types/index.d.ts",
"scripts": {
"build": "yarn build:other && yarn build:es",
"build:es": "rimraf es && babel src --env-name es --out-dir es",
"build:other": "rimraf lib && rollup -c scripts/rollup.config.js",
"coverage": "codecov",
"dev": "cd examples && yarn dev && cd ..",
"docs": "vuepress dev --host 0.0.0.0 --port 3000 docs",
"docs:build": "vuepress build docs",
"lint": "eslint src test",
"prerelease": "git checkout master && git pull -r",
"release": "yarn lint && yarn test && yarn build && standard-version",
"postrelease": "git push origin master --follow-tags && yarn publish",
"test": "yarn test:unit && yarn test:e2e-ssr && yarn test:e2e-browser",
"test:e2e-ssr": "jest test/e2e/ssr",
"test:e2e-browser": "jest test/e2e/browser",
"test:unit": "jest test/unit",
"test:types": "tsc -p types/test"
},
"dependencies": {
"deepmerge": "^3.2.0"
},
"resolutions": {
"webpack-dev-middleware": "3.6.0"
},
"devDependencies": {
"@babel/cli": "^7.4.3",
"@babel/core": "^7.4.3",
"@babel/node": "^7.2.2",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-env": "^7.4.3",
"@nuxt/babel-preset-app": "^2.6.2",
"@nuxtjs/eslint-config": "^0.0.1",
"@vue/server-test-utils": "^1.0.0-beta.29",
"@vue/test-utils": "^1.0.0-beta.29",
"babel-core": "^7.0.0-bridge",
"babel-eslint": "^10.0.1",
"babel-jest": "^24.7.1",
"babel-loader": "^8.0.5",
"babel-plugin-dynamic-import-node": "^2.2.0",
"browserstack-local": "^1.3.7",
"chromedriver": "^73.0.0",
"codecov": "^3.3.0",
"eslint": "^5.16.0",
"eslint-config-standard": "^12.0.0",
"eslint-plugin-import": "^2.17.2",
"eslint-plugin-jest": "^22.4.1",
"eslint-plugin-node": "^8.0.1",
"eslint-plugin-promise": "^4.1.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^5.2.2",
"esm": "^3.2.22",
"fs-extra": "^7.0.1",
"geckodriver": "^1.16.2",
"is-wsl": "^1.1.0",
"jest": "^24.7.1",
"jest-environment-jsdom": "^24.7.1",
"jest-environment-jsdom-global": "^1.2.0",
"jsdom": "^14.0.0",
"lodash": "^4.17.11",
"node-env-file": "^0.1.8",
"puppeteer-core": "^1.14.0",
"rimraf": "^2.6.3",
"rollup": "^1.10.1",
"rollup-plugin-buble": "^0.19.6",
"rollup-plugin-commonjs": "^9.3.4",
"rollup-plugin-json": "^4.0.0",
"rollup-plugin-node-resolve": "^4.2.3",
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-terser": "^4.0.4",
"selenium-webdriver": "^4.0.0-alpha.1",
"standard-version": "^5.0.2",
"tib": "^0.5.1",
"typescript": "^3.4.4",
"vue": "^2.6.10",
"vue-jest": "^3.0.4",
"vue-loader": "^15.7.0",
"vue-router": "^3.0.6",
"vue-server-renderer": "^2.6.10",
"vue-template-compiler": "^2.6.10",
"vuepress": "^0.14.11",
"vuepress-theme-vue": "^1.1.0",
"webpack": "^4.30.0"
}
}
-27
View File
@@ -1,27 +0,0 @@
import commonjs from 'rollup-plugin-commonjs'
import nodeResolve from 'rollup-plugin-node-resolve'
import json from 'rollup-plugin-json'
import buble from 'rollup-plugin-buble'
const pkg = require('./package.json')
export default {
input: './src/index.js',
output: {
file: pkg.main,
format: 'umd',
name: 'VueMeta',
banner: `/**
* vue-meta v${pkg.version}
* (c) ${new Date().getFullYear()} Declan de Wet & Sébastien Chopin (@Atinux)
* @license MIT
*/
`.replace(/ {4}/gm, '').trim()
},
plugins: [
json(),
nodeResolve({ jsnext: true }),
commonjs(),
buble()
]
}
+88
View File
@@ -0,0 +1,88 @@
import commonjs from 'rollup-plugin-commonjs'
import nodeResolve from 'rollup-plugin-node-resolve'
import json from 'rollup-plugin-json'
import buble from 'rollup-plugin-buble'
import replace from 'rollup-plugin-replace'
import { terser } from 'rollup-plugin-terser'
import defaultsDeep from 'lodash/defaultsDeep'
const pkg = require('../package.json')
const banner = `/**
* vue-meta v${pkg.version}
* (c) ${new Date().getFullYear()}
* - Declan de Wet
* - Sébastien Chopin (@Atinux)
* - All the amazing contributors
* @license MIT
*/
`
function rollupConfig({
plugins = [],
...config
}) {
const replaceConfig = {
exclude: 'node_modules/**',
delimiters: ['', ''],
values: {
// replaceConfig needs to have some values
'const polyfill = process.env.NODE_ENV === \'test\'': 'const polyfill = false',
}
}
if (!config.output.format || config.output.format === 'umd') {
replaceConfig.values = {
'const polyfill = process.env.NODE_ENV === \'test\'': 'const polyfill = true',
}
}
return defaultsDeep({}, config, {
input: 'src/browser.js',
output: {
name: 'VueMeta',
format: 'umd',
sourcemap: false,
banner
},
plugins: [
json(),
nodeResolve(),
replace(replaceConfig)
].concat(plugins),
})
}
export default [
rollupConfig({
output: {
file: pkg.web,
},
plugins: [
commonjs(),
buble()
]
}),
rollupConfig({
output: {
file: pkg.web.replace('.js', '.min.js'),
},
plugins: [
commonjs(),
buble(),
terser()
]
}),
rollupConfig({
input: 'src/index.js',
output: {
file: pkg.main,
format: 'cjs'
},
plugins: [
commonjs()
],
external: Object.keys(pkg.dependencies)
})
]
+30
View File
@@ -0,0 +1,30 @@
import { version } from '../package.json'
import createMixin from './shared/mixin'
import { setOptions } from './shared/options'
import { isUndefined } from './utils/is-type'
import $meta from './client/$meta'
import { hasMetaInfo } from './shared/meta-helpers'
/**
* Plugin install function.
* @param {Function} Vue - the Vue constructor.
*/
function install(Vue, options = {}) {
options = setOptions(options)
Vue.prototype.$meta = $meta(options)
Vue.mixin(createMixin(Vue, options))
}
// automatic install
if (!isUndefined(window) && !isUndefined(window.Vue)) {
/* istanbul ignore next */
install(window.Vue)
}
export default {
version,
install,
hasMetaInfo
}
+23
View File
@@ -0,0 +1,23 @@
import { getOptions } from '../shared/options'
import { pause, resume } from '../shared/pausing'
import refresh from './refresh'
export default function _$meta(options = {}) {
const _refresh = refresh(options)
const inject = () => {}
/**
* Returns an injector for server-side rendering.
* @this {Object} - the Vue instance (a root component)
* @return {Object} - injector
*/
return function $meta() {
return {
getOptions: () => getOptions(options),
refresh: _refresh.bind(this),
inject,
pause: pause.bind(this),
resume: resume.bind(this)
}
}
}
+6 -3
View File
@@ -1,6 +1,8 @@
import { hasGlobalWindow } from '../utils/window'
// fallback to timers if rAF not present
const stopUpdate = (typeof window !== 'undefined' ? window.cancelAnimationFrame : null) || clearTimeout
const startUpdate = (typeof window !== 'undefined' ? window.requestAnimationFrame : null) || ((cb) => setTimeout(cb, 0))
const stopUpdate = (hasGlobalWindow ? window.cancelAnimationFrame : null) || clearTimeout
const startUpdate = (hasGlobalWindow ? window.requestAnimationFrame : null) || (cb => setTimeout(cb, 0))
/**
* Performs a batched update. Uses requestAnimationFrame to prevent
@@ -12,8 +14,9 @@ const startUpdate = (typeof window !== 'undefined' ? window.requestAnimationFram
* @param {Function} callback - the update to perform
* @return {Number} id - a new ID
*/
export default function batchUpdate (id, callback) {
export default function batchUpdate(id, callback) {
stopUpdate(id)
return startUpdate(() => {
id = null
callback()
+13 -5
View File
@@ -1,7 +1,9 @@
import getMetaInfo from '../shared/getMetaInfo'
import { isFunction } from '../utils/is-type'
import { clientSequences } from '../shared/escaping'
import updateClientMetaInfo from './updateClientMetaInfo'
export default function _refresh (options = {}) {
export default function _refresh(options = {}) {
/**
* When called, will update the current meta info with new meta info.
* Useful when updating meta info as the result of an asynchronous
@@ -12,9 +14,15 @@ export default function _refresh (options = {}) {
*
* @return {Object} - new meta info
*/
return function refresh () {
const info = getMetaInfo(options)(this.$root)
updateClientMetaInfo(options).call(this, info)
return info
return function refresh() {
const metaInfo = getMetaInfo(options, this.$root, clientSequences)
const tags = updateClientMetaInfo(options, metaInfo)
// emit "event" with new info
if (tags && isFunction(metaInfo.changed)) {
metaInfo.changed(metaInfo, tags.addedTags, tags.removedTags)
}
return { vm: this, metaInfo, tags }
}
}
+14
View File
@@ -0,0 +1,14 @@
import batchUpdate from './batchUpdate'
// store an id to keep track of DOM updates
let batchId = null
export default function triggerUpdate(vm, hookName) {
if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.paused) {
// batch potential DOM updates to prevent extraneous re-rendering
batchId = batchUpdate(batchId, () => {
vm.$meta().refresh()
batchId = null
})
}
}
+68 -56
View File
@@ -1,64 +1,76 @@
import updateTitle from './updaters/updateTitle'
import updateTagAttributes from './updaters/updateTagAttributes'
import updateTags from './updaters/updateTags'
import { metaInfoOptionKeys, metaInfoAttributeKeys } from '../shared/constants'
import { isArray } from '../utils/is-type'
import { includes } from '../utils/array'
import { updateAttribute, updateTag, updateTitle } from './updaters'
export default function _updateClientMetaInfo (options = {}) {
function getTag(tags, tag) {
if (!tags[tag]) {
tags[tag] = document.getElementsByTagName(tag)[0]
}
return tags[tag]
}
/**
* Performs client-side updates when new meta info is received
*
* @param {Object} newInfo - the meta info to update to
*/
export default function updateClientMetaInfo(options = {}, newInfo) {
const { ssrAttribute } = options
/**
* Performs client-side updates when new meta info is received
*
* @param {Object} newInfo - the meta info to update to
*/
return function updateClientMetaInfo (newInfo) {
const htmlTag = document.getElementsByTagName('html')[0]
// if this is not a server render, then update
if (htmlTag.getAttribute(ssrAttribute) === null) {
// initialize tracked changes
const addedTags = {}
const removedTags = {}
// only cache tags for current update
const tags = {}
Object.keys(newInfo).forEach((key) => {
switch (key) {
// update the title
case 'title':
updateTitle(options)(newInfo.title)
break
// update attributes
case 'htmlAttrs':
updateTagAttributes(options)(newInfo[key], htmlTag)
break
case 'bodyAttrs':
updateTagAttributes(options)(newInfo[key], document.getElementsByTagName('body')[0])
break
case 'headAttrs':
updateTagAttributes(options)(newInfo[key], document.getElementsByTagName('head')[0])
break
// ignore these
case 'titleChunk':
case 'titleTemplate':
case 'changed':
case '__dangerouslyDisableSanitizers':
break
// catch-all update tags
default:
const headTag = document.getElementsByTagName('head')[0]
const bodyTag = document.getElementsByTagName('body')[0]
const { oldTags, newTags } = updateTags(options)(key, newInfo[key], headTag, bodyTag)
if (newTags.length) {
addedTags[key] = newTags
removedTags[key] = oldTags
}
}
})
const htmlTag = getTag(tags, 'html')
// emit "event" with new info
if (typeof newInfo.changed === 'function') {
newInfo.changed.call(this, newInfo, addedTags, removedTags)
}
} else {
// remove the server render attribute so we can update on changes
htmlTag.removeAttribute(ssrAttribute)
// if this is a server render, then dont update
if (htmlTag.hasAttribute(ssrAttribute)) {
// remove the server render attribute so we can update on (next) changes
htmlTag.removeAttribute(ssrAttribute)
return false
}
// initialize tracked changes
const addedTags = {}
const removedTags = {}
for (const type in newInfo) {
// ignore these
if (includes(metaInfoOptionKeys, type)) {
continue
}
if (type === 'title') {
// update the title
updateTitle(newInfo.title)
continue
}
if (includes(metaInfoAttributeKeys, type)) {
const tagName = type.substr(0, 4)
updateAttribute(options, newInfo[type], getTag(tags, tagName))
continue
}
// tags should always be an array, ignore if it isnt
if (!isArray(newInfo[type])) {
continue
}
const { oldTags, newTags } = updateTag(
options,
type,
newInfo[type],
getTag(tags, 'head'),
getTag(tags, 'body')
)
if (newTags.length) {
addedTags[type] = newTags
removedTags[type] = oldTags
}
}
return { addedTags, removedTags }
}
+46
View File
@@ -0,0 +1,46 @@
import { booleanHtmlAttributes } from '../../shared/constants'
import { toArray, includes } from '../../utils/array'
import { isArray } from '../../utils/is-type'
/**
* Updates the document's html tag attributes
*
* @param {Object} attrs - the new document html attributes
* @param {HTMLElement} tag - the HTMLElement tag to update with new attrs
*/
export default function updateAttribute({ attribute } = {}, attrs, tag) {
const vueMetaAttrString = tag.getAttribute(attribute)
const vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : []
const toRemove = toArray(vueMetaAttrs)
const keepIndexes = []
for (const attr in attrs) {
if (attrs.hasOwnProperty(attr)) {
const value = includes(booleanHtmlAttributes, attr)
? ''
: isArray(attrs[attr]) ? attrs[attr].join(' ') : attrs[attr]
tag.setAttribute(attr, value || '')
if (!includes(vueMetaAttrs, attr)) {
vueMetaAttrs.push(attr)
}
// filter below wont ever check -1
keepIndexes.push(toRemove.indexOf(attr))
}
}
const removedAttributesCount = toRemove
.filter((el, index) => !includes(keepIndexes, index))
.reduce((acc, attr) => {
tag.removeAttribute(attr)
return acc + 1
}, 0)
if (vueMetaAttrs.length === removedAttributesCount) {
tag.removeAttribute(attribute)
} else {
tag.setAttribute(attribute, (vueMetaAttrs.sort()).join(','))
}
}
+3
View File
@@ -0,0 +1,3 @@
export { default as updateAttribute } from './attribute'
export { default as updateTitle } from './title'
export { default as updateTag } from './tag'
+85
View File
@@ -0,0 +1,85 @@
import { isUndefined } from '../../utils/is-type'
import { toArray, includes } from '../../utils/array'
/**
* Updates meta tags inside <head> and <body> on the client. Borrowed from `react-helmet`:
* https://github.com/nfl/react-helmet/blob/004d448f8de5f823d10f838b02317521180f34da/src/Helmet.js#L195-L245
*
* @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} type - the name of the tag
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
* @return {Object} - a representation of what tags changed
*/
export default function updateTag({ attribute, tagIDKeyName } = {}, type, tags, headTag, bodyTag) {
const oldHeadTags = toArray(headTag.querySelectorAll(`${type}[${attribute}]`))
const oldBodyTags = toArray(bodyTag.querySelectorAll(`${type}[${attribute}][data-body="true"]`))
const dataAttributes = [tagIDKeyName, 'body']
const newTags = []
if (tags.length > 1) {
// remove duplicates that could have been found by merging tags
// which include a mixin with metaInfo and that mixin is used
// by multiple components on the same page
const found = []
tags = tags.filter((x) => {
const k = JSON.stringify(x)
const res = !includes(found, k)
found.push(k)
return res
})
}
if (tags.length) {
tags.forEach((tag) => {
const newElement = document.createElement(type)
newElement.setAttribute(attribute, 'true')
const oldTags = tag.body !== true ? oldHeadTags : oldBodyTags
for (const attr in tag) {
if (tag.hasOwnProperty(attr)) {
if (attr === 'innerHTML') {
newElement.innerHTML = tag.innerHTML
} else if (attr === 'cssText') {
if (newElement.styleSheet) {
/* istanbul ignore next */
newElement.styleSheet.cssText = tag.cssText
} else {
newElement.appendChild(document.createTextNode(tag.cssText))
}
} else {
const _attr = includes(dataAttributes, attr)
? `data-${attr}`
: attr
const value = isUndefined(tag[attr]) ? '' : tag[attr]
newElement.setAttribute(_attr, value)
}
}
}
// Remove a duplicate tag from domTagstoRemove, so it isn't cleared.
let indexToDelete
const hasEqualElement = oldTags.some((existingTag, index) => {
indexToDelete = index
return newElement.isEqualNode(existingTag)
})
if (hasEqualElement && (indexToDelete || indexToDelete === 0)) {
oldTags.splice(indexToDelete, 1)
} else {
newTags.push(newElement)
}
})
}
const oldTags = oldHeadTags.concat(oldBodyTags)
oldTags.forEach(tag => tag.parentNode.removeChild(tag))
newTags.forEach((tag) => {
if (tag.getAttribute('data-body') === 'true') {
bodyTag.appendChild(tag)
} else {
headTag.appendChild(tag)
}
})
return { oldTags, newTags }
}
+8
View File
@@ -0,0 +1,8 @@
/**
* Updates the document title
*
* @param {String} title - the new title of the document
*/
export default function updateTitle(title = document.title) {
document.title = title
}
@@ -1,37 +0,0 @@
export default function _updateTagAttributes (options = {}) {
const { attribute } = options
/**
* Updates the document's html tag attributes
*
* @param {Object} attrs - the new document html attributes
* @param {HTMLElement} tag - the HTMLElement tag to update with new attrs
*/
return function updateTagAttributes (attrs, tag) {
const vueMetaAttrString = tag.getAttribute(attribute)
const vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : []
const toRemove = [].concat(vueMetaAttrs)
for (let attr in attrs) {
if (attrs.hasOwnProperty(attr)) {
const val = attrs[attr] || ''
tag.setAttribute(attr, val)
if (vueMetaAttrs.indexOf(attr) === -1) {
vueMetaAttrs.push(attr)
}
const saveIndex = toRemove.indexOf(attr)
if (saveIndex !== -1) {
toRemove.splice(saveIndex, 1)
}
}
}
let i = toRemove.length - 1
for (; i >= 0; i--) {
tag.removeAttribute(toRemove[i])
}
if (vueMetaAttrs.length === toRemove.length) {
tag.removeAttribute(attribute)
} else {
tag.setAttribute(attribute, vueMetaAttrs.join(','))
}
}
}
-86
View File
@@ -1,86 +0,0 @@
// borrow the slice method
const toArray = Function.prototype.call.bind(Array.prototype.slice)
export default function _updateTags (options = {}) {
const { attribute } = options
/**
* Updates meta tags inside <head> and <body> on the client. Borrowed from `react-helmet`:
* https://github.com/nfl/react-helmet/blob/004d448f8de5f823d10f838b02317521180f34da/src/Helmet.js#L195-L245
*
* @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} type - the name of the tag
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
* @return {Object} - a representation of what tags changed
*/
return function updateTags (type, tags, headTag, bodyTag) {
const oldHeadTags = toArray(headTag.querySelectorAll(`${type}[${attribute}]`))
const oldBodyTags = toArray(bodyTag.querySelectorAll(`${type}[${attribute}][data-body="true"]`))
const newTags = []
let indexToDelete
if (tags.length > 1) {
// remove duplicates that could have been found by merging tags
// which include a mixin with metaInfo and that mixin is used
// by multiple components on the same page
const found = []
tags = tags.map(x => {
const k = JSON.stringify(x)
if (found.indexOf(k) < 0) {
found.push(k)
return x
}
}).filter(x => x)
}
if (tags && tags.length) {
tags.forEach((tag) => {
const newElement = document.createElement(type)
const oldTags = tag.body !== true ? oldHeadTags : oldBodyTags
for (const attr in tag) {
if (tag.hasOwnProperty(attr)) {
if (attr === 'innerHTML') {
newElement.innerHTML = tag.innerHTML
} else if (attr === 'cssText') {
if (newElement.styleSheet) {
newElement.styleSheet.cssText = tag.cssText
} else {
newElement.appendChild(document.createTextNode(tag.cssText))
}
} else if ([options.tagIDKeyName, 'body'].indexOf(attr) !== -1) {
const _attr = `data-${attr}`
const value = (typeof tag[attr] === 'undefined') ? '' : tag[attr]
newElement.setAttribute(_attr, value)
} else {
const value = (typeof tag[attr] === 'undefined') ? '' : tag[attr]
newElement.setAttribute(attr, value)
}
}
}
newElement.setAttribute(attribute, 'true')
// Remove a duplicate tag from domTagstoRemove, so it isn't cleared.
if (oldTags.some((existingTag, index) => {
indexToDelete = index
return newElement.isEqualNode(existingTag)
})) {
oldTags.splice(indexToDelete, 1)
} else {
newTags.push(newElement)
}
})
}
const oldTags = oldHeadTags.concat(oldBodyTags)
oldTags.forEach((tag) => tag.parentNode.removeChild(tag))
newTags.forEach((tag) => {
if (tag.getAttribute('data-body') === 'true') {
bodyTag.appendChild(tag)
} else {
headTag.appendChild(tag)
}
})
return { oldTags, newTags }
}
}
-10
View File
@@ -1,10 +0,0 @@
export default function _updateTitle () {
/**
* Updates the document title
*
* @param {String} title - the new title of the document
*/
return function updateTitle (title = document.title) {
document.title = title
}
}
+20 -3
View File
@@ -1,6 +1,23 @@
import install from './shared/plugin'
import { version } from '../package.json'
import createMixin from './shared/mixin'
import { setOptions } from './shared/options'
import $meta from './server/$meta'
import { hasMetaInfo } from './shared/meta-helpers'
install.version = version
/**
* Plugin install function.
* @param {Function} Vue - the Vue constructor.
*/
function install(Vue, options = {}) {
options = setOptions(options)
export default install
Vue.prototype.$meta = $meta(options)
Vue.mixin(createMixin(Vue, options))
}
export default {
version,
install,
hasMetaInfo
}
+24
View File
@@ -0,0 +1,24 @@
import { getOptions } from '../shared/options'
import { pause, resume } from '../shared/pausing'
import refresh from '../client/refresh'
import inject from './inject'
export default function _$meta(options = {}) {
const _refresh = refresh(options)
const _inject = inject(options)
/**
* Returns an injector for server-side rendering.
* @this {Object} - the Vue instance (a root component)
* @return {Object} - injector
*/
return function $meta() {
return {
getOptions: () => getOptions(options),
refresh: _refresh.bind(this),
inject: _inject.bind(this),
pause: pause.bind(this),
resume: resume.bind(this)
}
}
}
+19 -22
View File
@@ -1,25 +1,22 @@
import titleGenerator from './generators/titleGenerator'
import attrsGenerator from './generators/attrsGenerator'
import tagGenerator from './generators/tagGenerator'
import { metaInfoAttributeKeys } from '../shared/constants'
import { titleGenerator, attributeGenerator, tagGenerator } from './generators'
export default function _generateServerInjector (options = {}) {
/**
* Converts a meta info property to one that can be stringified on the server
*
* @param {String} type - the type of data to convert
* @param {(String|Object|Array<Object>)} data - the data value
* @return {Object} - the new injector
*/
return function generateServerInjector (type, data) {
switch (type) {
case 'title':
return titleGenerator(options)(type, data)
case 'htmlAttrs':
case 'bodyAttrs':
case 'headAttrs':
return attrsGenerator(options)(type, data)
default:
return tagGenerator(options)(type, data)
}
/**
* Converts a meta info property to one that can be stringified on the server
*
* @param {String} type - the type of data to convert
* @param {(String|Object|Array<Object>)} data - the data value
* @return {Object} - the new injector
*/
export default function generateServerInjector(options, type, data) {
if (type === 'title') {
return titleGenerator(options, type, data)
}
if (metaInfoAttributeKeys.includes(type)) {
return attributeGenerator(options, type, data)
}
return tagGenerator(options, type, data)
}
+33
View File
@@ -0,0 +1,33 @@
import { booleanHtmlAttributes } from '../../shared/constants'
import { isUndefined, isArray } from '../../utils/is-type'
/**
* Generates tag attributes for use on the server.
*
* @param {('bodyAttrs'|'htmlAttrs'|'headAttrs')} type - the type of attributes to generate
* @param {Object} data - the attributes to generate
* @return {Object} - the attribute generator
*/
export default function attributeGenerator({ attribute } = {}, type, data) {
return {
text() {
let attributeStr = ''
const watchedAttrs = []
for (const attr in data) {
if (data.hasOwnProperty(attr)) {
watchedAttrs.push(attr)
attributeStr += isUndefined(data[attr]) || booleanHtmlAttributes.includes(attr)
? attr
: `${attr}="${isArray(data[attr]) ? data[attr].join(' ') : data[attr]}"`
attributeStr += ' '
}
}
attributeStr += `${attribute}="${(watchedAttrs.sort()).join(',')}"`
return attributeStr
}
}
}
-31
View File
@@ -1,31 +0,0 @@
export default function _attrsGenerator (options = {}) {
const { attribute } = options
/**
* Generates tag attributes for use on the server.
*
* @param {('bodyAttrs'|'htmlAttrs'|'headAttrs')} type - the type of attributes to generate
* @param {Object} data - the attributes to generate
* @return {Object} - the attribute generator
*/
return function attrsGenerator (type, data) {
return {
text () {
let attributeStr = ''
let watchedAttrs = []
for (let attr in data) {
if (data.hasOwnProperty(attr)) {
watchedAttrs.push(attr)
attributeStr += `${
typeof data[attr] !== 'undefined'
? `${attr}="${data[attr]}"`
: attr
} `
}
}
attributeStr += `${attribute}="${watchedAttrs.join(',')}"`
return attributeStr.trim()
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
export { default as attributeGenerator } from './attribute'
export { default as titleGenerator } from './title'
export { default as tagGenerator } from './tag'
+65
View File
@@ -0,0 +1,65 @@
import { booleanHtmlAttributes, tagsWithoutEndTag, tagsWithInnerContent, tagAttributeAsInnerContent } from '../../shared/constants'
import { isUndefined } from '../../utils/is-type'
/**
* Generates meta, base, link, style, script, noscript tags for use on the server
*
* @param {('meta'|'base'|'link'|'style'|'script'|'noscript')} the name of the tag
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
* @return {Object} - the tag generator
*/
export default function tagGenerator({ attribute, tagIDKeyName } = {}, type, tags) {
return {
text({ body = false } = {}) {
// build a string containing all tags of this type
return tags.reduce((tagsStr, tag) => {
const tagKeys = Object.keys(tag)
if (tagKeys.length === 0) {
return tagsStr // Bail on empty tag object
}
if (Boolean(tag.body) !== body) {
return tagsStr
}
// build a string containing all attributes of this tag
const attrs = tagKeys.reduce((attrsStr, attr) => {
// these attributes are treated as children on the tag
if (tagAttributeAsInnerContent.includes(attr) || attr === 'once') {
return attrsStr
}
// these form the attribute list for this tag
let prefix = ''
if ([tagIDKeyName, 'body'].includes(attr)) {
prefix = 'data-'
}
return isUndefined(tag[attr]) || booleanHtmlAttributes.includes(attr)
? `${attrsStr} ${prefix}${attr}`
: `${attrsStr} ${prefix}${attr}="${tag[attr]}"`
}, '')
// grab child content from one of these attributes, if possible
const content = tag.innerHTML || tag.cssText || ''
// generate tag exactly without any other redundant attribute
const observeTag = tag.once
? ''
: `${attribute}="true"`
// these tags have no end tag
const hasEndTag = !tagsWithoutEndTag.includes(type)
// these tag types will have content inserted
const hasContent = hasEndTag && tagsWithInnerContent.includes(type)
// the final string for this specific tag
return !hasContent
? `${tagsStr}<${type} ${observeTag}${attrs}${hasEndTag ? '/' : ''}>`
: `${tagsStr}<${type} ${observeTag}${attrs}>${content}</${type}>`
}, '')
}
}
}
+14
View File
@@ -0,0 +1,14 @@
/**
* Generates title output for the server
*
* @param {'title'} type - the string "title"
* @param {String} data - the title text
* @return {Object} - the title generator
*/
export default function titleGenerator({ attribute } = {}, type, data) {
return {
text() {
return `<${type} ${attribute}="true">${data}</${type}>`
}
}
}
+9 -7
View File
@@ -1,7 +1,9 @@
import getMetaInfo from '../shared/getMetaInfo'
import { metaInfoOptionKeys } from '../shared/constants'
import { serverSequences } from '../shared/escaping'
import generateServerInjector from './generateServerInjector'
export default function _inject (options = {}) {
export default function _inject(options = {}) {
/**
* Converts the state of the meta info object such that each item
* can be compiled to a tag string on the server
@@ -9,17 +11,17 @@ export default function _inject (options = {}) {
* @this {Object} - Vue instance - ideally the root component
* @return {Object} - server meta info with `toString` methods
*/
return function inject () {
return function inject() {
// get meta info with sensible defaults
const info = getMetaInfo(options)(this.$root)
const metaInfo = getMetaInfo(options, this.$root, serverSequences)
// generate server injectors
for (let key in info) {
if (info.hasOwnProperty(key) && key !== 'titleTemplate' && key !== 'titleChunk') {
info[key] = generateServerInjector(options)(key, info[key])
for (const key in metaInfo) {
if (!metaInfoOptionKeys.includes(key) && metaInfo.hasOwnProperty(key)) {
metaInfo[key] = generateServerInjector(options, key, metaInfo[key])
}
}
return info
return metaInfo
}
}
-16
View File
@@ -1,16 +0,0 @@
import inject from '../server/inject'
import refresh from '../client/refresh'
export default function _$meta (options = {}) {
/**
* Returns an injector for server-side rendering.
* @this {Object} - the Vue instance (a root component)
* @return {Object} - injector
*/
return function $meta () {
return {
inject: inject(options).bind(this),
refresh: refresh(options).bind(this)
}
}
}
+112 -7
View File
@@ -2,26 +2,131 @@
* These are constant variables used throughout the application.
*/
// set some sane defaults
export const defaultInfo = {
title: '',
titleChunk: '',
titleTemplate: '%s',
htmlAttrs: {},
bodyAttrs: {},
headAttrs: {},
base: [],
link: [],
meta: [],
style: [],
script: [],
noscript: [],
__dangerouslyDisableSanitizers: [],
__dangerouslyDisableSanitizersByTagID: {}
}
// This is the name of the component option that contains all the information that
// gets converted to the various meta tags & attributes for the page.
export const VUE_META_KEY_NAME = 'metaInfo'
export const keyName = 'metaInfo'
// This is the attribute vue-meta augments on elements to know which it should
// This is the attribute vue-meta arguments on elements to know which it should
// manage and which it should ignore.
export const VUE_META_ATTRIBUTE = 'data-vue-meta'
export const attribute = 'data-vue-meta'
// This is the attribute that goes on the `html` tag to inform `vue-meta`
// that the server has already generated the meta tags for the initial render.
export const VUE_META_SERVER_RENDERED_ATTRIBUTE = 'data-vue-meta-server-rendered'
export const ssrAttribute = 'data-vue-meta-server-rendered'
// This is the property that tells vue-meta to overwrite (instead of append)
// an item in a tag list. For example, if you have two `meta` tag list items
// that both have `vmid` of "description", then vue-meta will overwrite the
// shallowest one with the deepest one.
export const VUE_META_TAG_LIST_ID_KEY_NAME = 'vmid'
export const tagIDKeyName = 'vmid'
// This is the key name for possible meta templates
export const VUE_META_TEMPLATE_KEY_NAME = 'template'
export const metaTemplateKeyName = 'template'
// This is the key name for the content-holding property
export const VUE_META_CONTENT_KEY = 'content'
export const contentKeyName = 'content'
export const defaultOptions = {
keyName,
attribute,
ssrAttribute,
tagIDKeyName,
contentKeyName,
metaTemplateKeyName
}
// List of metaInfo property keys which are configuration options (and dont generate html)
export const metaInfoOptionKeys = [
'titleChunk',
'titleTemplate',
'changed',
'__dangerouslyDisableSanitizers',
'__dangerouslyDisableSanitizersByTagID'
]
// The metaInfo property keys which are used to disable escaping
export const disableOptionKeys = [
'__dangerouslyDisableSanitizers',
'__dangerouslyDisableSanitizersByTagID'
]
// List of metaInfo property keys which only generates attributes and no tags
export const metaInfoAttributeKeys = [
'htmlAttrs',
'headAttrs',
'bodyAttrs'
]
// HTML elements which dont have a head tag (shortened to our needs)
// see: https://www.w3.org/TR/html52/document-metadata.html
export const tagsWithoutEndTag = ['base', 'meta', 'link']
// HTML elements which can have inner content (shortened to our needs)
export const tagsWithInnerContent = ['noscript', 'script', 'style']
// Attributes which are inserted as childNodes instead of HTMLAttribute
export const tagAttributeAsInnerContent = ['innerHTML', 'cssText']
// from: https://github.com/kangax/html-minifier/blob/gh-pages/src/htmlminifier.js#L202
export const booleanHtmlAttributes = [
'allowfullscreen',
'amp',
'async',
'autofocus',
'autoplay',
'checked',
'compact',
'controls',
'declare',
'default',
'defaultchecked',
'defaultmuted',
'defaultselected',
'defer',
'disabled',
'enabled',
'formnovalidate',
'hidden',
'indeterminate',
'inert',
'ismap',
'itemscope',
'loop',
'multiple',
'muted',
'nohref',
'noresize',
'noshade',
'novalidate',
'nowrap',
'open',
'pauseonexit',
'readonly',
'required',
'reversed',
'scoped',
'seamless',
'selected',
'sortable',
'truespeed',
'typemustmatch',
'visible'
]
+70
View File
@@ -0,0 +1,70 @@
import { isString, isArray, isObject } from '../utils/is-type'
import { includes } from '../utils/array'
import { metaInfoOptionKeys, disableOptionKeys } from './constants'
export const serverSequences = [
[/&/g, '&amp;'],
[/</g, '&lt;'],
[/>/g, '&gt;'],
[/"/g, '&quot;'],
[/'/g, '&#x27;']
]
export const clientSequences = [
[/&/g, '\u0026'],
[/</g, '\u003c'],
[/>/g, '\u003e'],
[/"/g, '\u0022'],
[/'/g, '\u0027']
]
// sanitizes potentially dangerous characters
export function escape(info, options, escapeOptions) {
const { tagIDKeyName } = options
const { doEscape = v => v } = escapeOptions
const escaped = {}
for (const key in info) {
const value = info[key]
// no need to escape configuration options
if (includes(metaInfoOptionKeys, key)) {
escaped[key] = value
continue
}
let [ disableKey ] = disableOptionKeys
if (escapeOptions[disableKey] && includes(escapeOptions[disableKey], key)) {
// this info[key] doesnt need to escaped if the option is listed in __dangerouslyDisableSanitizers
escaped[key] = value
continue
}
const tagId = info[tagIDKeyName]
if (tagId) {
disableKey = disableOptionKeys[1]
// keys which are listed in __dangerouslyDisableSanitizersByTagID for the current vmid do not need to be escaped
if (escapeOptions[disableKey] && escapeOptions[disableKey][tagId] && includes(escapeOptions[disableKey][tagId], key)) {
escaped[key] = value
continue
}
}
if (isString(value)) {
escaped[key] = doEscape(value)
} else if (isArray(value)) {
escaped[key] = value.map((v) => {
return isObject(v)
? escape(v, options, escapeOptions)
: doEscape(v)
})
} else if (isObject(value)) {
escaped[key] = escape(value, options, escapeOptions)
} else {
escaped[key] = value
}
}
return escaped
}
+44 -43
View File
@@ -1,6 +1,8 @@
import deepmerge from 'deepmerge'
import uniqBy from './uniqBy'
import uniqueId from 'lodash.uniqueid'
import { isFunction, isObject } from '../utils/is-type'
import { findIndex } from '../utils/array'
import { merge } from './merge'
import { applyTemplate } from './template'
import { inMetaInfoBranch } from './meta-helpers'
/**
* Returns the `opts.option` $option value of the given `opts.component`.
@@ -10,66 +12,65 @@ import uniqueId from 'lodash.uniqueid'
*
* @param {Object} opts - options
* @param {Object} opts.component - Vue component to fetch option data from
* @param {String} opts.option - what option to look for
* @param {Boolean} opts.deep - look for data in child components as well?
* @param {Function} opts.arrayMerge - how should arrays be merged?
* @param {String} opts.keyName - the name of the option to look for
* @param {Object} [result={}] - result so far
* @return {Object} result - final aggregated result
*/
export default function getComponentOption (opts, result = {}) {
const { component, option, deep, arrayMerge, metaTemplateKeyName, tagIDKeyName, contentKeyName } = opts
const { $options } = component
export default function getComponentOption(options = {}, component, result = {}) {
const { keyName, metaTemplateKeyName, tagIDKeyName } = options
const { $options, $children } = component
if (component._inactive) return result
if (component._inactive) {
return result
}
// only collect option data if it exists
if (typeof $options[option] !== 'undefined' && $options[option] !== null) {
let data = $options[option]
if ($options[keyName]) {
let data = $options[keyName]
// if option is a function, replace it with it's result
if (typeof data === 'function') {
if (isFunction(data)) {
data = data.call(component)
}
if (typeof data === 'object') {
// merge with existing options
result = deepmerge(result, data, { arrayMerge })
} else {
result = data
// ignore data if its not an object, then we keep our previous result
if (!isObject(data)) {
return result
}
// merge with existing options
result = merge(result, data, options)
}
// collect & aggregate child options if deep = true
if (deep && component.$children.length) {
component.$children.forEach((childComponent) => {
result = getComponentOption({
component: childComponent,
option,
deep,
arrayMerge
}, result)
})
}
if (metaTemplateKeyName && result.hasOwnProperty('meta')) {
result.meta = Object.keys(result.meta).map(metaKey => {
const metaObject = result.meta[metaKey]
if (!metaObject.hasOwnProperty(metaTemplateKeyName) || !metaObject.hasOwnProperty(contentKeyName) || typeof metaObject[metaTemplateKeyName] === 'undefined') {
return result.meta[metaKey]
if ($children.length) {
$children.forEach((childComponent) => {
// check if the childComponent is in a branch
// return otherwise so we dont walk all component branches unnecessarily
if (!inMetaInfoBranch(childComponent)) {
return
}
const template = metaObject[metaTemplateKeyName]
delete metaObject[metaTemplateKeyName]
if (template) {
metaObject.content = typeof template === 'function' ? template(metaObject.content) : template.replace(/%s/g, metaObject.content)
}
return metaObject
result = getComponentOption(options, childComponent, result)
})
result.meta = uniqBy(
result.meta,
metaObject => metaObject.hasOwnProperty(tagIDKeyName) ? metaObject[tagIDKeyName] : uniqueId()
)
}
if (metaTemplateKeyName && result.meta) {
// apply templates if needed
result.meta.forEach(metaObject => applyTemplate(options, metaObject))
// remove meta items with duplicate vmid's
result.meta = result.meta.filter((metaItem, index, arr) => {
return (
// keep meta item if it doesnt has a vmid
!metaItem.hasOwnProperty(tagIDKeyName) ||
// or if it's the first item in the array with this vmid
index === findIndex(arr, item => item[tagIDKeyName] === metaItem[tagIDKeyName])
)
})
}
return result
}
+50 -150
View File
@@ -1,156 +1,56 @@
import deepmerge from 'deepmerge'
import isPlainObject from 'lodash.isplainobject'
import isArray from './isArray'
import { ensureIsArray } from '../utils/ensure'
import { applyTemplate } from './template'
import { defaultInfo, disableOptionKeys } from './constants'
import { escape } from './escaping'
import getComponentOption from './getComponentOption'
const escapeHTML = (str) => typeof window === 'undefined'
// server-side escape sequence
? String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
// client-side escape sequence
: String(str)
.replace(/&/g, '\u0026')
.replace(/</g, '\u003c')
.replace(/>/g, '\u003e')
.replace(/"/g, '\u0022')
.replace(/'/g, '\u0027')
/**
* Returns the correct meta info for the given component
* (child components will overwrite parent meta info)
*
* @param {Object} component - the Vue instance to get meta info from
* @return {Object} - returned meta info
*/
export default function getMetaInfo(options = {}, component, escapeSequences = []) {
// collect & aggregate all metaInfo $options
let info = getComponentOption(options, component, defaultInfo)
export default function _getMetaInfo (options = {}) {
const { keyName, tagIDKeyName, metaTemplateKeyName, contentKeyName } = options
/**
* Returns the correct meta info for the given component
* (child components will overwrite parent meta info)
*
* @param {Object} component - the Vue instance to get meta info from
* @return {Object} - returned meta info
*/
return function getMetaInfo (component) {
// set some sane defaults
const defaultInfo = {
title: '',
titleChunk: '',
titleTemplate: '%s',
htmlAttrs: {},
bodyAttrs: {},
headAttrs: {},
meta: [],
base: [],
link: [],
style: [],
script: [],
noscript: [],
__dangerouslyDisableSanitizers: [],
__dangerouslyDisableSanitizersByTagID: {}
}
// Remove all "template" tags from meta
// collect & aggregate all metaInfo $options
let info = getComponentOption({
component,
option: keyName,
deep: true,
metaTemplateKeyName,
tagIDKeyName,
contentKeyName,
arrayMerge (target, source) {
// we concat the arrays without merging objects contained in,
// but we check for a `vmid` property on each object in the array
// using an O(1) lookup associative array exploit
// note the use of "for in" - we are looping through arrays here, not
// plain objects
const destination = []
for (let targetIndex in target) {
const targetItem = target[targetIndex]
let shared = false
for (let sourceIndex in source) {
const sourceItem = source[sourceIndex]
if (targetItem[tagIDKeyName] && targetItem[tagIDKeyName] === sourceItem[tagIDKeyName]) {
const targetTemplate = targetItem[metaTemplateKeyName]
const sourceTemplate = sourceItem[metaTemplateKeyName]
if (targetTemplate && !sourceTemplate) {
sourceItem[contentKeyName] = applyTemplate(component)(targetTemplate)(sourceItem[contentKeyName])
}
// If template defined in child but content in parent
if (targetTemplate && sourceTemplate && !sourceItem[contentKeyName]) {
sourceItem[contentKeyName] = applyTemplate(component)(sourceTemplate)(targetItem[contentKeyName])
delete sourceItem[metaTemplateKeyName]
}
shared = true
break
}
}
if (!shared) {
destination.push(targetItem)
}
}
return destination.concat(source)
}
})
// Remove all "template" tags from meta
// backup the title chunk in case user wants access to it
if (info.title) {
info.titleChunk = info.title
}
// replace title with populated template
if (info.titleTemplate) {
info.title = applyTemplate(component)(info.titleTemplate)(info.titleChunk || '')
}
// convert base tag to an array so it can be handled the same way
// as the other tags
if (info.base) {
info.base = Object.keys(info.base).length ? [info.base] : []
}
const ref = info.__dangerouslyDisableSanitizers
const refByTagID = info.__dangerouslyDisableSanitizersByTagID
// sanitizes potentially dangerous characters
const escape = (info) => Object.keys(info).reduce((escaped, key) => {
let isDisabled = ref && ref.indexOf(key) > -1
const tagID = info[tagIDKeyName]
if (!isDisabled && tagID) {
isDisabled = refByTagID && refByTagID[tagID] && refByTagID[tagID].indexOf(key) > -1
}
const val = info[key]
escaped[key] = val
if (key === '__dangerouslyDisableSanitizers' || key === '__dangerouslyDisableSanitizersByTagID') {
return escaped
}
if (!isDisabled) {
if (typeof val === 'string') {
escaped[key] = escapeHTML(val)
} else if (isPlainObject(val)) {
escaped[key] = escape(val)
} else if (isArray(val)) {
escaped[key] = val.map(escape)
} else {
escaped[key] = val
}
} else {
escaped[key] = val
}
return escaped
}, {})
// merge with defaults
info = deepmerge(defaultInfo, info)
// begin sanitization
info = escape(info)
return info
// backup the title chunk in case user wants access to it
if (info.title) {
info.titleChunk = info.title
}
}
const applyTemplate = component => template => chunk =>
typeof template === 'function' ? template.call(component, chunk) : template.replace(/%s/g, chunk)
// replace title with populated template
if (info.titleTemplate && info.titleTemplate !== '%s') {
applyTemplate({ component, contentKeyName: 'title' }, info, info.titleTemplate, info.titleChunk || '')
}
// convert base tag to an array so it can be handled the same way
// as the other tags
if (info.base) {
info.base = Object.keys(info.base).length ? [info.base] : []
}
const escapeOptions = {
doEscape: value => escapeSequences.reduce((val, [v, r]) => val.replace(v, r), value)
}
disableOptionKeys.forEach((disableKey, index) => {
if (index === 0) {
ensureIsArray(info, disableKey)
} else if (index === 1) {
for (const key in info[disableKey]) {
ensureIsArray(info[disableKey], key)
}
}
escapeOptions[disableKey] = info[disableKey]
})
// begin sanitization
info = escape(info, options, escapeOptions)
return info
}
-10
View File
@@ -1,10 +0,0 @@
/**
* checks if passed argument is an array
* @param {any} arr - the object to check
* @return {Boolean} - true if `arr` is an array
*/
export default function isArray (arr) {
return Array.isArray
? Array.isArray(arr)
: Object.prototype.toString.call(arr) === '[object Array]'
}
+91
View File
@@ -0,0 +1,91 @@
import deepmerge from 'deepmerge'
import { findIndex } from '../utils/array'
import { applyTemplate } from './template'
import { metaInfoAttributeKeys } from './constants'
export function arrayMerge({ component, tagIDKeyName, metaTemplateKeyName, contentKeyName }, target, source) {
// we concat the arrays without merging objects contained in,
// but we check for a `vmid` property on each object in the array
// using an O(1) lookup associative array exploit
const destination = []
target.forEach((targetItem, targetIndex) => {
// no tagID so no need to check for duplicity
if (!targetItem[tagIDKeyName]) {
destination.push(targetItem)
return
}
const sourceIndex = findIndex(source, item => item[tagIDKeyName] === targetItem[tagIDKeyName])
const sourceItem = source[sourceIndex]
// source doesnt contain any duplicate vmid's, we can keep targetItem
if (sourceIndex === -1) {
destination.push(targetItem)
return
}
// when sourceItem explictly defines contentKeyName or innerHTML as undefined, its
// an indication that we need to skip the default behaviour or child has preference over parent
// which means we keep the targetItem and ignore/remove the sourceItem
if ((sourceItem.hasOwnProperty(contentKeyName) && sourceItem[contentKeyName] === undefined) ||
(sourceItem.hasOwnProperty('innerHTML') && sourceItem.innerHTML === undefined)) {
destination.push(targetItem)
// remove current index from source array so its not concatenated to destination below
source.splice(sourceIndex, 1)
return
}
// we now know that targetItem is a duplicate and we should ignore it in favor of sourceItem
// if source specifies null as content then ignore both the target as the source
if (sourceItem[contentKeyName] === null || sourceItem.innerHTML === null) {
// remove current index from source array so its not concatenated to destination below
source.splice(sourceIndex, 1)
return
}
// now we only need to check if the target has a template to combine it with the source
const targetTemplate = targetItem[metaTemplateKeyName]
if (!targetTemplate) {
return
}
const sourceTemplate = sourceItem[metaTemplateKeyName]
if (!sourceTemplate) {
// use parent template and child content
applyTemplate({ component, metaTemplateKeyName, contentKeyName }, sourceItem, targetTemplate)
} else if (!sourceItem[contentKeyName]) {
// use child template and parent content
applyTemplate({ component, metaTemplateKeyName, contentKeyName }, sourceItem, undefined, targetItem[contentKeyName])
}
})
return destination.concat(source)
}
export function merge(target, source, options = {}) {
// remove properties explicitly set to false so child components can
// optionally _not_ overwrite the parents content
// (for array properties this is checked in arrayMerge)
if (source.hasOwnProperty('title') && source.title === undefined) {
delete source.title
}
metaInfoAttributeKeys.forEach((attrKey) => {
if (!source[attrKey]) {
return
}
for (const key in source[attrKey]) {
if (source[attrKey].hasOwnProperty(key) && source[attrKey][key] === undefined) {
delete source[attrKey][key]
}
}
})
return deepmerge(target, source, {
arrayMerge: (t, s) => arrayMerge(options, t, s)
})
}
+11
View File
@@ -0,0 +1,11 @@
import { isUndefined, isObject } from '../utils/is-type'
// Vue $root instance has a _vueMeta object property, otherwise its a boolean true
export function hasMetaInfo(vm = this) {
return vm && (vm._vueMeta === true || isObject(vm._vueMeta))
}
// a component is in a metaInfo branch when itself has meta info or one of its (grand-)children has
export function inMetaInfoBranch(vm = this) {
return vm && !isUndefined(vm._vueMeta)
}
+132
View File
@@ -0,0 +1,132 @@
import triggerUpdate from '../client/triggerUpdate'
import { isUndefined, isFunction } from '../utils/is-type'
import { ensuredPush } from '../utils/ensure'
import { hasMetaInfo } from './meta-helpers'
import { addNavGuards } from './nav-guards'
export default function createMixin(Vue, options) {
// for which Vue lifecycle hooks should the metaInfo be refreshed
const updateOnLifecycleHook = ['activated', 'deactivated', 'beforeMount']
// watch for client side component updates
return {
beforeCreate() {
Object.defineProperty(this, '_hasMetaInfo', {
get() {
// Show deprecation warning once when devtools enabled
if (Vue.config.devtools && !this.$root._vueMeta.hasMetaInfoDeprecationWarningShown) {
console.warn('VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please use hasMetaInfo(vm) instead') // eslint-disable-line no-console
this.$root._vueMeta.hasMetaInfoDeprecationWarningShown = true
}
return hasMetaInfo(this)
}
})
// Add a marker to know if it uses metaInfo
// _vnode is used to know that it's attached to a real component
// useful if we use some mixin to add some meta tags (like nuxt-i18n)
if (!isUndefined(this.$options[options.keyName]) && this.$options[options.keyName] !== null) {
if (!this.$root._vueMeta) {
this.$root._vueMeta = {}
}
// to speed up updates we keep track of branches which have a component with vue-meta info defined
// if _vueMeta = true it has info, if _vueMeta = false a child has info
if (!this._vueMeta) {
this._vueMeta = true
let p = this.$parent
while (p && p !== this.$root) {
if (isUndefined(p._vueMeta)) {
p._vueMeta = false
}
p = p.$parent
}
}
// coerce function-style metaInfo to a computed prop so we can observe
// it on creation
if (isFunction(this.$options[options.keyName])) {
if (!this.$options.computed) {
this.$options.computed = {}
}
this.$options.computed.$metaInfo = this.$options[options.keyName]
if (!this.$isServer) {
// if computed $metaInfo exists, watch it for updates & trigger a refresh
// when it changes (i.e. automatically handle async actions that affect metaInfo)
// credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux)
ensuredPush(this.$options, 'created', () => {
this.$watch('$metaInfo', function () {
triggerUpdate(this, 'watcher')
})
})
}
}
// force an initial refresh on page load and prevent other lifecycleHooks
// to triggerUpdate until this initial refresh is finished
// this is to make sure that when a page is opened in an inactive tab which
// has throttled rAF/timers we still immediately set the page title
if (isUndefined(this.$root._vueMeta.initialized)) {
this.$root._vueMeta.initialized = this.$isServer
if (!this.$root._vueMeta.initialized) {
ensuredPush(this.$options, 'mounted', () => {
if (!this.$root._vueMeta.initialized) {
// refresh meta in nextTick so all child components have loaded
this.$nextTick(function () {
this.$root.$meta().refresh()
this.$root._vueMeta.initialized = true
})
}
})
// add the navigation guards if requested
if (options.refreshOnceOnNavigation) {
addNavGuards(this)
}
}
}
// do not trigger refresh on the server side
if (!this.$isServer) {
// add the navigation guards if they havent been added yet
// if metaInfo is defined as a function, this does call the computed fn redundantly
// but as Vue internally caches the results of computed props it shouldnt hurt performance
if (!options.refreshOnceOnNavigation && (
(this.$options[options.keyName] && this.$options[options.keyName].afterNavigation) ||
(this.$options.computed && this.$options.computed.$metaInfo && (this.$options.computed.$metaInfo() || {}).afterNavigation)
)) {
addNavGuards(this)
}
// no need to add this hooks on server side
updateOnLifecycleHook.forEach((lifecycleHook) => {
ensuredPush(this.$options, lifecycleHook, () => triggerUpdate(this, lifecycleHook))
})
// re-render meta data when returning from a child component to parent
ensuredPush(this.$options, 'destroyed', () => {
// Wait that element is hidden before refreshing meta tags (to support animations)
const interval = setInterval(() => {
if (this.$el && this.$el.offsetParent !== null) {
/* istanbul ignore next line */
return
}
clearInterval(interval)
if (!this.$parent) {
/* istanbul ignore next line */
return
}
triggerUpdate(this, 'destroyed')
}, 50)
})
}
}
}
}
}
+26
View File
@@ -0,0 +1,26 @@
import { isFunction } from '../utils/is-type'
export function addNavGuards(vm) {
// return when nav guards already added or no router exists
if (vm.$root._vueMeta.navGuards || !vm.$root.$router) {
/* istanbul ignore next */
return
}
vm.$root._vueMeta.navGuards = true
const $router = vm.$root.$router
const $meta = vm.$root.$meta()
$router.beforeEach((to, from, next) => {
$meta.pause()
next()
})
$router.afterEach(() => {
const { metaInfo } = $meta.resume()
if (metaInfo && metaInfo.afterNavigation && isFunction(metaInfo.afterNavigation)) {
metaInfo.afterNavigation(metaInfo)
}
})
}
+23
View File
@@ -0,0 +1,23 @@
import { isObject } from '../utils/is-type'
import { defaultOptions } from './constants'
export function setOptions(options) {
// combine options
options = isObject(options) ? options : {}
for (const key in defaultOptions) {
if (!options[key]) {
options[key] = defaultOptions[key]
}
}
return options
}
export function getOptions(options) {
const optionsCopy = {}
for (const key in options) {
optionsCopy[key] = options[key]
}
return optionsCopy
}
+13
View File
@@ -0,0 +1,13 @@
export function pause(refresh = true) {
this.$root._vueMeta.paused = true
return () => resume(refresh)
}
export function resume(refresh = true) {
this.$root._vueMeta.paused = false
if (refresh) {
return this.$root.$meta().refresh()
}
}
-103
View File
@@ -1,103 +0,0 @@
import assign from 'object-assign'
import $meta from './$meta'
import batchUpdate from '../client/batchUpdate'
import {
VUE_META_KEY_NAME,
VUE_META_ATTRIBUTE,
VUE_META_SERVER_RENDERED_ATTRIBUTE,
VUE_META_TAG_LIST_ID_KEY_NAME,
VUE_META_TEMPLATE_KEY_NAME, VUE_META_CONTENT_KEY
} from './constants'
// automatic install
if (typeof window !== 'undefined' && typeof window.Vue !== 'undefined') {
Vue.use(VueMeta)
}
/**
* Plugin install function.
* @param {Function} Vue - the Vue constructor.
*/
export default function VueMeta (Vue, options = {}) {
// set some default options
const defaultOptions = {
keyName: VUE_META_KEY_NAME,
contentKeyName: VUE_META_CONTENT_KEY,
metaTemplateKeyName: VUE_META_TEMPLATE_KEY_NAME,
attribute: VUE_META_ATTRIBUTE,
ssrAttribute: VUE_META_SERVER_RENDERED_ATTRIBUTE,
tagIDKeyName: VUE_META_TAG_LIST_ID_KEY_NAME
}
// combine options
options = assign(defaultOptions, options)
// bind the $meta method to this component instance
Vue.prototype.$meta = $meta(options)
// store an id to keep track of DOM updates
let batchID = null
// watch for client side component updates
Vue.mixin({
beforeCreate () {
// Add a marker to know if it uses metaInfo
// _vnode is used to know that it's attached to a real component
// useful if we use some mixin to add some meta tags (like nuxt-i18n)
if (typeof this.$options[options.keyName] !== 'undefined') {
this._hasMetaInfo = true
}
// coerce function-style metaInfo to a computed prop so we can observe
// it on creation
if (typeof this.$options[options.keyName] === 'function') {
if (typeof this.$options.computed === 'undefined') {
this.$options.computed = {}
}
this.$options.computed.$metaInfo = this.$options[options.keyName]
}
},
created () {
// if computed $metaInfo exists, watch it for updates & trigger a refresh
// when it changes (i.e. automatically handle async actions that affect metaInfo)
// credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux)
if (!this.$isServer && this.$metaInfo) {
this.$watch('$metaInfo', () => {
// batch potential DOM updates to prevent extraneous re-rendering
batchID = batchUpdate(batchID, () => this.$meta().refresh())
})
}
},
activated () {
if (this._hasMetaInfo) {
// batch potential DOM updates to prevent extraneous re-rendering
batchID = batchUpdate(batchID, () => this.$meta().refresh())
}
},
deactivated () {
if (this._hasMetaInfo) {
// batch potential DOM updates to prevent extraneous re-rendering
batchID = batchUpdate(batchID, () => this.$meta().refresh())
}
},
beforeMount () {
// batch potential DOM updates to prevent extraneous re-rendering
if (this._hasMetaInfo) {
batchID = batchUpdate(batchID, () => this.$meta().refresh())
}
},
destroyed () {
// do not trigger refresh on the server side
if (this.$isServer) return
// re-render meta data when returning from a child component to parent
if (this._hasMetaInfo) {
// Wait that element is hidden before refreshing meta tags (to support animations)
const interval = setInterval(() => {
if (this.$el && this.$el.offsetParent !== null) return
clearInterval(interval)
if (!this.$parent) return
batchID = batchUpdate(batchID, () => this.$meta().refresh())
}, 50)
}
}
})
}
+23
View File
@@ -0,0 +1,23 @@
import { isUndefined, isFunction } from '../utils/is-type'
export function applyTemplate({ component, metaTemplateKeyName, contentKeyName }, headObject, template, chunk) {
if (isUndefined(template)) {
template = headObject[metaTemplateKeyName]
delete headObject[metaTemplateKeyName]
}
// return early if no template defined
if (!template) {
return false
}
if (isUndefined(chunk)) {
chunk = headObject[contentKeyName]
}
headObject[contentKeyName] = isFunction(template)
? template.call(component, chunk)
: template.replace(/%s/g, chunk)
return true
}
-7
View File
@@ -1,7 +0,0 @@
export default function uniqBy (inputArray, predicate) {
return inputArray
.filter((x, i, arr) => i === arr.length - 1
? true
: predicate(x) !== predicate(arr[i + 1])
)
}
+45
View File
@@ -0,0 +1,45 @@
/*
* To reduce build size, this file provides simple polyfills without
* overly excessive type checking and without modifying
* the global Array.prototype
* The polyfills are automatically removed in the commonjs build
* Also, only files in client/ & shared/ should use these functions
* files in server/ still use normal js function
*/
// this const is replaced by rollup to true for umd builds
// which means the polyfills are removed for other build formats
const polyfill = process.env.NODE_ENV === 'test'
export function findIndex(array, predicate) {
if (polyfill && !Array.prototype.findIndex) {
// idx needs to be a Number, for..in returns string
for (let idx = 0; idx < array.length; idx++) {
if (predicate.call(arguments[2], array[idx], idx, array)) {
return idx
}
}
return -1
}
return array.findIndex(predicate, arguments[2])
}
export function toArray(arg) {
if (polyfill && !Array.from) {
return Array.prototype.slice.call(arg)
}
return Array.from(arg)
}
export function includes(array, value) {
if (polyfill && !Array.prototype.includes) {
for (const idx in array) {
if (array[idx] === value) {
return true
}
}
return false
}
return array.includes(value)
}
+18
View File
@@ -0,0 +1,18 @@
import { isArray, isObject } from './is-type'
export function ensureIsArray(arg, key) {
if (!key || !isObject(arg)) {
return isArray(arg) ? arg : []
}
if (!isArray(arg[key])) {
arg[key] = []
}
return arg
}
export function ensuredPush(object, key, el) {
ensureIsArray(object, key)
object[key].push(el)
}
+24
View File
@@ -0,0 +1,24 @@
/**
* checks if passed argument is an array
* @param {any} arg - the object to check
* @return {Boolean} - true if `arg` is an array
*/
export function isArray(arg) {
return Array.isArray(arg)
}
export function isUndefined(arg) {
return typeof arg === 'undefined'
}
export function isObject(arg) {
return typeof arg === 'object'
}
export function isFunction(arg) {
return typeof arg === 'function'
}
export function isString(arg) {
return typeof arg === 'string'
}
+11
View File
@@ -0,0 +1,11 @@
import { isUndefined } from './is-type'
export function hasGlobalWindowFn() {
try {
return !isUndefined(window)
} catch (e) {
return false
}
}
export const hasGlobalWindow = hasGlobalWindowFn()
+34
View File
@@ -0,0 +1,34 @@
<template>
<div>
<hello-world v-if="childVisible"></hello-world>
</div>
</template>
<script>
import HelloWorld from './hello-world.vue'
export default {
components: {
HelloWorld
},
props: {
changed: {
type: Function
}
},
metaInfo() {
return {
changed: this._changed
}
},
data() {
return {
childVisible: false,
_changed: () => {}
}
},
mounted() {
this._changed = this.changed.bind(this)
}
}
</script>
+26
View File
@@ -0,0 +1,26 @@
<template>
<div>
<hello-world v-if="childVisible"></hello-world>
</div>
</template>
<script>
import HelloWorld from './hello-world.vue'
export default {
components: {
HelloWorld
},
metaInfo() {
return {
title: this.title,
}
},
data() {
return {
childVisible: true,
title: 'Goodbye World'
}
}
}
</script>
+24
View File
@@ -0,0 +1,24 @@
<template>
<div>Test</div>
</template>
<script>
export default {
metaInfo() {
return {
title: this.title
}
},
data() {
return {
title: 'Hello World',
htmlAttrs: {
lang: 'en'
},
meta: [
{ charset: 'utf-8' }
]
}
}
}
</script>
+28
View File
@@ -0,0 +1,28 @@
<template>
<div>
<keep-alive>
<hello-world v-if="childVisible"></hello-world>
</keep-alive>
</div>
</template>
<script>
import HelloWorld from './hello-world.vue'
export default {
components: {
HelloWorld
},
metaInfo() {
return {
title: this.title,
}
},
data() {
return {
childVisible: true,
title: 'Alive World'
}
}
}
</script>
+116
View File
@@ -0,0 +1,116 @@
/**
* @jest-environment node
*/
import fs from 'fs'
import path from 'path'
import env from 'node-env-file'
import { createBrowser } from 'tib'
const browserString = process.env.BROWSER_STRING || 'puppeteer/core'
describe(browserString, () => {
let browser
let page
const folder = path.resolve(__dirname, '..', 'fixtures/basic/.vue-meta/')
beforeAll(async () => {
if (browserString.includes('browserstack') && browserString.includes('local')) {
const envFile = path.resolve(__dirname, '..', '..', '.env-browserstack')
if (fs.existsSync(envFile)) {
env(envFile)
}
}
browser = await createBrowser(browserString, {
BrowserStackLocal: { folder },
extendPage(page) {
return {
async navigate(path) {
// IMPORTANT: use (arrow) function with block'ed body
// see: https://github.com/tunnckoCoreLabs/parse-function/issues/179
await page.runAsyncScript((path) => {
return new Promise((resolve) => {
const oldTitle = document.title
// local firefox has sometimes not updated the title
// even when the DOM is supposed to be fully updated
const waitTitleChanged = function () {
setTimeout(function () {
if (oldTitle !== document.title) {
resolve()
} else {
waitTitleChanged()
}
}, 50)
}
window.$vueMeta.$once('routeChanged', waitTitleChanged)
window.$vueMeta.$router.push(path)
})
}, path)
},
routeData() {
return page.runScript(() => ({
path: window.$vueMeta.$route.path,
query: window.$vueMeta.$route.query
}))
}
}
}
})
})
afterAll(async () => {
if (browser) {
await browser.close()
}
})
test('open page', async () => {
const webPath = '/index.html'
let url
if (browser.getLocalFolderUrl) {
url = browser.getLocalFolderUrl(webPath)
} else {
url = `file://${path.join(folder, webPath)}`
}
page = await browser.page(url)
expect(await page.getAttribute('html', 'data-vue-meta-server-rendered')).toBe(null)
expect(await page.getAttribute('html', 'lang')).toBe('en')
expect(await page.getAttribute('html', 'amp')).toBe('')
expect(await page.getAttribute('html', 'allowfullscreen')).toBe(null)
expect(await page.getAttribute('head', 'test')).toBe('true')
expect(await page.getText('h1')).toBe('Basic')
expect(await page.getText('title')).toBe('Home | Vue Meta Test')
expect(await page.getElementCount('meta')).toBe(2)
let sanitizeCheck = await page.getTexts('script')
sanitizeCheck.push(...(await page.getTexts('noscript')))
sanitizeCheck = sanitizeCheck.filter(v => !!v)
expect(sanitizeCheck.length).toBe(3)
expect(() => JSON.parse(sanitizeCheck[0])).not.toThrow()
// TODO: check why this doesnt Throw when Home is dynamic loaded
// (but that causes hydration error)
expect(() => JSON.parse(sanitizeCheck[1])).toThrow()
expect(() => JSON.parse(sanitizeCheck[2])).not.toThrow()
})
test('/about', async () => {
try {
await page.navigate('/about', false)
} catch (e) {
if (e.constructor.name !== 'ScriptTimeoutError') {
throw e
} else {
console.warn(e) // eslint-disable-line no-console
}
}
expect(await page.getText('title')).toBe('About')
expect(await page.getElementCount('meta')).toBe(1)
})
})
+33
View File
@@ -0,0 +1,33 @@
import { buildFixture } from '../utils/build'
describe('basic browser with ssr page', () => {
let html
beforeAll(async () => {
const fixture = await buildFixture('basic')
html = fixture.html
})
test('validate ssr', () => {
const htmlTag = html.match(/<html([^>]+)>/)[0]
expect(htmlTag).toContain('data-vue-meta-server-rendered')
expect(htmlTag).toContain(' lang="en" ')
expect(htmlTag).toContain(' amp ')
expect(htmlTag).not.toContain('allowfullscreen')
expect(html.match(/<title[^>]*>(.*?)<\/title>/)[1]).toBe('Home | Vue Meta Test')
expect(html.match(/<meta/g).length).toBe(2)
expect(html.match(/<meta/g).length).toBe(2)
const re = /<(no)?script[^>]+type="application\/ld\+json"[^>]*>(.*?)</g
const sanitizeCheck = []
let match
while ((match = re.exec(html))) {
sanitizeCheck.push(match[2])
}
expect(sanitizeCheck.length).toBe(3)
expect(() => JSON.parse(sanitizeCheck[0])).not.toThrow()
expect(() => JSON.parse(sanitizeCheck[1])).toThrow()
expect(() => JSON.parse(sanitizeCheck[2])).not.toThrow()
})
})
+17
View File
@@ -0,0 +1,17 @@
<!doctype html>
<html data-vue-meta-server-rendered {{ htmlAttrs.text() }}>
<head {{ headAttrs.text() }}>
{{ meta.text() }}
{{ title.text() }}
{{ link.text() }}
{{ style.text() }}
{{ webpackAssets }}
{{ script.text() }}
{{ noscript.text() }}
</head>
<body {{ bodyAttrs.text() }}>
{{ app }}
{{ script.text({ body: true }) }}
{{ noscript.text({ body: true }) }}
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
<template>
<div id="app">
<h1>Basic</h1>
<router-view></router-view>
<p>Inspect Element to see the meta info</p>
</div>
</template>
<script>
export default {
metaInfo() {
return {
meta: [
{ vmid: 'charset', charset: 'utf-8' }
],
afterNavigation: () => {
this.$emit('routeChanged')
}
}
},
mounted() {
window.$vueMeta = this
}
}
</script>
+10
View File
@@ -0,0 +1,10 @@
import Vue from 'vue'
import VueMeta from '../../../src/browser'
import App from './App.vue'
import createRouter from './router'
Vue.use(VueMeta)
App.router = createRouter()
new Vue(App).$mount('#app')

Some files were not shown because too many files have changed in this diff Show More