lib/plugins/helper/open_graph.ts

Summary

Maintainability
D
2 days
Test Coverage
import { isMoment, isDate, Moment } from 'moment';
import { encodeURL, prettyUrls, stripHTML, escapeHTML } from 'hexo-util';
import moize from 'moize';
import type { LocalsType } from '../../types';

const localeMap = {
  'en': 'en_US',
  'de': 'de_DE',
  'es': 'es_ES',
  'fr': 'fr_FR',
  'hu': 'hu_HU',
  'id': 'id_ID',
  'it': 'it_IT',
  'ja': 'ja_JP',
  'ko': 'ko_KR',
  'nl': 'nl_NL',
  'ru': 'ru_RU',
  'th': 'th_TH',
  'tr': 'tr_TR',
  'vi': 'vi_VN'
};

const localeToTerritory = moize.shallow(str => {
  if (str.length === 2 && localeMap[str]) return localeMap[str];

  if (str.length === 5) {
    let territory = [];
    if (str.includes('-')) {
      territory = str.split('-');
    } else {
      territory = str.split('_');
    }

    if (territory.length === 2) return territory[0].toLowerCase() + '_' + territory[1].toUpperCase();
  }
});

const meta = (name: string, content: string | URL, escape?: boolean) => {
  if (escape !== false && typeof content === 'string') {
    content = escapeHTML(content);
  }

  if (content) return `<meta name="${name}" content="${content}">\n`;
  return `<meta name="${name}">\n`;
};

const og = (name: string, content?: string, escape?: boolean) => {
  if (escape !== false && typeof content === 'string') {
    content = escapeHTML(content);
  }

  if (content) return `<meta property="${name}" content="${content}">\n`;
  return `<meta property="${name}">\n`;
};

interface Options {
  image?: string;
  images?: string[];
  description?: string;
  title?: string;
  type?: string;
  url?: string;
  site_name?: string;
  twitter_card?: string;
  date?: Moment | Date | false;
  updated?: Moment | Date | false;
  language?: string;
  author?: string;
  twitter_image?: string;
  twitter_id?: string;
  twitter_site?: string;
  fb_admins?: string;
  fb_app_id?: string;
}

function openGraphHelper(this: LocalsType, options: Options = {}) {
  const { config, page } = this;
  const { content } = page;
  let images = options.image || options.images || page.photos || [];
  let description = options.description || page.description || page.excerpt || content || config.description;
  let keywords = (page.tags && page.tags.length ? page.tags : undefined) || (config as any).keywords || false;
  const title = options.title || page.title || config.title;
  const type = options.type || (this.is_post() ? 'article' : 'website');
  const url = prettyUrls(options.url || this.url, config.pretty_urls);
  const siteName = options.site_name || config.title;
  const twitterCard = options.twitter_card || 'summary';
  const date = options.date !== false ? options.date || page.date : false;
  const updated = options.updated !== false ? options.updated || page.updated : false;
  const language = options.language || page.lang || page.language || config.language;
  const author = options.author || config.author;

  if (!Array.isArray(images)) images = [images];

  if (description) {
    description = escapeHTML(stripHTML(description).substring(0, 200)
      .trim() // Remove prefixing/trailing spaces
    ).replace(/\n/g, ' '); // Replace new lines by spaces
  }

  if (!images.length && content) {
    images = images.slice();

    if (content.includes('<img')) {
      let img;
      const imgPattern = /<img [^>]*src=['"]([^'"]+)([^>]*>)/gi;
      while ((img = imgPattern.exec(content)) !== null) {
        images.push(img[1]);
      }
    }

  }

  let result = '';

  if (description) {
    result += meta('description', description);
  }

  result += og('og:type', type);
  result += og('og:title', title);

  if (url) {
    result += og('og:url', encodeURL(url), false);
  } else {
    result += og('og:url');
  }

  result += og('og:site_name', siteName);
  if (description) {
    result += og('og:description', description, false);
  }

  if (language) {
    result += og('og:locale', localeToTerritory(language), false);
  }

  images = images.map(path => new URL(path, url || config.url).toString())
    .filter(url => !url.startsWith('data:'));

  images.forEach(path => {
    result += og('og:image', path, false);
  });

  if (date) {
    if ((isMoment(date) || isDate(date)) && !isNaN(date.valueOf())) {
      result += og('article:published_time', date.toISOString());
    }
  }

  if (updated) {
    if ((isMoment(updated) || isDate(updated)) && !isNaN(updated.valueOf())) {
      result += og('article:modified_time', updated.toISOString());
    }
  }

  if (author) {
    result += og('article:author', author);
  }

  if (keywords) {
    if (typeof keywords === 'string') keywords = [keywords];

    keywords.map(tag => {
      return tag.name ? tag.name : tag;
    }).filter(Boolean).forEach(keyword => {
      result += og('article:tag', keyword);
    });
  }

  result += meta('twitter:card', twitterCard);

  if (options.twitter_image) {
    let twitter_image: string | URL = options.twitter_image;
    twitter_image = new URL(twitter_image, url || config.url);
    result += meta('twitter:image', twitter_image, false);
  } else if (images.length) {
    result += meta('twitter:image', images[0], false);
  }

  if (options.twitter_id) {
    let twitterId = options.twitter_id;
    if (!twitterId.startsWith('@')) twitterId = `@${twitterId}`;

    result += meta('twitter:creator', twitterId);
  }

  if (options.twitter_site) {
    result += meta('twitter:site', options.twitter_site, false);
  }

  if (options.fb_admins) {
    result += og('fb:admins', options.fb_admins);
  }

  if (options.fb_app_id) {
    result += og('fb:app_id', options.fb_app_id);
  }

  return result.trim();
}

export = openGraphHelper;