Skip to content
← Public packages

@kentcdodds/christ-paintings

Official Latter-day Saint paintings of Jesus Christ, one at a time, to show children during church, plus coloring pages of the Savior. Swipe to move. Works offline.

FollowForkOpen app

AGENTS.md

216 lines · 11.5 KB · Markdown
Raw

christ-paintings (agent notes)

Package app with no credentials. One Worker fetch handler renders the viewer, coloring, and About pages with inline CSS and JS, and serves the Church's image files from a same-origin route. Only PNG icons are static kody.app.assets under public/. Only packageStorage use is the publish version record.

Images are NOT stored in the repo. Kody's publish check counts binary files at about 1.8x their size (they are measured as lossily decoded UTF-8), against a 15 MiB source limit, so about 8 MB of JPEGs is the real ceiling. Instead src/images.ts serves {appBase}/img/p/{id}.jpg and {appBase}/img/c/{id}.jpg by fetching the curated Church URL (only ids in the lists; not an open proxy), with caches.default when the runtime has it. Same origin keeps the service worker cache normal (no opaque responses) and keeps the coloring canvas untainted.

Layout

  • src/app.ts: kody.app.entry. Routes (mount-stripped): / (viewer), /coloring, /about, /img/p/{id}.jpg, /img/c/{id}.jpg, /api/version, /manifest.webmanifest, /icon.svg, /og.jpg, /sw.js. Canonical, og:url, and og:image are absolute URLs built from the request's forwarded host (publicOrigin()), so a fork or a new domain needs no code change.
  • src/og-image.ts: GENERATED base64 of /og.jpg (1200x630) by node scripts/render-og.mjs [appUrl] (headless Chrome screenshot of the real app showing "Jesus the Christ" by Del Parson, plus live text). Run it against kody dev --app-base-path / (for example http://127.0.0.1:4598/) with OG_SITE_LABEL=your.domain to change the address at the bottom. Needs playwright-core resolvable (node_modules is gitignored) and the Cormorant Garamond and Inter fonts. No em dashes in its text. Anything else 404, non GET/HEAD 405. Icons at {assetBasePath}/icons/*.png. All URLs come from packageContext.
  • src/images.ts: the image route (id to Church URL map, fetch, long cache headers), allImagePaths(), totalImageBytes().
  • src/coloring-page.ts: coloring picker and editor (HTML, CSS, JS in String.raw, same no backticks rule) and the 16 color palette.
  • src/client-script.ts: viewer JS as a String.raw template (no backticks or template placeholders inside). src/styles.ts: viewer CSS, same rule.
  • src/paintings.ts and src/coloring.ts: GENERATED by node scripts/gen-paintings.mjs from scripts/paintings.json and scripts/coloring.json. Do not edit by hand.
  • scripts/paintings.json, scripts/coloring.json: the curated lists with each image's Media Library slug, title (and artist for paintings) exactly as the Church gives them, source page, imageUrl (the Church file the app serves), bytes, ipCode, and full Church description.
  • scripts/make-icons.py: regenerates public/icons/* and .kody/icon.png (Pillow).
  • src/version.ts, src/record-version.ts, src/update-script.ts: publish metadata, update check, and update toast, copied from the package-app-kit starter-remix pattern (same as @kentcdodds/pocket-piano).
  • src/about-page.ts: About page (how to use, offline progress, credits with a link to every image's Church page, terms note, version card with Check for updates). CODE_URL is the one constant for the "View this app on Kody" button (Source and fork on Kody section); forks change it.
  • src/index.ts: default export listPaintings({}).
  • test/paintings.test.ts: npm test (node --test test/). Checks ids, source and image URLs, titles, artists, sizes, that the image route only serves curated ids, the offline total, and no em dashes in src, tests, or docs.

Image rules (keep these when adding or swapping paintings)

  • Only images from the Church's Gospel Media Library (https://www.churchofjesuschrist.org/media/image/<slug>), and only ones that depict Jesus Christ.
  • The page's creditLine.ipCode must be CODE_0 or CODE_7 (the page shows only the standard Gospel Media terms). Skip CODE_4 (shown as Church Use Only), CODE_1 (Do Not Copy or not downloadable), and anything whose description says DO NOT COPY, Church use only, or Church purposes only. CODE_4MR (Bloch museum loans), CODE_0MR, CODE_0E, and CODE_9 were also skipped to stay conservative.
  • Title and artist are copied exactly from the Church page. When the Media Library description has no artist, the artist may come from the Church's Gospel Art Book page (artistSource), and the About page says so. If neither names the artist, leave the image out.
  • Use the LARGE download (1280 tall or 1920 wide). When it is wider than 1600 px (or not a JPEG), use the Church's own IIIF rendition of the same image at 1600 wide as JPEG (/imgs/<assetId>/full/1600,/0/default.jpg). largeDownloadUrl keeps the original LARGE link. Put the URL (ending in default.jpg) in imageUrl and its size in bytes.
  • Coloring pages follow the same rules (CODE_0 only, exact title, must show the Savior), from the Media Library Coloring Pages collections. No artist is shown for them because the Church pages do not name one.
  • Add to the JSON, run node scripts/gen-paintings.mjs and npm test. The whole offline download (all images) is about 14.7 MB; the test keeps it under 18 MB.

Coloring

  • /coloring opens a picker: a CSS scroll-snap track (native swipe), one card per page, with Previous, Next, and Color this page buttons. The card shows saved progress under the line art. Swiping only happens in the picker.
  • The editor stacks a paint canvas under the line art img (mix-blend-mode: multiply), so the lines always stay on top. The canvas is the line art size capped at 1280 px on the long side.
  • Fill: walls are line art pixels darker than luminance 175. A tap floods the enclosed non-wall area (ignoring earlier paint, so tapping again recolors), then grows 2 px into the lines so no white halo shows. A tap on a line snaps to the nearest open pixel within 8 px.
  • Brush sizes small, medium, large (8, 18, 36 CSS px), eraser 30 px (destination-out). One pointer only; a second finger cancels and undoes the stroke. Undo keeps 10 ImageData snapshots. Clear asks first.
  • Progress: IndexedDB christ-paintings-coloring, store pages, key = page id, value { blob: PNG of the paint layer, updated }, saved 700 ms after each change and on pagehide. A blank page deletes its record.
  • Share: composites white, paint, and line art (multiply) to a PNG; uses navigator.share({ files }) when canShare allows, otherwise downloads it.
  • Opening the editor pushes a history entry so the phone's back button returns to the picker. localStorage christ-paintings:coloring:index remembers the picker page.
  • Test hooks: window.christColoring (state, pages, open(i), close(), fillAt(x, y), flush(), pixel(x, y), wallAt(x, y), size()).

Viewer behavior

  • Three .slide figures hold the previous, current, and next painting; each img is object-fit: contain on a dark background. Slides move with translate3d; after a move the off-screen slide is recycled to the far side, so neighbors are always loaded. Two more in each direction are warmed with new Image().
  • Pointer events on #viewer (touch-action: none). A horizontal drag follows the finger and commits past 18% of the width or a flick faster than 0.35 px/ms (and 24 px). Otherwise it snaps back. A tap (under 10 px, 600 ms) in the left or right 22% goes back or forward; a tap in the middle toggles the caption. Arrow keys, PageUp/PageDown, Space, Home, End work on desktop. The list wraps around.
  • localStorage: christ-paintings:current (painting id), christ-paintings:caption (hidden or shown), plus the update check keys christ-paintings-last-update-check and christ-paintings-update-dismissed-<sha>.
  • Screen Wake Lock is requested on load, on every pointerdown, and when the page becomes visible again.
  • The caption holds the only links: the palette (Coloring) and the small circle (About). The main screen has no other UI by design, except the update toast when a new version exists.
  • Test hooks: window.christPaintings.state (index, id, count, captionHidden, animating, dragging, moves, wakeLock, loaded), plus next(), prev(), show(i).

Service worker and offline

  • /sw.js (scope {appBase}/): shell pages cache first with background refresh, manifest and icons network first, /api/* never cached, Kody gate responses (403, redirected, x-kody-handoff) never cached, navigations with a query string go straight to the network (session handoff).
  • Shell pages: /, /coloring, /about.
  • All images ({appBase}/img/) are cache first in the christ-paintings-images cache. The worker's script embeds the full list (ALL_IMAGES). On activate, and whenever a page posts WARM_ALL, it downloads every missing image one at a time, drops ones no longer listed, stops if the gate answers, and posts WARM_PROGRESS { done, total, running, failed } to open pages. The About page shows a progress bar and a Save everything for offline button.
  • Bump VERSION in src/app.ts to rotate the shell cache. Change an image id (or path) if the Church file it points to changes, since the image cache is keyed by path.
  • The manifest link keeps crossorigin="use-credentials" because the app is behind Kody's session gate.

After every publish

import recordVersion from 'kody:@kentcdodds/christ-paintings/record-version'

await recordVersion({ sha: '<published_commit>', message: '<commit subject>', committedAt: '<ISO>' })

Call this package's export, never the kit's.

Imports

import listPaintings from 'kody:@kentcdodds/christ-paintings'

const { count, paintings, coloringPages } = await listPaintings({})
// paintings: [{ id, title, artist, sourceUrl }], coloringPages: [{ id, title, sourceUrl }]

Smoke tests

  • Local: kody dev --app-base-path /christ-paintings, then the Playwright checks (Pixel 7, CDP Input.dispatchTouchEvent swipes, taps, and strokes, fills checked by reading canvas pixels, offline reload of a never-opened painting and coloring page, Page.getInstallabilityErrors in a persistent context).
  • Git lane: npm test, kody dev check, commit, push, then packagePublishExternalPush, then recordVersion.
  • After publish, packageAppFetch on /, /about, /manifest.webmanifest, /sw.js, /coloring, one /img/p/...jpg, one /img/c/...jpg, and /api/version should be 200 with the right content type.

Image route security

The app is internet-visible, so /img/* is anonymous. imageSourceFor() only accepts /img/(p|c)/<slug>.jpg where <slug> is an id in the generated lists, and the upstream URL always comes from that list (never from the request), so there is no open proxy or SSRF. Upstream redirects are not followed. Responses carry cache-control: public, max-age=31536000, immutable and are stored in caches.default when the runtime has it (x-image-cache: hit|miss shows which), so churchofjesuschrist.org is asked rarely. Failures are 502 with no-store.

Forks

  • License: MIT for code only (LICENSE). The images stay under the Church's terms; keep the About credits, links, and terms note.
  • No kody:@... imports and no credentials, so a fork publishes as is. Only kody:runtime is imported.
  • Change CODE_URL in src/about-page.ts, and re-render the og image if you want your own address on it.

Sharing

Public source (forkable, community listing) and internet-visible, served at https://christ-paintings.kentcdodds.com/ and the kody.run mount.

No em dashes in UI text or docs.