@sherby/sherby-metadata
Version:
Manage meta tags for Search Engine Optimization (SEO)
242 lines (207 loc) • 6.41 kB
JavaScript
import { LitElement } from 'lit';
/**
* `sherby-metadata` is a LitElement used to manage meta tags data for
* Search Engine Optimization (SEO). It will add, update and remove `<meta>`
* elements to the `<head>` section based on the JSON object passed to it.
*
* To use this element, add the import to your shell component and include it
* in your component code.
*
* <sherby-metadata .data=${data}></sherby-metadata>
*
* To update your meta tags data, you can update his data property in your shell
* component:
*
* this.data = {
* title: 'This is the page title',
* description: 'This is the page description',
* keywords: 'these,are,keywords'
* };
*
* Alternatively, after the `sherby-metadata` is include in your shell component,
* you can dispatch a `sherby-metadata` event:
*
* this.dispatchEvent(new CustomEvent('sherby-metadata', {
* detail: {
* description: 'This is the page description',
* keywords: 'these,are,keywords',
* title: 'This is the page title',
* }
* }));
*
* This component support also the `OpenGraph` tags.
*
* @customElement
* @extends {LitElement}
* @group SherbyElements
* @demo demo/index.html
*/
export class SherbyMetadata extends LitElement {
/**
* Return the properties.
* @static
* @return {Object} Properties.
*/
static get properties() {
return {
/**
* An object that contains the meta data currently set on the page.
* The object keys will be used for the `name` of the <meta> tag
* and the value the `content`.
* @public
*/
data: {
type: Object,
},
};
}
/**
* Render template in light DOM.
* @return {Element} This element.
*/
createRenderRoot() {
return this;
}
/**
* Create the listener and initialize the meta elements.
* @constructor
*/
constructor() {
super();
this.data = {};
// Object to keep track of meta elements so they can be reused
this._metaElements = {};
// Metadata event listener
this.__metadataEventListener = this._onMetadataEvent.bind(this);
// Get all valid meta elements on the page
this._initializeMetaElements();
}
/**
* Add an event listener to the body element after the next render.
* @public
*/
connectedCallback() {
super.connectedCallback();
window.addEventListener('sherby-metadata', this.__metadataEventListener);
}
/**
* Remove the event listener from the body element.
* @public
*/
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('sherby-metadata', this.__metadataEventListener);
}
/**
* Update the DOM when the data changes.
* @protected
* @param {PropertyValues} changedProperties Changed properties.
*/
updated(changedProperties) {
super.updated(changedProperties);
/* c8 ignore next 3 */
if (!changedProperties.has('data')) {
return;
}
const data = this.data;
// For each key in data
for (const name in data) {
// Continue if it's not a direct property
if (!Object.prototype.hasOwnProperty.call(data, name)) {
continue;
}
// Special case: title
if (name === 'title') {
document.title = data[name] || '';
continue;
} else if (name === 'canonical') {
this._handleCanonical(data[name] || '');
continue;
}
// Do we have this meta element?
if (Object.prototype.hasOwnProperty.call(this._metaElements, name)) {
// Update the content if it is defined
if (data[name]) {
this._metaElements[name].content = data[name];
} else {
// Remove the meta element from the document
document.head.removeChild(this._metaElements[name]);
delete this._metaElements[name];
}
continue;
}
// If the content is not defined at this step,
// we have nothing to do
if (!data[name]) {
continue;
}
// Create a new meta element
const meta = document.createElement('meta');
// Open Graph meta tags name is in property attribute
const attribute = name.substring(0, 3) === 'og:' ? 'property' : 'name';
// Set the corresponding attribute
meta.setAttribute(attribute, name);
// Add the content
meta.content = data[name];
// Add the meta tag to the document
document.head.appendChild(meta);
// Keep track of the inserted meta tag
this._metaElements[name] = meta;
}
}
_handleCanonical(href) {
let canonicalLink = document.querySelector('link[rel="canonical"]');
if (canonicalLink) {
if (href) {
// Update its href
canonicalLink.href = href;
} else {
// Delete the canonical link from the document
document.head.removeChild(canonicalLink);
}
} else {
// Create a new link element
canonicalLink = document.createElement('link');
// Set the corresponding attribute
canonicalLink.setAttribute('rel', 'canonical');
// Add the href
canonicalLink.href = href;
// Add the meta tag to the document
document.head.appendChild(canonicalLink);
}
}
/**
* Initialize the meta elements.
* @protected
*/
_initializeMetaElements() {
const documentMetaElements = document.querySelectorAll('meta');
const metaElements = {};
const iterateOnMetaElement = (metaElement) => {
// Get the name of the meta element
const name = metaElement.name || metaElement.getAttribute('property');
// Add the meta element only if we found a name
if (name) {
metaElements[name] = metaElement;
}
};
// For each meta elements found in the document
documentMetaElements.forEach(iterateOnMetaElement);
// Set the metaElements property
this._metaElements = metaElements;
}
/**
* Update the data when we receive an metadata event.
* @protected
* @param {Event} event Event.
*/
_onMetadataEvent(event) {
// Do we have a detail object?
if (event.detail && typeof event.detail === 'object' && event.detail.constructor === Object) {
this.data = event.detail;
}
// Stop the propagation
event.stopPropagation();
}
}
window.customElements.define('sherby-metadata', SherbyMetadata);