You might also like
diff --git a/website-next/src/design-system/BrokenMedia.tsx b/website-next/src/design-system/BrokenMedia.tsx
index e50a2222642..8472d5b3414 100644
--- a/website-next/src/design-system/BrokenMedia.tsx
+++ b/website-next/src/design-system/BrokenMedia.tsx
@@ -1,21 +1,21 @@
-// TODO: Create a better illustration
+import type { CSSProperties } from "react";
type BrokenMediaProps = {
- /** Short reason shown under the icon, e.g. "This image couldn't be loaded." */
message: string;
+ className?: string;
+ style?: CSSProperties;
};
-/**
- * Placeholder shown when a media source (video, image) can't be resolved or
- * loaded: a broken-link illustration with a short message, sized like a 16:9
- * media frame.
- */
-export function BrokenMedia({ message }: BrokenMediaProps) {
+// TODO: Create a better illustration
+export function BrokenMedia({ message, className, style }: BrokenMediaProps) {
return (
- // Rendered as s (not /) so it stays valid phrasing content:
- // markdown wraps a standalone image in a
, and a block-level fallback
- // there would be invalid HTML and trigger a hydration error.
-
+
- {message}
+
+ {message}
+
);
}
diff --git a/website-next/src/design-system/Image.stories.tsx b/website-next/src/design-system/Image.stories.tsx
index 121dac52989..fa364c8b2d8 100644
--- a/website-next/src/design-system/Image.stories.tsx
+++ b/website-next/src/design-system/Image.stories.tsx
@@ -36,7 +36,8 @@ export const Tall: Story = {
/**
* When the source fails to load, the image swaps in the same broken-link
- * placeholder used for unavailable videos.
+ * placeholder used for unavailable videos, sized by the image's intrinsic
+ * aspect ratio.
*/
export const Broken: Story = {
args: {
@@ -46,3 +47,41 @@ export const Broken: Story = {
height: 360,
},
};
+
+/**
+ * Inside a card (e.g. a blog teaser), the placeholder adopts the image's own
+ * layout classes and fills the card's media frame edge to edge.
+ */
+export const BrokenInCard: Story = {
+ args: {
+ src: "https://example.invalid/missing.png",
+ alt: "An image that fails to load",
+ width: 640,
+ height: 360,
+ className: "h-full w-full object-cover",
+ },
+ decorators: [
+ (Story) => (
+
+
+
+
+ Card content
+
+ ),
+ ],
+};
+
+/**
+ * In tiny frames like avatars, the icon shrinks and the message is kept for
+ * screen readers only.
+ */
+export const BrokenAvatar: Story = {
+ args: {
+ src: "https://example.invalid/missing.png",
+ alt: "An avatar that fails to load",
+ width: 30,
+ height: 30,
+ className: "h-[30px] w-[30px] rounded-full object-cover",
+ },
+};
diff --git a/website-next/src/design-system/Image.tsx b/website-next/src/design-system/Image.tsx
index 821ee0e9349..bb2a8ca82b8 100644
--- a/website-next/src/design-system/Image.tsx
+++ b/website-next/src/design-system/Image.tsx
@@ -74,7 +74,21 @@ export function Image({
}, []);
if (broken) {
- return ;
+ // The placeholder takes over the image's own layout classes and intrinsic
+ // aspect ratio so it occupies the same box the image would have. Without a
+ // className it falls back to BrokenMedia's standalone media frame.
+ const { width, height } = props;
+ return (
+
+ );
}
const showBlur = !loaded && !!blurDataURL;
diff --git a/website-next/src/helpers/buildContentTree.ts b/website-next/src/helpers/buildContentTree.ts
index e06fa52186c..5c85481bb0d 100644
--- a/website-next/src/helpers/buildContentTree.ts
+++ b/website-next/src/helpers/buildContentTree.ts
@@ -92,3 +92,44 @@ function walk(dirRel: string, urlPrefix: string): TreeNode[] {
return nodes;
}
+
+export type Breadcrumb = { name: string; href: string | null };
+
+/**
+ * Resolves the navigation titles along a docs slug into breadcrumb entries by
+ * locating the page in the product's navigation tree. The first entry is the
+ * product itself (titled by its `structure.yaml`); the rest are the tree nodes
+ * on the path to the page. `href` is null for groups without an index page.
+ */
+export function docBreadcrumbs(slug: string[]): Breadcrumb[] {
+ const product = slug[0];
+ const rootRel = `docs/${product}`;
+ if (!fs.existsSync(abs(`${rootRel}/${META_FILE}`))) {
+ return [];
+ }
+
+ const productCrumb: Breadcrumb = {
+ name: readMeta(rootRel).title,
+ href: `/docs/${product}`,
+ };
+ if (slug.length === 1) {
+ return [productCrumb];
+ }
+
+ const tree = buildContentTree(rootRel, `/docs/${product}`);
+ const trail = findTrail(tree, `/docs/${slug.join("/")}`);
+ return trail ? [productCrumb, ...trail] : [productCrumb];
+}
+
+function findTrail(nodes: TreeNode[], targetHref: string): Breadcrumb[] | null {
+ for (const node of nodes) {
+ if (node.href === targetHref) {
+ return [{ name: node.title, href: node.href }];
+ }
+ const sub = findTrail(node.children, targetHref);
+ if (sub !== null) {
+ return [{ name: node.title, href: node.href }, ...sub];
+ }
+ }
+ return null;
+}
diff --git a/website-next/src/image-optimization/manifest.ts b/website-next/src/image-optimization/manifest.ts
index 313e2da42da..af28bd15e4d 100644
--- a/website-next/src/image-optimization/manifest.ts
+++ b/website-next/src/image-optimization/manifest.ts
@@ -21,6 +21,11 @@ export interface OptimizedImage {
let cache: Record | null | undefined;
function load(): Record | null {
+ // In development, always render the original image from public/ so new or
+ // changed images show up without running `optimize-images` first.
+ if (process.env.NODE_ENV === "development") {
+ return null;
+ }
if (cache !== undefined) {
return cache;
}
) so it stays valid phrasing content: - // markdown wraps a standalone image in a
, and a block-level fallback
- // there would be invalid HTML and trigger a hydration error.
-
+
- {message}
+
+ {message}
+
);
}
diff --git a/website-next/src/design-system/Image.stories.tsx b/website-next/src/design-system/Image.stories.tsx
index 121dac52989..fa364c8b2d8 100644
--- a/website-next/src/design-system/Image.stories.tsx
+++ b/website-next/src/design-system/Image.stories.tsx
@@ -36,7 +36,8 @@ export const Tall: Story = {
/**
* When the source fails to load, the image swaps in the same broken-link
- * placeholder used for unavailable videos.
+ * placeholder used for unavailable videos, sized by the image's intrinsic
+ * aspect ratio.
*/
export const Broken: Story = {
args: {
@@ -46,3 +47,41 @@ export const Broken: Story = {
height: 360,
},
};
+
+/**
+ * Inside a card (e.g. a blog teaser), the placeholder adopts the image's own
+ * layout classes and fills the card's media frame edge to edge.
+ */
+export const BrokenInCard: Story = {
+ args: {
+ src: "https://example.invalid/missing.png",
+ alt: "An image that fails to load",
+ width: 640,
+ height: 360,
+ className: "h-full w-full object-cover",
+ },
+ decorators: [
+ (Story) => (
+