feat: added art a day 0ba191e3
Steve Simkins · 2026-05-08 16:52 1 file(s) · +183 −0
src/pages/art-a-day.astro (added) +183 −0
1 +
---
2 +
export const prerender = false;
3 +
4 +
import PageLayout from "@/layouts/Base.astro";
5 +
import { getFormattedDate } from "@/utils";
6 +
import sanitizeHtml from "sanitize-html";
7 +
8 +
type Artwork = {
9 +
	id: number;
10 +
	title: string;
11 +
	artist_display: string | null;
12 +
	artist_title: string | null;
13 +
	date_display: string | null;
14 +
	medium_display: string | null;
15 +
	dimensions: string | null;
16 +
	place_of_origin: string | null;
17 +
	credit_line: string | null;
18 +
	description: string | null;
19 +
	short_description: string | null;
20 +
	image_id: string | null;
21 +
};
22 +
23 +
const meta = {
24 +
	title: "Art a Day",
25 +
	description:
26 +
		"A different painting every day from the Art Institute of Chicago.",
27 +
};
28 +
29 +
const SEARCH_URL = "https://api.artic.edu/api/v1/artworks/search";
30 +
const FIELDS = [
31 +
	"id",
32 +
	"title",
33 +
	"artist_display",
34 +
	"artist_title",
35 +
	"date_display",
36 +
	"medium_display",
37 +
	"dimensions",
38 +
	"place_of_origin",
39 +
	"credit_line",
40 +
	"description",
41 +
	"short_description",
42 +
	"image_id",
43 +
].join(",");
44 +
45 +
const filterParams = {
46 +
	query: {
47 +
		bool: {
48 +
			must: [
49 +
				{ term: { is_public_domain: true } },
50 +
				{ term: { "classification_title.keyword": "painting" } },
51 +
				{ term: { "artwork_type_title.keyword": "Painting" } },
52 +
				{ exists: { field: "image_id" } },
53 +
			],
54 +
		},
55 +
	},
56 +
};
57 +
const encodedParams = encodeURIComponent(JSON.stringify(filterParams));
58 +
59 +
const today = new Date();
60 +
const dayIndex = Math.floor(today.getTime() / 86_400_000);
61 +
62 +
let artwork: Artwork | null = null;
63 +
let error: string | null = null;
64 +
65 +
const aicHeaders = {
66 +
	"AIC-User-Agent": "stevedylan.dev (stevedsimkins@gmail.com)",
67 +
	"User-Agent": "stevedylan.dev (stevedsimkins@gmail.com)",
68 +
	Accept: "application/json",
69 +
};
70 +
71 +
try {
72 +
	const countRes = await fetch(
73 +
		`${SEARCH_URL}?params=${encodedParams}&limit=1&fields=id`,
74 +
		{ headers: aicHeaders },
75 +
	);
76 +
	if (!countRes.ok) throw new Error(`AIC count returned ${countRes.status}`);
77 +
	const countJson = await countRes.json();
78 +
	const total: number = countJson?.pagination?.total ?? 0;
79 +
	if (total <= 0) throw new Error("no paintings found");
80 +
	// AIC caps from + size at 10000; stay well under.
81 +
	const safeTotal = Math.min(total, 9000);
82 +
	const page = (dayIndex % safeTotal) + 1;
83 +
84 +
	const pickRes = await fetch(
85 +
		`${SEARCH_URL}?params=${encodedParams}&limit=1&page=${page}&fields=${FIELDS}`,
86 +
		{ headers: aicHeaders },
87 +
	);
88 +
	if (!pickRes.ok) throw new Error(`AIC pick returned ${pickRes.status}`);
89 +
	const pickJson = await pickRes.json();
90 +
	artwork = pickJson?.data?.[0] ?? null;
91 +
	if (!artwork) throw new Error("empty result");
92 +
} catch (e) {
93 +
	error = e instanceof Error ? e.message : "Failed to reach AIC API";
94 +
}
95 +
96 +
const imageUrl = artwork?.image_id
97 +
	? `https://www.artic.edu/iiif/2/${artwork.image_id}/full/843,/0/default.jpg`
