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
48 changes: 43 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,14 @@
"types": "tsc --noEmit"
},
"dependencies": {
"@prismicio/react": "^3.3.0",
"@prismicio/simulator": "^0.2.0",
"imgix-url-builder": "^0.0.5",
"lz-string": "^1.5.0"
},
"devDependencies": {
"@playwright/test": "^1.49.1",
"@prismicio/client": "^7.12.0",
"@prismicio/client": "^7.21.3",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^22.10.2",
"@types/react": "^19.2.3",
Expand Down
187 changes: 187 additions & 0 deletions src/PrismicImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
"use client";

import { imgixLoader } from "./imgixLoader";
import { devMsg } from "./lib/devMsg";
import { resolveDefaultExport } from "./lib/resolveDefaultExport";
import type { ImageFieldImage } from "@prismicio/client";
import { isFilled } from "@prismicio/client";
import type { ImgixURLParams } from "imgix-url-builder";
import { buildURL } from "imgix-url-builder";
import type { ImageProps } from "next/image";
import Image from "next/image";
import type {
ForwardRefExoticComponent,
PropsWithoutRef,
RefAttributes,
} from "react";
import { forwardRef } from "react";

/**
* @deprecated Use `PrismicImage` instead.
*/
export { PrismicImage as PrismicNextImage };

/**
* @deprecated Use `PrismicImageProps` instead.
*/
export type { PrismicImageProps as PrismicNextImageProps };

const castInt = (input: string | number | undefined): number | undefined => {
if (typeof input === "number" || typeof input === "undefined") {
return input;
} else {
const parsed = Number.parseInt(input);

if (Number.isNaN(parsed)) {
return undefined;
} else {
return parsed;
}
}
};

export type PrismicImageProps = Omit<ImageProps, "src" | "alt" | "loader"> & {
/** The Prismic Image field or thumbnail to render. */
field: ImageFieldImage | null | undefined;

/**
* An object of Imgix URL API parameters to transform the image.
*
* @see https://docs.imgix.com/apis/rendering
*/
imgixParams?: { [P in keyof ImgixURLParams]: ImgixURLParams[P] | null };

/**
* Declare an image as decorative by providing `alt=""`.
*
* See:
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/alt#decorative_images
*/
alt?: "";

/**
* Declare an image as decorative only if the Image field does not have
* alternative text by providing `fallbackAlt=""`.
*
* See:
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/alt#decorative_images
*/
fallbackAlt?: "";

/**
* Rendered when the field is empty. If a fallback is not given, `null` will
* be rendered.
*/
fallback?: React.ReactNode;

loader?: ImageProps["loader"] | null;
};

/**
* React component that renders an image from a Prismic Image field or one of
* its thumbnails using `next/image`. It will automatically set the `alt`
* attribute using the Image field's `alt` property.
*
* It uses an Imgix URL-based loader by default. A custom loader can be provided
* with the `loader` prop. If you would like to use the Next.js Image
* Optimization API instead, set `loader={undefined}`.
*
* @param props - Props for the component.
*
* @returns A responsive image component using `next/image` for the given Image
* field.
*
* @see To learn more about `next/image`, see: https://nextjs.org/docs/api-reference/next/image
*/
// The type annotation is necessary to avoid a type reference issue.
export const PrismicImage: ForwardRefExoticComponent<
PropsWithoutRef<PrismicImageProps> & RefAttributes<HTMLImageElement>
> = forwardRef<HTMLImageElement, PrismicImageProps>(function PrismicImage(
{
field,
imgixParams = {},
alt,
fallbackAlt,
fill,
width,
height,
fallback = null,
loader = imgixLoader,
...restProps
},
ref,
) {
if (process.env.NODE_ENV === "development") {
if (typeof alt === "string" && alt !== "") {
console.warn(
`[PrismicImage] The "alt" prop can only be used to declare an image as decorative by passing an empty string (alt="") but was provided a non-empty string. You can resolve this warning by removing the "alt" prop or changing it to alt="". For more details, see ${devMsg(
"alt-must-be-an-empty-string",
)}`,
);
}

if (typeof fallbackAlt === "string" && fallbackAlt !== "") {
console.warn(
`[PrismicImage] The "fallbackAlt" prop can only be used to declare an image as decorative by passing an empty string (fallbackAlt="") but was provided a non-empty string. You can resolve this warning by removing the "fallbackAlt" prop or changing it to fallbackAlt="". For more details, see ${devMsg(
"alt-must-be-an-empty-string",
)}`,
);
}
}

if (!isFilled.imageThumbnail(field)) {
return <>{fallback}</>;
}

const resolvedImgixParams = imgixParams;
for (const x in imgixParams) {
if (resolvedImgixParams[x as keyof typeof resolvedImgixParams] === null) {
resolvedImgixParams[x as keyof typeof resolvedImgixParams] = undefined;
}
}

const src = buildURL(field.url, imgixParams as ImgixURLParams);

const ar = field.dimensions.width / field.dimensions.height;

const castedWidth = castInt(width);
const castedHeight = castInt(height);

let resolvedWidth = castedWidth ?? field.dimensions.width;
let resolvedHeight = castedHeight ?? field.dimensions.height;

if (castedWidth != null && castedHeight == null) {
resolvedHeight = castedWidth / ar;
} else if (castedWidth == null && castedHeight != null) {
resolvedWidth = castedHeight * ar;
}

// A non-null assertion is required since we can't statically
// know if an alt attribute is available.
const resolvedAlt = (alt ?? (field.alt || fallbackAlt))!;

if (
process.env.NODE_ENV === "development" &&
typeof resolvedAlt !== "string"
) {
console.error(
`[PrismicImage] The following image is missing an "alt" property. Please add Alternative Text to the image in Prismic. To mark the image as decorative instead, add one of \`alt=""\` or \`fallbackAlt=""\`.`,
src,
);
}

const ResolvedImage = resolveDefaultExport(Image);

return (
<ResolvedImage
ref={ref}
src={src}
width={fill ? undefined : resolvedWidth}
height={fill ? undefined : resolvedHeight}
alt={resolvedAlt}
fill={fill}
loader={loader === null ? undefined : loader}
{...restProps}
/>
);
});
74 changes: 74 additions & 0 deletions src/PrismicLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { resolveDefaultExport } from "./lib/resolveDefaultExport";
import type {
AsLinkAttrsConfig,
LinkField,
LinkResolverFunction,
PrismicDocument,
} from "@prismicio/client";
import { asLinkAttrs } from "@prismicio/client";
import Link from "next/link";
import type { ComponentProps } from "react";
import { forwardRef } from "react";

/**
* @deprecated Use `PrismicLink` instead.
*/
export { PrismicLink as PrismicNextLink } from "./PrismicLink";

/**
* @deprecated Use `PrismicLinkProps` instead.
*/
export type { PrismicLinkProps as PrismicNextLinkProps } from "./PrismicLink";

export type PrismicLinkProps = Omit<
ComponentProps<typeof Link>,
"field" | "document" | "href" | "rel"
> & {
linkResolver?: LinkResolverFunction;
rel?: string | AsLinkAttrsConfig["rel"];
} & (
| {
field: LinkField | null | undefined;
document?: never;
href?: never;
}
| {
field?: never;
document: PrismicDocument | null | undefined;
href?: never;
}
| {
field?: never;
document?: never;
href: ComponentProps<typeof Link>["href"];
}
);

export const PrismicLink = forwardRef<HTMLAnchorElement, PrismicLinkProps>(
function PrismicLink(props, ref) {
const { field, document, linkResolver, children, ...restProps } = props;
const {
href: computedHref,
rel: computedRel,
...attrs
} = asLinkAttrs(field ?? document, {
linkResolver,
rel: typeof restProps.rel === "function" ? restProps.rel : undefined,
});

const href = ("href" in restProps ? restProps.href : computedHref) || "";

let rel = computedRel;
if ("rel" in restProps && typeof restProps.rel !== "function") {
rel = restProps.rel;
}

const ResolvedLink = resolveDefaultExport(Link);

return (
<ResolvedLink ref={ref} {...attrs} {...restProps} href={href} rel={rel}>
{"children" in props ? children : field?.text}
</ResolvedLink>
);
},
);
Loading