Skip to content

Commit

Permalink
Add automatic og:image generation to docs (#5867)
Browse files Browse the repository at this point in the history
## Summary
This pr includes changes to dynamically build OG Images for every single
subpage in docs.

## How does it work?
The algorithm runs after docusaurus build. It reads titles from markdown
files in docs dictionary, generates images and place .png files in the
build dictionary.
Docusaurus has been configured to automatically link to generated OG
Images.

## How to test this feature?
Command to change current working dictionary:
> cd docs

Command to download libraries:
> yarn install

Command to build project and generete OG Images:
> yarn build

Command to see the result:
> yarn serve

---------

Co-authored-by: Kacper Kapuściak <39658211+kacperkapusciak@users.noreply.github.com>
Co-authored-by: Patrycja Kalińska <59940332+patrycjakalinska@users.noreply.github.com>
  • Loading branch information
3 people committed Apr 19, 2024
1 parent 41ff27a commit 7531e83
Show file tree
Hide file tree
Showing 6 changed files with 520 additions and 8 deletions.
5 changes: 4 additions & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"build": "docusaurus build && node -r esbuild-register scripts/build-og-images.jsx",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
Expand All @@ -29,6 +29,7 @@
"@emotion/styled": "^11.10.6",
"@mdx-js/react": "^1.6.22",
"@mui/material": "^5.12.0",
"@vercel/og": "^0.6.2",
"@shopify/flash-list": "^1.6.3",
"babel-polyfill": "^6.26.0",
"babel-preset-expo": "^9.2.2",
Expand All @@ -53,6 +54,8 @@
"@docusaurus/module-type-aliases": "^2.4.3",
"@tsconfig/docusaurus": "^1.0.7",
"copy-webpack-plugin": "^11.0.0",
"esbuild": "^0.20.2",
"esbuild-register": "^3.5.0",
"eslint-plugin-mdx": "^2.2.0",
"prettier": "^2.8.4",
"typescript": "^4.7.4",
Expand Down
77 changes: 77 additions & 0 deletions docs/scripts/build-og-images.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { createWriteStream } from 'fs';
import { pipeline } from 'stream';
import { promisify } from 'util';
import React from 'react';
import path from 'path';
import fs from 'fs';
import OGImageStream from './og-image-stream';

const getMarkdownHeader = (path) => {
const content = fs.readFileSync(path, 'utf-8');
const headers = content
.split('\n')
.map((line) => line.trim())
.filter((line) => line.startsWith('#'))
.map((line, index) => ({
level: line.match(/#/g).length,
title: line.replace(/#/g, '').trim(),
index,
}))
.sort((a, b) => a.level - b.level || a.index - b.index);

return headers[0]?.title || 'React Native Reanimated';
};

async function saveStreamToFile(stream, path) {
const writeStream = createWriteStream(path);
await promisify(pipeline)(stream, writeStream);
}

async function buildOGImages() {
const baseDirPath = path.resolve(__dirname, '../docs');
const dirs = await Promise.all(
(
await fs.promises.readdir(baseDirPath)
).map(async (dir) => {
const files = await fs.promises.readdir(path.resolve(baseDirPath, dir));
return {
dir,
files: files.filter(
(file) => file.endsWith('.md') || file.endsWith('.mdx')
),
};
})
);

const ogImageTargets = path.resolve(__dirname, '../build/img/og');

if (fs.existsSync(ogImageTargets)) {
fs.rmSync(ogImageTargets, { recursive: true });
}

fs.mkdirSync(ogImageTargets, { recursive: true });

console.log('Generating OG images for docs...');

const imagePath = path.resolve(__dirname, '../unproccessed/og-image.png');
const imageBuffer = fs.readFileSync(imagePath);
const base64Image = `data:image/png;base64,${imageBuffer.toString('base64')}`;

dirs.map(async ({ dir, files }) => {
files.map(async (file) => {
const header = getMarkdownHeader(path.resolve(baseDirPath, dir, file));

const ogImageStream = OGImageStream(header, base64Image);

await saveStreamToFile(
await ogImageStream,
path.resolve(
ogImageTargets,
`${header.replace(/ /g, '-').replace('/', '-').toLowerCase()}.png`
)
);
});
});
}

buildOGImages();
88 changes: 88 additions & 0 deletions docs/scripts/og-image-stream.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React from 'react';
import { ImageResponse } from '@vercel/og';
import fs from 'fs';
import path from 'path';

export default async function OGImageStream(header, base64Image) {
return new ImageResponse(
(
<div
style={{
display: 'flex',
fontSize: 40,
color: 'black',
background: 'white',
width: '100%',
height: '100%',
padding: '50px 200px',
textAlign: 'center',
justifyContent: 'center',
alignItems: 'center',
}}>
<img
style={{
width: 1200,
height: 630,
objectFit: 'cover',
position: 'absolute',
top: 0,
left: 0,
}}
src={base64Image}
alt=""
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
width: 1200,
gap: -20,
padding: '0 201px 0 67px',
}}>
<p
style={{
fontSize: 72,
fontWeight: 'bold',
color: '#001A72',
textAlign: 'left',
fontFamily: 'Aeonik Bold',
textWrap: 'wrap',
}}>
{header}
</p>
<pre
style={{
fontSize: 40,
fontWeight: 'normal',
color: '#001A72',
textAlign: 'left',
fontFamily: 'Aeonik Regular',
textWrap: 'wrap',
}}>
{'Check out the React Native\nReanimated documentation.'}
</pre>
</div>
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: 'Aeonik Bold',
data: fs.readFileSync(
path.resolve(__dirname, '../static/font/Aeonik-Bold.otf')
),
style: 'normal',
},
{
name: 'Aeonik Regular',
data: fs.readFileSync(
path.resolve(__dirname, '../static/font/Aeonik-Regular.otf')
),
style: 'normal',
},
],
}
).body;
}
29 changes: 29 additions & 0 deletions docs/src/theme/DocItem/Metadata/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import { PageMetadata } from '@docusaurus/theme-common';
import { useDoc } from '@docusaurus/theme-common/internal';
import useBaseUrl from '@docusaurus/useBaseUrl';
export default function DocItemMetadata() {
const { metadata, frontMatter, assets } = useDoc();

if (!metadata.title) {
return null;
}

const ogImageName = metadata.title
.replace(/ /g, '-')
.replace('/', '-')
.toLowerCase();

return (
<PageMetadata
title={metadata.title}
description={metadata.description}
keywords={frontMatter.keywords}
image={`img/og/${
ogImageName === '' || !ogImageName
? 'React Native Reanimated'
: ogImageName
}.png`}
/>
);
}
Binary file added docs/unproccessed/og-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 7531e83

Please sign in to comment.