Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs.config.js
Original file line number Diff line number Diff line change
@@ -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;
7 changes: 7 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
20 changes: 19 additions & 1 deletion src/components/mdx/ZoomableImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,23 @@

import {useCallback, useEffect, useState} from 'react';

import docsConfig from '../../../docs.config';

interface ZoomableImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {}

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), []);
Expand Down Expand Up @@ -32,6 +46,9 @@ export function ZoomableImage({className, alt, ...props}: ZoomableImageProps) {
<img
className={`cursor-zoom-in rounded-xl ${className ?? ''}`}
alt={alt}
src={optimizedSrc(src)}
loading="lazy"
decoding="async"
onClick={openModal}
{...props}
/>
Expand Down Expand Up @@ -68,6 +85,7 @@ export function ZoomableImage({className, alt, ...props}: ZoomableImageProps) {
<img
className="max-h-[90vh] max-w-[90vw] cursor-zoom-out rounded-lg object-contain"
alt={alt}
src={src}
onClick={e => e.stopPropagation()}
{...props}
/>
Expand Down
Loading