Skip to content

Commit

Permalink
Added support for bookmark card (#11024)
Browse files Browse the repository at this point in the history
requires TryGhost/Admin#1293

- updates `oembed` endpoint behaviour
  - if an oembed provider is not found then we use `metascraper` to populate a metadata object
  - when metadata is returned rather than an oembed response the payload will look like this:
    ```json
    {
        "url": "...",
        "type": "bookmark",
        "metadata": {
            "url": "...",
            "title": "...",
            "description": "...",
            "author": "...",
            "publisher": "...",
            "thumbnail": "...",
            "icon": "..."
        }
    }
    ```
- adds a `bookmark` card which generates output for the bookmark card:
  ```html
  <figure class="kg-card kg-bookmark-card">
    <a href="[URL]" class="kg-bookmark-container">
      <div class="kg-bookmark-content">
        <div class="kg-bookmark-title">[TITLE]</div>
        <div class="kg-bookmark-description">[DESCRIPTION]</div>
        <div class="kg-bookmark-metadata">
          <img src="[ICON]" class="kg-bookmark-icon">
          <span class="kg-bookmark-author">[AUTHOR]</span>
          <span class="kg-bookmark-publisher">[PUBLISHER]</span>
        </div>
      </div>
      <div class="kg-bookmark-thumbnail">
        <img src="[THUMBNAIL]">
      </div>
    </a>
  </figure>
  ```
  - if a particular bit of data does not exist then the associated html element will not be present
  • Loading branch information
rishabhgrg authored and kevinansfield committed Aug 27, 2019
1 parent 4d7164d commit c2aa620
Show file tree
Hide file tree
Showing 5 changed files with 562 additions and 52 deletions.
143 changes: 99 additions & 44 deletions core/server/api/canary/oembed.js
Expand Up @@ -3,6 +3,44 @@ const {extract, hasProvider} = require('oembed-parser');
const Promise = require('bluebird');
const request = require('../../lib/request');
const cheerio = require('cheerio');
const metascraper = require('metascraper')([
require('metascraper-url')(),
require('metascraper-title')(),
require('metascraper-description')(),
require('metascraper-author')(),
require('metascraper-publisher')(),
require('metascraper-image')(),
require('metascraper-logo')(),
require('metascraper-logo-favicon')()
]);

async function fetchBookmarkData(url, html) {
if (!html) {
const response = await request(url, {
headers: {
'user-agent': 'Ghost(https://github.com/TryGhost/Ghost)'
}
});
html = response.body;
}
const scraperResponse = await metascraper({html, url});
const metadata = Object.assign({}, scraperResponse, {
thumbnail: scraperResponse.image,
icon: scraperResponse.logo
});
// We want to use standard naming for image and logo
delete metadata.image;
delete metadata.logo;

if (metadata.title && metadata.description) {
return Promise.resolve({
type: 'bookmark',
url,
metadata
});
}
return Promise.resolve();
}

const findUrlWithProvider = (url) => {
let provider;
Expand Down Expand Up @@ -32,65 +70,82 @@ const getOembedUrlFromHTML = (html) => {
return cheerio('link[type="application/json+oembed"]', html).attr('href');
};

function unknownProvider(url) {
return Promise.reject(new common.errors.ValidationError({
message: common.i18n.t('errors.api.oembed.unknownProvider'),
context: url
}));
}

function knownProvider(url) {
return extract(url).catch((err) => {
return Promise.reject(new common.errors.InternalServerError({
message: err.message
}));
});
}

function fetchOembedData(url) {
let provider;
({url, provider} = findUrlWithProvider(url));
if (provider) {
return knownProvider(url);
}
return request(url, {
method: 'GET',
timeout: 2 * 1000,
followRedirect: true,
headers: {
'user-agent': 'Ghost(https://github.com/TryGhost/Ghost)'
}
}).then((response) => {
if (response.url !== url) {
({url, provider} = findUrlWithProvider(response.url));
}
if (provider) {
return knownProvider(url);
}
const oembedUrl = getOembedUrlFromHTML(response.body);
if (oembedUrl) {
return request(oembedUrl, {
method: 'GET',
json: true
}).then((response) => {
return response.body;
}).catch(() => {});
}
});
}

module.exports = {
docName: 'oembed',

read: {
permissions: false,
data: [
'url'
'url',
'type'
],
options: [],
query({data}) {
let {url} = data;

function unknownProvider() {
return Promise.reject(new common.errors.ValidationError({
message: common.i18n.t('errors.api.oembed.unknownProvider'),
context: url
}));
}

function knownProvider(url) {
return extract(url).catch((err) => {
return Promise.reject(new common.errors.InternalServerError({
message: err.message
}));
});
}
let {url, type} = data;

let provider;
({url, provider} = findUrlWithProvider(url));

if (provider) {
return knownProvider(url);
if (type === 'bookmark') {
return fetchBookmarkData(url);
}

// see if the URL is a redirect to cater for shortened urls
return request(url, {
method: 'GET',
timeout: 2 * 1000,
followRedirect: true
}).then((response) => {
if (response.url !== url) {
({url, provider} = findUrlWithProvider(response.url));
return provider ? knownProvider(url) : unknownProvider();
return fetchOembedData(url).then((response) => {
if (!response && !type) {
return fetchBookmarkData(url);
}

const oembedUrl = getOembedUrlFromHTML(response.body);

if (!oembedUrl) {
return unknownProvider();
return response;
}).then((response) => {
if (!response) {
return unknownProvider(url);
}

return request(oembedUrl, {
method: 'GET',
json: true
}).then((response) => {
return response.body;
});
return response;
}).catch(() => {
return unknownProvider();
return unknownProvider(url);
});
}
}
Expand Down
93 changes: 93 additions & 0 deletions core/server/lib/mobiledoc/cards/bookmark.js
@@ -0,0 +1,93 @@
/**
<figure class="kg-card kg-bookmark-card">
<a href="[URL]" class="kg-bookmark-container">
<div class="kg-bookmark-content">
<div class="kg-bookmark-title">[TITLE]</div>
<div class="kg-bookmark-description">[DESCRIPTION]</div>
<div class="kg-bookmark-metadata">
<img src="[ICON]" class="kg-bookmark-icon">
<span class="kg-bookmark-author">[AUTHOR]</span>
<span class="kg-bookmark-publisher">[PUBLISHER]</span>
</div>
</div>
<div class="kg-bookmark-thumbnail">
<img src="[THUMBNAIL]">
</div>
</a>
</figure>
*/

const createCard = require('../create-card');

function createElement(dom, elem, classNames = '', attributes = [], text) {
let element = dom.createElement(elem);
if (classNames) {
element.setAttribute('class', classNames);
}
attributes.forEach((attr) => {
element.setAttribute(attr.key, attr.value);
});
if (text) {
element.appendChild(dom.createTextNode(text));
}
return element;
}

module.exports = createCard({
name: 'bookmark',
type: 'dom',
render(opts) {
if (!opts.payload.metadata) {
return '';
}

let {payload, env: {dom}} = opts;
let figure = createElement(dom, 'figure', 'kg-card kg-bookmark-card');
let linkTag = createElement(dom, 'a', 'kg-bookmark-container', [{
key: 'href',
value: payload.metadata.url
}]);
let contentDiv = createElement(dom, 'div', 'kg-bookmark-content');
let titleDiv = createElement(dom, 'div', 'kg-bookmark-title', [] , payload.metadata.title);
let descriptionDiv = createElement(dom, 'div', 'kg-bookmark-description', [] , payload.metadata.description);
let metadataDiv = createElement(dom, 'div', 'kg-bookmark-metadata');
let imgIcon = createElement(dom, 'img', 'kg-bookmark-icon', [{
key: 'src',
value: payload.metadata.icon
}]);
let authorSpan = createElement(dom, 'span', 'kg-bookmark-author', [] , payload.metadata.author);
let publisherSpan = createElement(dom, 'span', 'kg-bookmark-publisher', [] , payload.metadata.publisher);
let thumbnailDiv = createElement(dom, 'div', 'kg-bookmark-thumbnail');
let thumbnailImg = createElement(dom, 'img', '', [{
key: 'src',
value: payload.metadata.thumbnail
}]);
thumbnailDiv.appendChild(thumbnailImg);
if (payload.metadata.icon) {
metadataDiv.appendChild(imgIcon);
}
if (payload.metadata.author) {
metadataDiv.appendChild(authorSpan);
}
if (payload.metadata.publisher) {
metadataDiv.appendChild(publisherSpan);
}
contentDiv.appendChild(titleDiv);
contentDiv.appendChild(descriptionDiv);
contentDiv.appendChild(metadataDiv);
linkTag.appendChild(contentDiv);
if (payload.metadata.thumbnail) {
linkTag.appendChild(thumbnailDiv);
}
figure.appendChild(linkTag);

if (payload.caption) {
let figcaption = dom.createElement('figcaption');
figcaption.appendChild(dom.createRawHTMLSection(payload.caption));
figure.appendChild(figcaption);
figure.setAttribute('class', `${figure.getAttribute('class')} kg-card-hascaption`);
}

return figure;
}
});
1 change: 1 addition & 0 deletions core/server/lib/mobiledoc/cards/index.js
Expand Up @@ -2,6 +2,7 @@ module.exports = [
require('./card-markdown'),
require('./code'),
require('./embed'),
require('./bookmark'),
require('./hr'),
require('./html'),
require('./image'),
Expand Down
9 changes: 9 additions & 0 deletions package.json
Expand Up @@ -98,6 +98,15 @@
"markdown-it-footnote": "3.0.2",
"markdown-it-lazy-headers": "0.1.3",
"markdown-it-mark": "2.0.0",
"metascraper": "5.6.6",
"metascraper-url": "5.6.6",
"metascraper-title": "5.6.6",
"metascraper-description": "5.6.6",
"metascraper-author": "5.6.6",
"metascraper-publisher": "5.6.6",
"metascraper-image": "5.6.6",
"metascraper-logo": "5.6.6",
"metascraper-logo-favicon": "5.6.6",
"mobiledoc-dom-renderer": "0.6.6",
"moment": "2.24.0",
"moment-timezone": "0.5.23",
Expand Down

0 comments on commit c2aa620

Please sign in to comment.