-
-
Notifications
You must be signed in to change notification settings - Fork 975
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add automatic og:image generation to docs (#2851)
## 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
- Loading branch information
Showing
6 changed files
with
588 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
import { createWriteStream } from 'fs'; | ||
import { pipeline } from 'stream'; | ||
import { promisify } from 'util'; | ||
import path from 'path'; | ||
import fs from 'fs'; | ||
import OGImageStream from './og-image-stream'; | ||
|
||
const formatImportantHeaders = (headers) => { | ||
return Object.fromEntries( | ||
headers | ||
.map((header) => header.replace(/---/g, '').split('\n')) | ||
.flat() | ||
.filter((header) => header !== '') | ||
.map((header) => header.split(':').map((part) => part.trim())) | ||
); | ||
}; | ||
|
||
const formatHeaderToFilename = (header) => { | ||
return `${header | ||
.replace(/[ /]/g, '-') | ||
.replace(/`/g, '') | ||
.replace(/:/g, '') | ||
.toLowerCase()}.png`; | ||
}; | ||
|
||
const getMarkdownHeader = (path) => { | ||
const content = fs.readFileSync(path, 'utf-8'); | ||
|
||
// get first text between --- | ||
const importantHeaders = content | ||
.match(/---([\s\S]*?)---/g) | ||
?.filter((header) => header !== '------'); | ||
|
||
if (importantHeaders) { | ||
const obj = formatImportantHeaders(importantHeaders); | ||
|
||
if (obj?.title) { | ||
return obj.title; | ||
} | ||
} | ||
|
||
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); | ||
} | ||
|
||
const formatFilesInDoc = async (dir, files, baseDirPath) => { | ||
return await Promise.all( | ||
files.map(async (file) => ({ | ||
file, | ||
isDirectory: ( | ||
await fs.promises.lstat(path.resolve(baseDirPath, dir, file)) | ||
).isDirectory(), | ||
isMarkdown: file.endsWith('.md') || file.endsWith('.mdx'), | ||
})) | ||
); | ||
}; | ||
|
||
const formatDocInDocs = async (dir, baseDirPath) => { | ||
const files = await fs.promises.readdir(path.resolve(baseDirPath, dir)); | ||
return { | ||
dir, | ||
files: (await formatFilesInDoc(dir, files, baseDirPath)).filter( | ||
({ isDirectory, isMarkdown }) => isDirectory || isMarkdown | ||
), | ||
}; | ||
}; | ||
|
||
const extractSubFiles = async (dir, files, baseDirPath) => { | ||
return ( | ||
await Promise.all( | ||
files.map(async (file) => { | ||
if (!file.isDirectory) return file.file; | ||
|
||
const subFiles = ( | ||
await fs.promises.readdir(path.resolve(baseDirPath, dir, file.file)) | ||
).filter((file) => file.endsWith('.md') || file.endsWith('.mdx')); | ||
|
||
return subFiles.map((subFile) => `${file.file}/${subFile}`); | ||
}) | ||
) | ||
).flat(); | ||
}; | ||
|
||
const getDocs = async (baseDirPath) => { | ||
let docs = await Promise.all( | ||
( | ||
await fs.promises.readdir(baseDirPath) | ||
).map(async (dir) => formatDocInDocs(dir, baseDirPath)) | ||
); | ||
|
||
docs = await Promise.all( | ||
docs.map(async ({ dir, files }) => ({ | ||
dir, | ||
files: await extractSubFiles(dir, files, baseDirPath), | ||
})) | ||
); | ||
|
||
return docs; | ||
}; | ||
|
||
async function buildOGImages() { | ||
const baseDirPath = path.resolve(__dirname, '../docs'); | ||
|
||
const docs = await getDocs(baseDirPath); | ||
|
||
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')}`; | ||
|
||
docs.map(async ({ dir, files }) => { | ||
files.map(async (file) => { | ||
const header = getMarkdownHeader(path.resolve(baseDirPath, dir, file)); | ||
|
||
const ogImageStream = await OGImageStream(header, base64Image); | ||
|
||
await saveStreamToFile( | ||
ogImageStream, | ||
path.resolve(ogImageTargets, formatHeaderToFilename(header)) | ||
); | ||
}); | ||
}); | ||
} | ||
|
||
buildOGImages(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import React from 'react'; | ||
import path from 'path'; | ||
import { ImageResponse } from '@vercel/og'; | ||
import fs from 'fs'; | ||
|
||
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 600px', | ||
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/fonts/Aeonik-Bold.otf') | ||
), | ||
style: 'normal', | ||
}, | ||
{ | ||
name: 'Aeonik Regular', | ||
data: fs.readFileSync( | ||
path.resolve(__dirname, '../static/fonts/Aeonik-Regular.otf') | ||
), | ||
style: 'normal', | ||
}, | ||
], | ||
} | ||
).body | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import React from 'react'; | ||
import { PageMetadata } from '@docusaurus/theme-common'; | ||
import { useDoc } from '@docusaurus/theme-common/internal'; | ||
export default function DocItemMetadata() { | ||
const { metadata, frontMatter } = useDoc(); | ||
|
||
if (!metadata) return null; | ||
|
||
const ogImageName = metadata.title | ||
.replace(/[ /]/g, '-') | ||
.replace(/`/g, '') | ||
.replace(/:/g, '') | ||
.toLowerCase(); | ||
|
||
return ( | ||
<PageMetadata | ||
title={metadata.title} | ||
description={metadata.description} | ||
keywords={frontMatter.keywords} | ||
image={`img/og/${ogImageName}.png`} | ||
/> | ||
); | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.