98 +
	: null;
99 +
const sourceUrl = artwork
100 +
	? `https://www.artic.edu/artworks/${artwork.id}`
101 +
	: null;
102 +
const cleanDescription = artwork?.description
103 +
	? sanitizeHtml(artwork.description, {
104 +
			allowedTags: ["p", "em", "strong", "a", "br", "i", "b"],
105 +
			allowedAttributes: { a: ["href", "title"] },
106 +
		})
107 +
	: null;
108 +
109 +
Astro.response.headers.set(
110 +
	"Cache-Control",
111 +
	"public, max-age=3600, s-maxage=86400",
112 +
);
113 +
---
114 +
115 +
<PageLayout meta={meta}>
116 +
	<div class="flex min-h-screen flex-col items-start justify-start gap-6">
117 +
		<div class="flex flex-col gap-1">
118 +
			<h1 class="title">Art a Day</h1>
119 +
			<p class="text-sm opacity-70">
120 +
				One painting every day from the
121 +
				<a class="style-link" href="https://www.artic.edu/" target="_blank" rel="noopener">Art Institute of Chicago</a>.
122 +
				{getFormattedDate(today)}.
123 +
			</p>
124 +
		</div>
125 +
		{error || !artwork ? (
126 +
			<p class="text-red-400 text-sm">Couldn't load today's painting{error ? `: ${error}` : ""}.</p>
127 +
		) : (
128 +
			<article class="flex flex-col w-full gap-6">
129 +
				{imageUrl && (
130 +
					<img
131 +
						src={imageUrl}
132 +
						alt={artwork.title}
133 +
						loading="eager"
134 +
						class="w-full max-w-2xl h-auto"
135 +
					/>
136 +
				)}
137 +
				<div class="flex flex-col gap-1">
138 +
					<h2 class="text-xl italic text-accent-2">{artwork.title}</h2>
139 +
					{artwork.artist_display && (
140 +
						<p class="text-sm whitespace-pre-line">{artwork.artist_display}</p>
141 +
					)}
142 +
					{artwork.date_display && (
143 +
						<p class="text-sm opacity-70">{artwork.date_display}</p>
144 +
					)}
145 +
				</div>
146 +
				<dl class="grid grid-cols-1 sm:grid-cols-[max-content_1fr] gap-x-6 gap-y-2 text-sm">
147 +
					{artwork.place_of_origin && (
148 +
						<>
149 +
							<dt class="opacity-50 uppercase tracking-widest text-xs">Origin</dt>
150 +
							<dd>{artwork.place_of_origin}</dd>
151 +
						</>
152 +
					)}
153 +
					{artwork.medium_display && (
154 +
						<>
155 +
							<dt class="opacity-50 uppercase tracking-widest text-xs">Medium</dt>
156 +
							<dd>{artwork.medium_display}</dd>
157 +
						</>
158 +
					)}
159 +
					{artwork.dimensions && (
160 +
						<>
161 +
							<dt class="opacity-50 uppercase tracking-widest text-xs">Dimensions</dt>
162 +
							<dd>{artwork.dimensions}</dd>
163 +
						</>
164 +
					)}
165 +
					{artwork.credit_line && (
166 +
						<>
167 +
							<dt class="opacity-50 uppercase tracking-widest text-xs">Credit</dt>
168 +
							<dd>{artwork.credit_line}</dd>
169 +
						</>
170 +
					)}
171 +
				</dl>
172 +
				{cleanDescription && (
173 +
					<div class="prose prose-cactus prose-sm max-w-none" set:html={cleanDescription} />
174 +
				)}
175 +
				{sourceUrl && (
176 +
					<a class="style-link self-start" href={sourceUrl} target="_blank" rel="noopener">
177 +
						View on artic.edu
178 +
					</a>
179 +
				)}
180 +
			</article>
181 +
		)}
182 +
	</div>
183 +
</PageLayout>