Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/page-builder-image-primitive-optimizer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@frontlit/page-builder": patch
---

The `Image` primitive now goes through the host's image resolver, like block images, so blog, docs and feed images are resized by the host's optimizer. It also accepts `priority` for above-the-fold images. The resolver is shared across the package's entry points.
1 change: 1 addition & 0 deletions apps/sites/components/themed-content-feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export async function ThemedArticleFeed({
src={imageUrl}
alt={imageAlt}
theme={theme}
sizes="(min-width: 1024px) 1024px, 100vw"
className="aspect-[16/9] max-h-72 w-full object-cover transition-transform duration-300 group-hover:scale-105"
/>
</div>
Expand Down
2 changes: 2 additions & 0 deletions apps/sites/components/themed-docs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,8 @@ export async function ThemedDocArticle({
src={imageUrl}
alt={imageAlt}
theme={theme}
priority
sizes="(min-width: 1024px) 768px, 100vw"
className="aspect-[2/1] max-h-[400px] w-full object-cover"
/>
</div>
Expand Down
2 changes: 2 additions & 0 deletions apps/sites/components/themed-document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export async function ThemedBlogPost({
src={imageUrl}
alt={imageAlt}
theme={theme}
priority
sizes="(min-width: 1024px) 1024px, 100vw"
className="aspect-[2/1] max-h-[480px] w-full object-cover"
/>
</div>
Expand Down
17 changes: 14 additions & 3 deletions packages/page-builder/src/components/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,25 @@ export type ImageAttributesResolver = (
input: ImageAttributesInput,
) => React.ImgHTMLAttributes<HTMLImageElement> | null;

let imageAttributesResolver: ImageAttributesResolver | null = null;
// Kept on `globalThis`: each package entry (`/server`, `/primitives/server`)
// bundles its own copy of this module, and they must share one resolver.
const registry = globalThis as {
__frontlitImageResolver?: ImageAttributesResolver | null;
};

/**
* Registers the host's resolver. Call once at server start-up; renders that
* run without one emit the original URL.
*/
export function setImageAttributesResolver(resolver: ImageAttributesResolver | null) {
imageAttributesResolver = resolver;
registry.__frontlitImageResolver = resolver;
}

/** The registered resolver's attributes for an image, if any. */
export function resolveImageAttributes(
input: ImageAttributesInput,
): React.ImgHTMLAttributes<HTMLImageElement> | null {
return registry.__frontlitImageResolver?.(input) ?? null;
}

export type ImageObjectFit = "cover" | "contain" | "fill" | "none" | "scale-down";
Expand Down Expand Up @@ -106,7 +117,7 @@ export const Image = (props: ImgProps) => {
);
}

const optimized = imageAttributesResolver?.({ src: source, alt, sizes, priority });
const optimized = resolveImageAttributes({ src: source, alt, sizes, priority });

return (
<div
Expand Down
21 changes: 20 additions & 1 deletion packages/page-builder/src/primitives/image.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,42 @@
import type React from "react";
import type { ThemeStyle } from "../models";
import { resolveImageAttributes } from "../components/image";
import { cn } from "./lib/utils";

export interface ImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
src: string;
alt?: string;
className?: string;
theme?: ThemeStyle;
/** Above-the-fold image (e.g. LCP): fetched eagerly at high priority. */
priority?: boolean;
}

export const Image: React.FC<ImageProps> = ({
src,
alt = "",
className = "",
theme,
priority = false,
sizes,
...props
}) => {
const cardStyles = theme?.interactives?.card;
const classes = cn("w-full object-cover", cardStyles?.border?.radius, className);
// The host's optimizer (if registered) serves a resized, modern-format copy.
const optimized = resolveImageAttributes({ src, alt, sizes, priority });

return <img src={src} alt={alt} className={classes} {...props} />;
return (
<img
src={src}
alt={alt}
sizes={sizes}
loading={priority ? "eager" : "lazy"}
decoding="async"
fetchPriority={priority ? "high" : undefined}
className={classes}
{...props}
{...optimized}
/>
);
};
Loading