feat(backend): Initial support for open graph images
This commit is contained in:
parent
134bcd979e
commit
018a2c461d
5 changed files with 2357 additions and 104 deletions
|
@ -403,7 +403,7 @@
|
||||||
id="tspan1657"
|
id="tspan1657"
|
||||||
sodipodi:role="line"><tspan
|
sodipodi:role="line"><tspan
|
||||||
style="font-size:8.46666622px;fill:#a3a3a3;fill-opacity:0.53472218"
|
style="font-size:8.46666622px;fill:#a3a3a3;fill-opacity:0.53472218"
|
||||||
id="tspan1665">sub_1 | sub_2</tspan></tspan></text>
|
id="tspan1665">sub_1 sub_2</tspan></tspan></text>
|
||||||
<text
|
<text
|
||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
style="font-style:normal;font-weight:normal;font-size:13.63457108px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#fdf4ff;fill-opacity:1;stroke:none;stroke-width:0.34086427"
|
style="font-style:normal;font-weight:normal;font-size:13.63457108px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#fdf4ff;fill-opacity:1;stroke:none;stroke-width:0.34086427"
|
||||||
|
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
@ -59,10 +59,12 @@
|
||||||
"passport-jwt": "4.0.0",
|
"passport-jwt": "4.0.0",
|
||||||
"query-string": "6.2.0",
|
"query-string": "6.2.0",
|
||||||
"remark": "13",
|
"remark": "13",
|
||||||
|
"remark-frontmatter": "^4.0.1",
|
||||||
"remark-parse": "^9.0.0",
|
"remark-parse": "^9.0.0",
|
||||||
"remark-plain-text": "^0.2.0",
|
"remark-plain-text": "^0.2.0",
|
||||||
"rimraf": "2.6.2",
|
"rimraf": "2.6.2",
|
||||||
"sharp": "^0.29.3"
|
"sharp": "^0.29.3",
|
||||||
|
"yaml": "^1.10.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"backpack-core": "0.7.0"
|
"backpack-core": "0.7.0"
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import config from "../config";
|
import config from "../config";
|
||||||
import { log } from "../utils";
|
import { capitalize, log } from "../utils";
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import remark from 'remark'
|
import remark from 'remark'
|
||||||
import remarkParse from 'remark-parse'
|
import remarkParse from 'remark-parse'
|
||||||
|
import remarkFrontmatter from 'remark-frontmatter'
|
||||||
import toString from 'mdast-util-to-string'
|
import toString from 'mdast-util-to-string'
|
||||||
import { Buffer } from 'buffer'
|
import { Buffer } from 'buffer'
|
||||||
|
import yaml from 'yaml'
|
||||||
|
|
||||||
// Sites for which we generate images
|
// Sites for which we generate images
|
||||||
const sites = ['dev', 'org']
|
const sites = ['dev', 'org']
|
||||||
|
@ -20,9 +22,8 @@ const template = fs.readFileSync(
|
||||||
'utf-8'
|
'utf-8'
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Turns markdown into a syntax tree */
|
/* Helper method to extract intro from strapi markdown */
|
||||||
/* Helper method to extract intro from markdown */
|
const introFromStrapiMarkdown = async (md, slug) => {
|
||||||
const introFromMarkdown = async (md, slug) => {
|
|
||||||
const tree = await remark().use(remarkParse).parse(md)
|
const tree = await remark().use(remarkParse).parse(md)
|
||||||
if (tree.children[0].type !== 'paragraph')
|
if (tree.children[0].type !== 'paragraph')
|
||||||
console.log('Markdown does not start with paragraph', slug)
|
console.log('Markdown does not start with paragraph', slug)
|
||||||
|
@ -30,6 +31,23 @@ const introFromMarkdown = async (md, slug) => {
|
||||||
return toString(tree.children[0])
|
return toString(tree.children[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Helper method to extract title from markdown frontmatter */
|
||||||
|
const titleAndIntroFromLocalMarkdown = async (md, slug) => {
|
||||||
|
const tree = await remark()
|
||||||
|
.use(remarkParse)
|
||||||
|
.use(remarkFrontmatter, ['yaml'])
|
||||||
|
.parse(md)
|
||||||
|
|
||||||
|
if (tree.children[0].type !== 'yaml')
|
||||||
|
console.log('Markdown does not start with frontmatter', slug)
|
||||||
|
else return {
|
||||||
|
title: titleAsLines(yaml.parse(tree.children[0].value).title),
|
||||||
|
intro: introAsLines(toString(tree.children.slice(1, 2)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
/* Helper method to load dev blog post */
|
/* Helper method to load dev blog post */
|
||||||
const loadDevBlogPost = async (slug) => {
|
const loadDevBlogPost = async (slug) => {
|
||||||
const result = await axios.get(
|
const result = await axios.get(
|
||||||
|
@ -37,7 +55,7 @@ const loadDevBlogPost = async (slug) => {
|
||||||
)
|
)
|
||||||
if (result.data) return {
|
if (result.data) return {
|
||||||
title: titleAsLines(result.data[0].title),
|
title: titleAsLines(result.data[0].title),
|
||||||
intro: introAsLines(await introFromMarkdown(result.data[0].body, slug)),
|
intro: introAsLines(await introFromStrapiMarkdown(result.data[0].body, slug)),
|
||||||
sub: [
|
sub: [
|
||||||
result.data[0].author.displayname,
|
result.data[0].author.displayname,
|
||||||
new Date(result.data[0].published_at).toString().split(' ').slice(0,4).join(' '),
|
new Date(result.data[0].published_at).toString().split(' ').slice(0,4).join(' '),
|
||||||
|
@ -48,6 +66,22 @@ const loadDevBlogPost = async (slug) => {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Helper method to load markdown file from disk */
|
||||||
|
const loadMarkdownFile = async (page, site, lang) => fs.promises.readFile(
|
||||||
|
path.resolve('..', '..', 'markdown', site, ...page.split('/'), `${lang}.md`),
|
||||||
|
'utf-8'
|
||||||
|
).then(async (md) => md
|
||||||
|
? {
|
||||||
|
...((await titleAndIntroFromLocalMarkdown(md, page))),
|
||||||
|
sub: [
|
||||||
|
'freesewing.dev',
|
||||||
|
page
|
||||||
|
],
|
||||||
|
lead: capitalize(page.split('/').shift())
|
||||||
|
}
|
||||||
|
: false
|
||||||
|
)
|
||||||
|
|
||||||
/* Find longest possible place to split a string */
|
/* Find longest possible place to split a string */
|
||||||
const splitLine = (line, chars) => {
|
const splitLine = (line, chars) => {
|
||||||
const words = line.split(' ')
|
const words = line.split(' ')
|
||||||
|
@ -84,28 +118,43 @@ const introAsLines = intro => {
|
||||||
// Two lines it is
|
// Two lines it is
|
||||||
return splitLine(intro, config.og.chars.intro)
|
return splitLine(intro, config.og.chars.intro)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get title and intro
|
// Get title and intro
|
||||||
const getMetaData = {
|
const getMetaData = {
|
||||||
dev: async (page) => {
|
dev: async (page) => {
|
||||||
const data = {}
|
const data = {}
|
||||||
const chunks = page.split('/')
|
const chunks = page.split('/')
|
||||||
|
// Home page
|
||||||
if (chunks.length === 0) return {
|
if (chunks.length === 0) return {
|
||||||
title: 'FreeSewing FIXME',
|
title: 'FreeSewing FIXME',
|
||||||
intro: "FreeSewing's fixme",
|
intro: "FreeSewing's fixme",
|
||||||
sub: ['freesewing.dev', '/fixme'],
|
sub: ['freesewing.dev', '/fixme'],
|
||||||
}
|
}
|
||||||
if (chunks.length === 1) {
|
// Blog index page
|
||||||
if (chunks[0] === 'blog') return {
|
if (chunks.length === 1 && chunks[0] === 'blog') return {
|
||||||
title: titleAsLines('FreeSewing Development Blog'),
|
title: titleAsLines('FreeSewing Developer Blog'),
|
||||||
intro: introAsLines("FreeSewing's blog for developers and contributors"),
|
intro: introAsLines("Contains no sewing news whatsover. Only posts for (aspiring) developers :)"),
|
||||||
sub: ['freesewing.dev', '/blog'],
|
sub: ['freesewing.dev', '/blog'],
|
||||||
|
lead: 'Developer Blog',
|
||||||
}
|
}
|
||||||
}
|
// Blog post
|
||||||
if (chunks.length === 2 && chunks[0] === 'blog') {
|
if (chunks.length === 2 && chunks[0] === 'blog') {
|
||||||
return await loadDevBlogPost(chunks[1])
|
return await loadDevBlogPost(chunks[1])
|
||||||
}
|
}
|
||||||
|
// Other (MDX) page
|
||||||
|
const md = await loadMarkdownFile(page, 'dev', 'en')
|
||||||
|
|
||||||
|
// Return markdown info or default generic data
|
||||||
|
return md
|
||||||
|
? md
|
||||||
|
: {
|
||||||
|
title: titleAsLines('FreeSewing.dev'),
|
||||||
|
intro: introAsLines('Documentation, guides, and howtos for contributors and developers alike'),
|
||||||
|
sub: ['https://freesewing.dev/', '<== Check it out'],
|
||||||
|
lead: 'freesewing.dev'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
org: page => ({})
|
org: async (page, site, lang) => ({})
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide unused placeholders */
|
/* Hide unused placeholders */
|
||||||
|
@ -169,7 +218,7 @@ OgController.prototype.image = async function (req, res) {
|
||||||
if (languages.indexOf(lang) === -1) return res.send({error: 'sorry'})
|
if (languages.indexOf(lang) === -1) return res.send({error: 'sorry'})
|
||||||
|
|
||||||
// Load meta data
|
// Load meta data
|
||||||
const data = await getMetaData[site](page)
|
const data = await getMetaData[site](page, site, lang)
|
||||||
// Inject into SVG
|
// Inject into SVG
|
||||||
const svg = decorateSvg(data)
|
const svg = decorateSvg(data)
|
||||||
// Turn into PNG
|
// Turn into PNG
|
||||||
|
|
|
@ -12,6 +12,8 @@ import sharp from "sharp";
|
||||||
export const email = mailer;
|
export const email = mailer;
|
||||||
export const log = logger;
|
export const log = logger;
|
||||||
|
|
||||||
|
export const capitalize = string => string.charAt(0).toUpperCase() + string.slice(1);
|
||||||
|
|
||||||
export const getHash = (email) => {
|
export const getHash = (email) => {
|
||||||
let hash = crypto.createHash("sha256");
|
let hash = crypto.createHash("sha256");
|
||||||
hash.update(clean(email));
|
hash.update(clean(email));
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue