1
1
mirror of https://github.com/mdx-js/mdx.git synced 2024-08-17 02:40:27 +03:00
mdx/website/post.js

370 lines
11 KiB
JavaScript
Raw Permalink Normal View History

/**
* @import {Stats} from 'node:fs'
* @import {DataMapMatter, DataMapMeta} from 'vfile'
* @import {Entry} from 'xast-util-feed'
2023-10-14 17:07:54 +03:00
*/
/**
* @typedef Info
2023-10-18 15:38:06 +03:00
* Info.
* @property {Readonly<DataMapMeta>} meta
2023-10-18 15:38:06 +03:00
* Meta.
* @property {Readonly<DataMapMatter>} matter
2023-10-18 15:38:06 +03:00
* Matter.
* @property {boolean | undefined} [navExclude=false]
* Whether to exclude from the navigation (default: `false`).
* @property {number | undefined} [navSortSelf=0]
* Self sort order (default: `0`).
*/
2023-10-16 13:04:24 +03:00
import assert from 'node:assert'
2023-10-18 15:38:06 +03:00
import fs from 'node:fs/promises'
2023-10-16 18:43:31 +03:00
import process from 'node:process'
2023-10-16 13:04:24 +03:00
import {fileURLToPath} from 'node:url'
2023-10-18 15:38:06 +03:00
import chromium from '@sparticuz/chromium'
import {globby} from 'globby'
2023-10-18 17:32:48 +03:00
import {fromHtml} from 'hast-util-from-html'
import {sanitize, defaultSchema} from 'hast-util-sanitize'
import {select} from 'hast-util-select'
2023-10-18 17:32:48 +03:00
import {toHtml} from 'hast-util-to-html'
import {h, s} from 'hastscript'
2023-10-18 15:38:06 +03:00
import pAll from 'p-all'
import puppeteer from 'puppeteer'
import {rss} from 'xast-util-feed'
import {toXml} from 'xast-util-to-xml'
2021-09-26 19:02:09 +03:00
import {config} from '../docs/_config.js'
import {schema} from './schema-description.js'
const dateTimeFormat = new Intl.DateTimeFormat('en', {dateStyle: 'long'})
2023-10-14 17:07:54 +03:00
fs.copyFile(
new URL('404/index.html', config.output),
new URL('404.html', config.output)
)
console.log('✔ `/404/index.html` -> `/404.html`')
2023-10-14 17:07:54 +03:00
const css = await fs.readFile(
new URL('../docs/_asset/index.css', import.meta.url),
'utf8'
)
2021-09-14 12:14:10 +03:00
2023-10-16 13:04:24 +03:00
const filePaths = await globby('**/index.json', {
cwd: fileURLToPath(config.output)
})
2023-10-18 15:38:06 +03:00
const files = filePaths.map(function (d) {
2023-10-14 17:07:54 +03:00
return new URL(d, config.output)
})
2023-10-14 17:07:54 +03:00
const allInfo = await Promise.all(
2023-10-18 15:38:06 +03:00
files.map(async function (url) {
2023-10-14 17:07:54 +03:00
/** @type {Info} */
const info = JSON.parse(String(await fs.readFile(url)))
2023-10-18 15:38:06 +03:00
return {info, url}
})
2023-10-14 17:07:54 +03:00
)
2023-10-14 17:07:54 +03:00
// RSS feed.
const now = new Date()
2023-10-14 17:07:54 +03:00
const entries = await pAll(
[...allInfo]
// All blog entries that are published in the past.
2023-10-18 15:38:06 +03:00
.filter(function (d) {
return (
2023-10-14 17:07:54 +03:00
d.info.meta.pathname &&
d.info.meta.pathname.startsWith('/blog/') &&
d.info.meta.pathname !== '/blog/' &&
d.info.meta.published !== null &&
d.info.meta.published !== undefined &&
new Date(d.info.meta.published) < now
2023-10-18 15:38:06 +03:00
)
})
2023-10-14 17:07:54 +03:00
// Sort.
2023-10-18 15:38:06 +03:00
.sort(function (a, b) {
2023-10-14 17:07:54 +03:00
assert(a.info.meta.published)
assert(b.info.meta.published)
return (
new Date(b.info.meta.published).valueOf() -
new Date(a.info.meta.published).valueOf()
)
2023-10-14 17:07:54 +03:00
})
// Ten most recently published articles.
.slice(0, 10)
2023-10-18 15:38:06 +03:00
.map(function ({info, url}) {
2023-10-14 17:07:54 +03:00
/**
* @returns {Promise<Entry>}
2023-10-18 15:38:06 +03:00
* Entry.
2023-10-14 17:07:54 +03:00
*/
2023-10-18 15:38:06 +03:00
return async function () {
2023-10-14 17:07:54 +03:00
const buf = await fs.readFile(new URL('index.html', url))
2023-10-18 17:32:48 +03:00
const tree = fromHtml(buf)
const body = select('.body', tree)
assert(body)
const clean = sanitize(body, {
...defaultSchema,
attributes: {
...defaultSchema.attributes,
code: [
[
'className',
'language-diff',
'language-html',
'language-js',
'language-jsx',
'language-md',
'language-mdx',
'language-sh',
'language-ts'
2023-10-14 17:07:54 +03:00
]
2023-10-18 17:32:48 +03:00
]
},
clobber: []
})
2023-10-14 17:07:54 +03:00
return {
2023-10-18 15:38:06 +03:00
author: info.meta.author,
2023-10-14 17:07:54 +03:00
description: info.meta.description,
2023-10-18 17:32:48 +03:00
descriptionHtml: toHtml(clean),
2023-10-18 15:38:06 +03:00
modified: info.meta.modified,
published: info.meta.published,
title: info.meta.title,
2023-10-14 17:07:54 +03:00
url: new URL(
url.href.slice(config.output.href.length - 1) + '/../',
config.site
2023-10-18 15:38:06 +03:00
).href
}
2023-10-14 17:07:54 +03:00
}
2023-10-18 15:38:06 +03:00
}),
2023-10-14 17:07:54 +03:00
{concurrency: 6}
)
2023-10-14 17:07:54 +03:00
await fs.writeFile(
new URL('rss.xml', config.output),
toXml(
rss(
{
author: config.author,
2023-10-18 15:38:06 +03:00
description: 'MDX updates',
feedUrl: new URL('rss.xml', config.site).href,
2023-10-14 17:07:54 +03:00
lang: 'en',
2023-10-18 15:38:06 +03:00
tags: config.tags,
title: config.title,
url: config.site.href
2023-10-14 17:07:54 +03:00
},
entries
)
) + '\n'
)
2023-10-14 17:07:54 +03:00
console.log('✔ `/rss.xml`')
2023-10-16 18:43:31 +03:00
const browser = await puppeteer.launch(
process.env.AWS_EXECUTION_ENV
? {
// See: <https://github.com/Sparticuz/chromium/issues/85#issuecomment-1527692751>
args: [...chromium.args, '--disable-gpu'],
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(),
2024-02-10 13:53:22 +03:00
headless: true
2023-10-16 18:43:31 +03:00
}
2024-02-10 13:53:22 +03:00
: {headless: true}
2023-10-16 18:43:31 +03:00
)
2023-10-14 17:07:54 +03:00
await pAll(
2023-10-18 15:38:06 +03:00
allInfo.map(function (data) {
return async function () {
const {url, info} = data
const output = new URL('index.png', url)
/** @type {Stats | undefined} */
let stats
try {
stats = await fs.stat(output)
} catch (error) {
const cause = /** @type {NodeJS.ErrnoException} */ (error)
if (cause.code !== 'ENOENT') throw cause
}
2023-10-18 15:38:06 +03:00
// Dont regenerate to improve performance.
if (stats) return
2023-10-18 17:32:48 +03:00
const value = toHtml({
type: 'root',
children: [
{type: 'doctype'},
2023-10-18 15:38:06 +03:00
h('html', {lang: 'en'}, [
h('head', [
h('meta', {charSet: 'utf8'}),
h('title', 'Generated image'),
h('style', css),
h(
'style',
`
2023-10-14 17:07:54 +03:00
html {
font-size: 24px;
}
2023-10-14 17:07:54 +03:00
body {
/* yellow */
background-image: radial-gradient(
ellipse at 0% 0%,
rgb(252 180 45 / 15%) 20%,
rgb(252 180 45 / 0%) 80%
),
/* purple */
radial-gradient(
ellipse at 0% 100%,
rgb(130 80 223 / 15%) 20%,
rgb(130 80 223 / 0%) 80%
);
}
2023-10-14 17:07:54 +03:00
.og-root {
/* Twitter seems to cut 1em off the size in the height,
* compared to facebook. So thisll look a bit big on FB
* but the assumption is that most folks will share on
* twitter */
height: calc(100vh - calc(2 * (1em + 1ex)));
display: flex;
flex-flow: column;
margin-block: calc(1 * (1em + 1ex));
padding-block: calc(1 * (1em + 1ex));
padding-inline: calc(1 * (1em + 1ex));
background-color: var(--bg);
}
2023-10-14 17:07:54 +03:00
.og-head {
margin-block-end: calc(2 * (1em + 1ex));
}
2023-10-14 17:07:54 +03:00
.og-icon {
display: block;
height: calc(1em + 1ex);
width: auto;
vertical-align: middle;
}
2023-10-14 17:07:54 +03:00
.og-title {
font-size: 3rem;
line-height: calc(1em + (1 / 3 * 1ex));
margin-block: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-shrink: 0;
}
2023-10-14 17:07:54 +03:00
.og-description {
flex-grow: 1;
overflow: hidden;
}
2023-10-14 17:07:54 +03:00
.og-description-inside {
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
display: -webkit-box;
overflow: hidden;
}
2023-10-14 17:07:54 +03:00
.og-meta {
flex-shrink: 0;
margin-top: calc(1em + 1ex);
display: flex;
justify-content: space-between;
}
2023-10-14 17:07:54 +03:00
.og-right {
margin-left: auto;
text-align: right;
}
`
2023-10-18 15:38:06 +03:00
)
]),
h('body', [
h('.og-root', [
h('.og-head', [
s(
'svg.og-icon.og-icon-mdx',
{height: 28.5, width: 69, viewBox: '0 0 138 57'},
[
s('rect', {
fill: 'var(--fg)',
height: 55.5,
rx: 4.5,
width: 136.5,
x: 0.75,
y: 0.75
}),
s(
'g',
{fill: 'none', stroke: 'var(--bg)', strokeWidth: 6},
[
s('path', {
d: 'M16.5 44V19L30.25 32.75l14-14v25'
}),
s('path', {d: 'M70.5 40V10.75'}),
s('path', {d: 'M57 27.25L70.5 40.75l13.5-13.5'}),
s('path', {
d: 'M122.5 41.24L93.25 12M93.5 41.25L122.75 12'
})
]
2023-10-14 17:07:54 +03:00
)
2023-10-18 15:38:06 +03:00
]
)
]),
h('h2.og-title', info.meta.title),
h('.og-description', [
h('.og-description-inside', [
info.meta.descriptionHast
2023-10-18 17:32:48 +03:00
? sanitize(info.meta.descriptionHast, schema)
2023-10-18 15:38:06 +03:00
: info.meta.description || info.matter.description
])
]),
h('.og-meta', [
h('.og-left', [
h('small', 'By'),
h('br'),
h('b', info.meta.author || 'MDX contributors')
]),
info.meta.modified
? h('.og-right', [
h('small', 'Last modified on'),
h('br'),
h(
'b',
dateTimeFormat.format(new Date(info.meta.modified))
)
])
: undefined
])
])
])
])
2023-10-18 17:32:48 +03:00
]
})
2023-10-18 15:38:06 +03:00
try {
await fs.unlink(output)
} catch {}
2023-10-18 15:38:06 +03:00
const page = await browser.newPage()
// This is doubled in the actual file dimensions.
await page.setViewport({deviceScaleFactor: 2, height: 628, width: 1200})
await page.emulateMediaFeatures([
{name: 'prefers-color-scheme', value: 'light'}
])
2023-10-18 17:32:48 +03:00
await page.setContent(value)
2023-10-18 15:38:06 +03:00
const screenshot = await page.screenshot()
await page.close()
2023-10-16 18:43:31 +03:00
2023-10-18 15:38:06 +03:00
await fs.writeFile(output, screenshot)
2023-10-18 15:38:06 +03:00
console.log('OG image `%s`', info.meta.title)
}
2023-10-16 18:43:31 +03:00
})
2023-10-14 17:07:54 +03:00
)
2023-10-16 18:43:31 +03:00
await browser.close()
2023-10-14 17:07:54 +03:00
console.log('✔ OG images')