1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106 | ---
/**
* A photograph with a caption and, optionally, the camera settings.
*
* Astro re-encodes the source through sharp into AVIF and WebP at several
* widths and emits a srcset, so a phone downloads a phone-sized file. That
* re-encode also strips every scrap of metadata from the served image — which
* is the privacy behaviour we want, and the reason the EXIF you *do* want
* shown has to be rendered here as text. See lib/exif.ts.
*
* <Figure src={photo} alt="Fog over the lake" width="wide" exif />
*/
import { Picture } from 'astro:assets';
import type { ImageMetadata } from 'astro';
import { readExif, exifLine } from '../lib/exif.js';
interface Props {
src: ImageMetadata;
alt: string;
caption?: string;
/** Which grid track to occupy. */
width?: 'content' | 'wide' | 'full';
/** Render the camera settings under the caption. */
exif?: boolean;
/** Above-the-fold images should not lazy-load. */
priority?: boolean;
}
const { src, alt, caption, width = 'content', exif = false, priority = false } = Astro.props;
const settings = exif ? exifLine(await readExif(src)) : null;
---
<figure class:list={[width === 'wide' && 'u-wide', width === 'full' && 'u-full']}>
{/*
Picture, not Image, for two reasons. AVIF is meaningfully smaller than
WebP on photographs, and Picture is what emits a <source> per format.
Just as importantly, fallbackFormat="webp" keeps the fallback in a
processed format — an Image falls back to the original file, and the
original is the one copy that still carries the photographer's metadata.
*/}
<Picture
src={src}
alt={alt}
layout="constrained"
widths={[480, 800, 1200, 1600]}
sizes={width === 'full'
? '100vw'
: width === 'wide'
? '(width >= 52rem) 52rem, 100vw'
: '(width >= 34rem) 34rem, 100vw'}
formats={['avif', 'webp']}
fallbackFormat="webp"
loading={priority ? 'eager' : 'lazy'}
decoding={priority ? 'sync' : 'async'}
fetchpriority={priority ? 'high' : 'auto'}
/>
{
(caption || settings) && (
<figcaption>
{caption}
{settings && <span class="exif">{settings}</span>}
</figcaption>
)
}
</figure>
<style>
figure :global(img) {
inline-size: 100%;
background: var(--surface-sunk);
border-radius: var(--radius);
}
/*
* The caption is styled here rather than left to prose.css, because a
* Figure is not always inside a .prose. A photo set renders its grid
* directly into the entry, above the article body — there the prose rules
* never apply, and captions inherited full-strength ink at body size,
* which read as the loudest thing on a page of photographs.
*
* prose.css keeps its own figcaption rule for <figure> elements written by
* hand in Markdown. The two agree; this one just does not depend on
* ancestry.
*/
figcaption {
margin-block-start: var(--space-2xs);
font-family: var(--font-ui);
font-size: var(--step--1);
line-height: var(--leading-prose);
color: var(--ink-muted);
text-wrap: pretty;
}
.exif {
display: block;
margin-block-start: var(--space-3xs);
font-family: var(--font-mono);
font-size: var(--step--2);
font-variant-numeric: tabular-nums;
color: var(--ink-faint);
/* Long lens names should wrap rather than overflow on a phone. */
overflow-wrap: break-word;
}
</style>
|