diff --git a/packages/logos-docusaurus-theme/package.json b/packages/logos-docusaurus-theme/package.json index 78fcf33..017177f 100644 --- a/packages/logos-docusaurus-theme/package.json +++ b/packages/logos-docusaurus-theme/package.json @@ -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", diff --git a/packages/logos-docusaurus-theme/src/client/components/OGImageRenderer/index.tsx b/packages/logos-docusaurus-theme/src/client/components/OGImageRenderer/index.tsx index 830797e..1cd32cd 100644 --- a/packages/logos-docusaurus-theme/src/client/components/OGImageRenderer/index.tsx +++ b/packages/logos-docusaurus-theme/src/client/components/OGImageRenderer/index.tsx @@ -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 -}> = ({ title, footer, logo }) => { +}> = ({ title, footer, logo, image }) => { const dot = (
!!item) : [footer] + if (image) { + return ( +
+
+
+ {logo} +
+
+
{title}
+ {footerItems.length > 0 && ( +
+
{footerItems[0]}
+
+ )} +
+
+
+ {image} +
+
+ ) + } + return (
{ - 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 [ {doc.version.label} ), ]} - logo={} + logo={logo && } + image={ + image && ( + + ) + } />, 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 [ author.name) .join(', ')}`, ]} - logo={} + logo={logo && } + image={ + image && ( + + ) + } />, 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 [ } + logo={logo && } + image={ + image && ( + + ) + } />, options, ] }, ) -const getLogo = (() => { - const cache: Record = {} +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 = {} + + 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 } })() diff --git a/yarn.lock b/yarn.lock index 9a4337f..11d2f3c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"