diff --git a/docs.config.js b/docs.config.js index 5fec13fd4..7a40b6fae 100644 --- a/docs.config.js +++ b/docs.config.js @@ -1,5 +1,11 @@ const config = { - DOCS_LATEST_VERSION: '7.7' + DOCS_LATEST_VERSION: '7.7', + // Remote image origins that Next's optimizer may fetch, convert to WebP and cache. + OPTIMIZED_IMAGE_ORIGINS: [ + 'https://sourcegraphstatic.com/', + 'https://storage.googleapis.com/sourcegraph-assets/', + 'https://storage.googleapis.com/changelog-static-assets-prod/' + ] }; module.exports = config; diff --git a/next.config.js b/next.config.js index 2603ca187..5024f0ea1 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,5 @@ const {withContentlayer} = require('next-contentlayer2'); +const {OPTIMIZED_IMAGE_ORIGINS} = require('./docs.config'); /** @type {import('next').NextConfig} */ // in prod, we serve the docs from sourcegraph.com/docs, and this requires special config on the GFE side @@ -18,6 +19,12 @@ const nextConfig = { env: { NEXT_PUBLIC_DOCS_BASE_PATH: basePath }, + // MDX screenshots are uncompressed PNGs on GCS with a one-hour cache. ZoomableImage + // routes them through `/_next/image`, which serves WebP from Vercel's edge cache. + images: { + remotePatterns: OPTIMIZED_IMAGE_ORIGINS.map(origin => new URL(`${origin}**`)), + minimumCacheTTL: 60 * 60 * 24 * 7 + }, // With basePath set, nothing serves `/`, so the *.vercel.app deployment URL // that Vercel links from Slack / GitHub 404s. sourcegraph.com never proxies // `/` to us, so this only affects visitors opening that raw URL. Stay on the diff --git a/src/components/mdx/ZoomableImage.tsx b/src/components/mdx/ZoomableImage.tsx index ef395215c..daba9fca6 100644 --- a/src/components/mdx/ZoomableImage.tsx +++ b/src/components/mdx/ZoomableImage.tsx @@ -2,9 +2,23 @@ import {useCallback, useEffect, useState} from 'react'; +import docsConfig from '../../../docs.config'; + interface ZoomableImageProps extends React.ImgHTMLAttributes {} -export function ZoomableImage({className, alt, ...props}: ZoomableImageProps) { +/** + * Serve allow-listed remote PNG/JPEGs through Next's image optimizer: WebP, at most + * 1920px wide (enough for the content column on a 2x display), cached at the edge. + * The width must be one of Next's default `deviceSizes`. + */ +function optimizedSrc(src: ZoomableImageProps['src']) { + if (typeof src !== 'string' || !/\.(png|jpe?g)$/i.test(src)) return src; + if (!docsConfig.OPTIMIZED_IMAGE_ORIGINS.some(origin => src.startsWith(origin))) return src; + const basePath = process.env.NEXT_PUBLIC_DOCS_BASE_PATH ?? ''; + return `${basePath}/_next/image?url=${encodeURIComponent(src)}&w=1920&q=75`; +} + +export function ZoomableImage({className, alt, src, ...props}: ZoomableImageProps) { const [isOpen, setIsOpen] = useState(false); const openModal = useCallback(() => setIsOpen(true), []); @@ -32,6 +46,9 @@ export function ZoomableImage({className, alt, ...props}: ZoomableImageProps) { {alt} @@ -68,6 +85,7 @@ export function ZoomableImage({className, alt, ...props}: ZoomableImageProps) { {alt} e.stopPropagation()} {...props} />