diff --git a/src/components/Image/index.tsx b/src/components/Image/index.tsx
index 7de660f1..07d07623 100644
--- a/src/components/Image/index.tsx
+++ b/src/components/Image/index.tsx
@@ -70,6 +70,8 @@ export default function Image({
className = '',
classNames = {},
hideIfError = false,
+ /** Called after internal URL fallbacks are exhausted and the image still failed to load. */
+ onFinalError,
errorPlaceholder = ,
style: wrapperStyleProp,
holdUntilClick = false,
@@ -95,6 +97,7 @@ export default function Image({
/** Caption below the image; defaults to resolved alt when {@link showAltCaption} is true. */
caption?: string
hideIfError?: boolean
+ onFinalError?: () => void
errorPlaceholder?: React.ReactNode
/** Passed to the inner `
` (e.g. profile banner vs avatar load order). */
fetchPriority?: 'high' | 'low' | 'auto'
@@ -291,6 +294,7 @@ export default function Image({
setIsLoading(false)
setDisplaySkeleton(false)
setHasError(true)
+ onFinalError?.()
}
const handleLoad = () => {
diff --git a/src/components/Note/PublicationCoverFallback.tsx b/src/components/Note/PublicationCoverFallback.tsx
index a2abc8b7..913679f6 100644
--- a/src/components/Note/PublicationCoverFallback.tsx
+++ b/src/components/Note/PublicationCoverFallback.tsx
@@ -14,14 +14,17 @@ export default function PublicationCoverFallback({
size?: 'library' | 'default'
className?: string
}) {
- const maxClass = size === 'library' ? LIBRARY_PUBLICATION_COVER_MAX_CLASS : PUBLICATION_COVER_MAX_CLASS
+ const isLibrary = size === 'library'
+ const maxClass = isLibrary ? LIBRARY_PUBLICATION_COVER_MAX_CLASS : PUBLICATION_COVER_MAX_CLASS
+
+ const stackedLayoutClass = isLibrary ? 'aspect-[3/4] w-full' : 'aspect-[3/4] w-48 max-w-full'
return (
(null)
const isNearViewport = useNearViewport(wrapperRef, { enabled: isLibrary })
const maxClass = isLibrary ? LIBRARY_PUBLICATION_COVER_MAX_CLASS : PUBLICATION_COVER_MAX_CLASS
- const resolvedUrl = isLibrary ? gutenbergLibraryCoverImageUrl(imageUrl) : imageUrl
+ const candidateUrls = useMemo(
+ () => gutenbergCoverCandidateUrls(imageUrl, isLibrary),
+ [imageUrl, isLibrary]
+ )
+ const [urlIndex, setUrlIndex] = useState(0)
+ const [exhausted, setExhausted] = useState(false)
+ const activeUrl = candidateUrls[urlIndex] ?? candidateUrls[0] ?? imageUrl.trim()
+
+ useEffect(() => {
+ setUrlIndex(0)
+ setExhausted(false)
+ }, [imageUrl, isLibrary])
+
+ const handleImageError = useCallback(() => {
+ setUrlIndex((index) => {
+ const next = index + 1
+ if (next < candidateUrls.length) return next
+ setExhausted(true)
+ return index
+ })
+ }, [candidateUrls.length])
+
+ if (exhausted || !activeUrl) {
+ return
+ }
+
+ const stackedLayoutClass = isLibrary ? 'aspect-[3/4] w-full' : 'w-fit'
return (
{isNearViewport ? (
) : isFull ? (
-
+
) : null}
{showTitle ? (
diff --git a/src/lib/gutenberg-cover.test.ts b/src/lib/gutenberg-cover.test.ts
index 6a17048d..51854166 100644
--- a/src/lib/gutenberg-cover.test.ts
+++ b/src/lib/gutenberg-cover.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import {
+ gutenbergCoverCandidateUrls,
gutenbergCoverImageUrl,
gutenbergEbookPageUrl,
gutenbergLibraryCoverImageUrl,
@@ -58,6 +59,21 @@ describe('gutenberg-cover', () => {
expect(gutenbergEbookPageUrl('28217')).toBe('https://www.gutenberg.org/ebooks/28217')
})
+ it('gutenbergCoverCandidateUrls tries small then medium in library mode', () => {
+ expect(
+ gutenbergCoverCandidateUrls(
+ 'https://www.gutenberg.org/cache/epub/11/pg11.cover.medium.jpg',
+ true
+ )
+ ).toEqual([
+ 'https://www.gutenberg.org/cache/epub/11/pg11.cover.small.jpg',
+ 'https://www.gutenberg.org/cache/epub/11/pg11.cover.medium.jpg'
+ ])
+ expect(gutenbergCoverCandidateUrls('https://example.com/cover.jpg', true)).toEqual([
+ 'https://example.com/cover.jpg'
+ ])
+ })
+
it('normalizeGutenbergCoverImageUrl converts ebook pages to cover JPG', () => {
expect(normalizeGutenbergCoverImageUrl('https://www.gutenberg.org/ebooks/16702')).toBe(
'https://www.gutenberg.org/cache/epub/16702/pg16702.cover.medium.jpg'
diff --git a/src/lib/gutenberg-cover.ts b/src/lib/gutenberg-cover.ts
index d746f919..7ad36333 100644
--- a/src/lib/gutenberg-cover.ts
+++ b/src/lib/gutenberg-cover.ts
@@ -67,3 +67,20 @@ export function normalizeGutenbergCoverImageUrl(url: string): string {
if (DIRECT_IMAGE_EXT.test(trimmed)) return trimmed
return gutenbergCoverImageUrl(id)
}
+
+/** Ordered cover URLs to try (library prefers small, then medium; always deduped). */
+export function gutenbergCoverCandidateUrls(url: string, preferSmall: boolean): string[] {
+ const trimmed = url.trim()
+ if (!trimmed) return []
+ const id = parseGutenbergEbookId(trimmed)
+ if (!id || !trimmed.toLowerCase().includes('gutenberg')) return [trimmed]
+
+ const ordered: string[] = []
+ if (preferSmall) ordered.push(gutenbergCoverImageUrl(id, 'small'))
+ ordered.push(gutenbergCoverImageUrl(id, 'medium'))
+ const normalized = normalizeGutenbergCoverImageUrl(trimmed)
+ for (const candidate of [normalized, trimmed]) {
+ if (!ordered.includes(candidate)) ordered.push(candidate)
+ }
+ return ordered
+}