mirror of
https://github.com/wavetermdev/docusaurus-og.git
synced 2026-08-05 13:46:35 -07:00
feat: use user image in auto-generated preview images
This commit is contained in:
@@ -42,6 +42,8 @@
|
||||
"@react-three/drei": "8.20.2",
|
||||
"@react-three/fiber": "6.2.3",
|
||||
"@tryghost/content-api": "^1.11.4",
|
||||
"axios": "^1.4.0",
|
||||
"boolean": "^3.2.0",
|
||||
"clsx": "^1.2.1",
|
||||
"copy-text-to-clipboard": "^3.0.1",
|
||||
"copy-to-clipboard": "^3.3.2",
|
||||
@@ -49,6 +51,7 @@
|
||||
"docusaurus-plugin-sass": "^0.2.3",
|
||||
"dotenv": "^16.0.3",
|
||||
"lodash": "^4.17.21",
|
||||
"object-hash": "^3.0.0",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
||||
@@ -1,12 +1,35 @@
|
||||
import {
|
||||
BlogPageData,
|
||||
DocsPageData,
|
||||
ImageGeneratorOptions,
|
||||
imageRendererFactory,
|
||||
PageData,
|
||||
} from '@acid-info/docusaurus-og'
|
||||
import { BlogPostFrontMatter } from '@docusaurus/plugin-content-blog'
|
||||
import { DocFrontMatter } from '@docusaurus/plugin-content-docs'
|
||||
import { MDXPageMetadata } from '@docusaurus/plugin-content-pages'
|
||||
import { DocusaurusConfig } from '@docusaurus/types'
|
||||
import axios from 'axios'
|
||||
import { boolean } from 'boolean'
|
||||
import { readFileSync } from 'fs'
|
||||
import * as fsp from 'fs/promises'
|
||||
import _ from 'lodash'
|
||||
import hashObject from 'object-hash'
|
||||
import path from 'path'
|
||||
import React from 'react'
|
||||
import sharp from 'sharp'
|
||||
import sharp, { ResizeOptions } from 'sharp'
|
||||
|
||||
const shouldSkip = (
|
||||
frontMatter:
|
||||
| DocFrontMatter
|
||||
| BlogPostFrontMatter
|
||||
| MDXPageMetadata['frontMatter']
|
||||
| undefined
|
||||
| null,
|
||||
) =>
|
||||
frontMatter &&
|
||||
typeof frontMatter['og:generate_image'] !== 'undefined' &&
|
||||
!boolean(frontMatter['og:generate_image'])
|
||||
|
||||
const options: ImageGeneratorOptions = {
|
||||
width: 1200,
|
||||
@@ -26,9 +49,10 @@ const options: ImageGeneratorOptions = {
|
||||
|
||||
const Layout: React.FC<{
|
||||
logo?: React.ReactNode
|
||||
image?: React.ReactNode
|
||||
title: React.ReactNode
|
||||
footer: React.ReactNode | Array<React.ReactNode | undefined | false>
|
||||
}> = ({ title, footer, logo }) => {
|
||||
}> = ({ title, footer, logo, image }) => {
|
||||
const dot = (
|
||||
<div
|
||||
style={{
|
||||
@@ -55,6 +79,69 @@ const Layout: React.FC<{
|
||||
? footer.filter((item) => !!item)
|
||||
: [footer]
|
||||
|
||||
if (image) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
color: 'white',
|
||||
background: 'black',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '50%',
|
||||
height: '100%',
|
||||
color: 'white',
|
||||
background: 'black',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '48px 40px',
|
||||
gap: '16px',
|
||||
justifyContent: 'space-between',
|
||||
flexBasis: '50%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', height: '120px', overflow: 'hidden' }}>
|
||||
{logo}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ fontSize: '54px', display: 'flex' }}>{title}</div>
|
||||
{footerItems.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '24px',
|
||||
fontSize: '32px',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: '0 24px',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex' }}>{footerItems[0]}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '50%',
|
||||
height: '100%',
|
||||
flexBasis: '50%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{image}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -99,9 +186,10 @@ const Layout: React.FC<{
|
||||
const docsImageRenderer = imageRendererFactory(
|
||||
'docusaurus-plugin-content-docs',
|
||||
async (doc, { outDir, siteConfig }) => {
|
||||
if (doc.metadata.frontMatter.image) return
|
||||
if (shouldSkip(doc.metadata.frontMatter)) return
|
||||
|
||||
const logo = await getLogo(siteConfig, outDir)
|
||||
const image = await getPageImage('docs', outDir, doc)
|
||||
|
||||
return [
|
||||
<Layout
|
||||
@@ -114,7 +202,16 @@ const docsImageRenderer = imageRendererFactory(
|
||||
<span>{doc.version.label}</span>
|
||||
),
|
||||
]}
|
||||
logo={<img src={logo.src} style={{ height: 120 }} />}
|
||||
logo={logo && <img src={logo.src as any} style={{ height: 120 }} />}
|
||||
image={
|
||||
image && (
|
||||
<img
|
||||
src={image.src as any}
|
||||
width={image.width}
|
||||
height={image.height}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>,
|
||||
options,
|
||||
]
|
||||
@@ -124,12 +221,12 @@ const docsImageRenderer = imageRendererFactory(
|
||||
const blogImageRenderer = imageRendererFactory(
|
||||
'docusaurus-plugin-content-blog',
|
||||
async (page, { outDir, siteConfig }) => {
|
||||
if (page.pageType === 'post' && !!page.data.metadata.frontMatter.image)
|
||||
const { pageType, data } = page
|
||||
if (pageType === 'post' && shouldSkip(page.data.metadata.frontMatter))
|
||||
return
|
||||
|
||||
const { pageType, data } = page
|
||||
|
||||
const logo = await getLogo(siteConfig, outDir)
|
||||
const image = await getPageImage('blog', outDir, page)
|
||||
|
||||
return [
|
||||
<Layout
|
||||
@@ -153,7 +250,16 @@ const blogImageRenderer = imageRendererFactory(
|
||||
.map((author) => author.name)
|
||||
.join(', ')}`,
|
||||
]}
|
||||
logo={<img src={logo.src} style={{ height: 120 }} />}
|
||||
logo={logo && <img src={logo.src as any} style={{ height: 120 }} />}
|
||||
image={
|
||||
image && (
|
||||
<img
|
||||
src={image.src as any}
|
||||
width={image.width}
|
||||
height={image.height}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>,
|
||||
options,
|
||||
]
|
||||
@@ -164,43 +270,110 @@ const pagesImageRenderer = imageRendererFactory(
|
||||
'docusaurus-plugin-content-pages',
|
||||
async (page, { outDir, siteConfig }) => {
|
||||
const { metadata, plugin } = page
|
||||
if (shouldSkip(_.get(metadata, 'frontMatter'))) return
|
||||
|
||||
const url = new URL(siteConfig.url)
|
||||
|
||||
const logo = await getLogo(siteConfig, outDir)
|
||||
const image = await getPageImage('page', outDir, page)
|
||||
|
||||
return [
|
||||
<Layout
|
||||
title={metadata.title}
|
||||
footer={url.host}
|
||||
logo={<img src={logo.src} style={{ height: 120 }} />}
|
||||
logo={logo && <img src={logo.src as any} style={{ height: 120 }} />}
|
||||
image={
|
||||
image && (
|
||||
<img
|
||||
src={image.src as any}
|
||||
width={image.width}
|
||||
height={image.height}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>,
|
||||
options,
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
const getLogo = (() => {
|
||||
const cache: Record<string, any> = {}
|
||||
const getLogo = async (siteConfig: DocusaurusConfig, outDir: string) => {
|
||||
const logo: any = (siteConfig.themeConfig as any).navbar.logo
|
||||
|
||||
return async (
|
||||
siteConfig: DocusaurusConfig,
|
||||
outDir: string,
|
||||
): Promise<{ src: any }> => {
|
||||
const logo: any = (siteConfig.themeConfig as any).navbar.logo
|
||||
return await loadImage(outDir, logo.src, { height: 120 })
|
||||
}
|
||||
|
||||
if (cache[logo.src]) return cache[logo.src]
|
||||
const getPageImage = async (
|
||||
type: 'docs' | 'blog' | 'page',
|
||||
outDir: string,
|
||||
page: DocsPageData | BlogPageData | PageData,
|
||||
) => {
|
||||
const frontMatter:
|
||||
| DocFrontMatter
|
||||
| BlogPostFrontMatter
|
||||
| MDXPageMetadata['frontMatter']
|
||||
| null
|
||||
| undefined =
|
||||
type === 'docs'
|
||||
? (page as DocsPageData).metadata.frontMatter
|
||||
: type === 'blog'
|
||||
? _.get(page, 'data.metadata.frontMatter')
|
||||
: _.get(page, 'metadata.frontMatter')
|
||||
|
||||
const isUrl = logo.src.startsWith('http')
|
||||
const image = frontMatter?.image
|
||||
|
||||
if (isUrl) return { src: logo.src }
|
||||
if (typeof image === 'string' && image.length > 0)
|
||||
return await loadImage(outDir, image, {
|
||||
width: options.width / 2,
|
||||
height: options.height,
|
||||
fit: 'cover',
|
||||
})
|
||||
|
||||
const logoPath = path.join(outDir, logo.src)
|
||||
return null
|
||||
}
|
||||
|
||||
const img = sharp(logoPath)
|
||||
const buffer = await img.resize({ height: 120 }).toBuffer()
|
||||
type LoadedImage = {
|
||||
src: string | ArrayBuffer
|
||||
width: number | undefined
|
||||
height: number | undefined
|
||||
}
|
||||
|
||||
cache[logo.src] = { src: buffer.buffer }
|
||||
return cache[logo.src]
|
||||
const loadImage = (() => {
|
||||
const cache: Record<string, LoadedImage> = {}
|
||||
|
||||
const download = async (url: string) =>
|
||||
axios
|
||||
.get(url, {
|
||||
responseType: 'arraybuffer',
|
||||
})
|
||||
.then((response) => Buffer.from(response.data, 'binary'))
|
||||
|
||||
return async (outDir: string, url: string, resize?: ResizeOptions) => {
|
||||
const cacheKey = hashObject({ url, resize })
|
||||
|
||||
if (cache[cacheKey]) return cache[cacheKey]
|
||||
|
||||
const buffer: Buffer = url.startsWith('data:')
|
||||
? Buffer.from(url, 'base64')
|
||||
: url.startsWith('http:')
|
||||
? await download(url)
|
||||
: await fsp.readFile(path.join(outDir, url))
|
||||
|
||||
let img = sharp(buffer)
|
||||
if (resize) img = img.resize({ ...resize })
|
||||
|
||||
const output = await img.toBuffer()
|
||||
const outputImage = sharp(output)
|
||||
const outputMetadata = await outputImage.metadata()
|
||||
const arrayBuffer = output.buffer
|
||||
|
||||
const loadedImage: LoadedImage = (cache[cacheKey] = {
|
||||
src: arrayBuffer,
|
||||
width: outputMetadata.width,
|
||||
height: outputMetadata.height,
|
||||
})
|
||||
|
||||
return loadedImage
|
||||
}
|
||||
})()
|
||||
|
||||
|
||||
@@ -4501,7 +4501,7 @@ axios@^0.27.0:
|
||||
follow-redirects "^1.14.9"
|
||||
form-data "^4.0.0"
|
||||
|
||||
axios@^1.0.0:
|
||||
axios@^1.0.0, axios@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f"
|
||||
integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==
|
||||
@@ -4840,6 +4840,11 @@ boolbase@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||
integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
|
||||
|
||||
boolean@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b"
|
||||
integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==
|
||||
|
||||
boxen@^5.0.0:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50"
|
||||
|
||||
Reference in New Issue
Block a user