Skip to content

canopy-iiif/app

Repository files navigation

Canopy IIIF (Work in Progress)

Deploy to GitHub Pages

Static site generator powered by MDX and IIIF. The stable app entry at app/scripts/canopy-build.mjs orchestrates UI assets and calls the library to build from content/ into site/.

Quick Start

  • Install: npm install
  • Develop: npm run dev (serves http://localhost:5001)
  • Build: npm run build

Entrypoint details

  • Both commands run node app/scripts/canopy-build.mjs.
  • Dev mode starts the UI watcher (@canopy-iiif/appui:watch) and the dev server from @canopy-iiif/app.
  • Build mode builds the UI once, then runs the site build from @canopy-iiif/app.

Content Tree

content/
  _layout.mdx          # optional: site-wide layout wrapper
  _styles.css          # optional: site-wide CSS copied to site/styles.css
  index.mdx            # homepage → site/index.html
  sitemap.mdx          # sitemap page (receives props.pages) → site/sitemap.html
  docs/
    getting-started.mdx
    guide.mdx
  works/
    _layout.mdx        # layout for IIIF manifests (receives props.manifest)

Build output goes to site/. Development cache lives in .cache/:

  • .cache/mdx: transient compiled MDX modules used to render MDX/JSX.
  • .cache/iiif: IIIF cache used by the builder:
    • index.json: primary index storing byId (Collection/Manifest ids to slugs) and collection metadata (uri, hash, updatedAt).
    • manifests/{slug}.json: cached, normalized Manifest JSON per work page.
    • Legacy files like manifest-index.json may be removed as part of migrations.
    • Clear this cache by deleting .cache/iiif/ if you need a fresh fetch.

Assets

Place static files under assets/ and they will be copied to the site root, preserving subpaths. For example:

  • assets/images/example.jpgsite/images/example.jpg
  • assets/downloads/file.pdfsite/downloads/file.pdf

Development

  • Run npm run dev to start a local server at http://localhost:5001 with live reload.
  • Editing MDX under content/ triggers a site rebuild and automatic browser reload.
  • Editing files under assets/ copies only the changed files into site/ (no full rebuild) and reloads the browser.

Tailwind (Canopy IIIF UI)

Canopy ships a small Tailwind preset and plugin so you can opt into sensible defaults with semantic CSS, or disable them entirely.

  • Preset (tokens + plugin): @canopy-iiif/app/ui/canopy-iiif-preset
  • Plugin (component CSS only): @canopy-iiif/app/ui/canopy-iiif-plugin

Defaults (recommended): Canopy now ships a Tailwind config internally, so you only edit app/styles/index.css:

/* app/styles/index.css */
@import 'tailwindcss';
@import '@canopy-iiif/app/ui/styles/index.css';

@theme {
  --color-accent-500: #4f46e5;
  --font-serif: 'Fraunces', Georgia, 'Times New Roman', serif;
}

@utility font-display {
  font-family: var(--font-serif);
  font-weight: 600;
}
  • The preset (design tokens) and plugin (component CSS/utilities) are loaded automatically; just override tokens via @theme (or regular custom properties) when you need custom colors, fonts, radii, etc.
  • Want full control? Create your own tailwind.config.mts and use defineCanopyTailwindConfig(import.meta.url, …) from @canopy-iiif/app/ui/tailwind-config to opt out or add additional presets/plugins.
  • The UI components use semantic selectors (e.g., .canopy-card). Override them in @layer blocks as needed.
  • CSS changes still hot‑swap during npm run dev (Tailwind picks up app/styles/** plus the Canopy UI preset/plugin).

Theme configuration

Add a theme block to canopy.yml to override the default Indigo/Slate palette. Accent and gray colors map to Radix color families (e.g., indigo, cyan, slate). An optional appearance flag switches between the light and dark Radix ramps while keeping token names (50, 100, …) consistent.

theme:
  accentColor: cyan
  grayColor: slate
  appearance: dark # light by default

When appearance: dark is set, Canopy pulls from the *Dark Radix palettes, flips the CSS color-scheme to dark, and continues to expose Tailwind utilities such as bg-brand-500 and text-gray-900 with values appropriate for the darker surface.

IIIF Build

  • Layout: add content/works/_layout.mdx to enable IIIF work page generation. The layout receives props.manifest (normalized to Presentation 3).
  • Source: collection URIs come from canopy.yml (collection, either a string or an array). When omitted, CANOPY_COLLECTION_URI can provide a single fallback URI.
  • Behavior:
    • Recursively walks the collection and subcollections, fetching Manifests.
    • Normalizes resources using @iiif/helpers to v3 where possible.
    • Caches fetched Manifests in .cache/iiif/manifests/ and tracks ids/slugs in .cache/iiif/index.json.
    • Emits one HTML page per Manifest under site/works/<slug>.html.
  • Performance: tune with environment variables — CANOPY_CHUNK_SIZE (default 20) and CANOPY_FETCH_CONCURRENCY (default 5).
  • Cache notes: updating the configured collection URIs resets the manifest cache; you can also delete .cache/iiif/ to force a refetch.

Facet Collections (from IIIF metadata)

Canopy can derive new IIIF Presentation 3 Collections from a single source Collection by faceting on Manifest metadata labels you choose.

  • Configure labels in canopy.yml:
    • metadata: then a YAML list (e.g., - Date, - Subject, - Creator). Matching is case-insensitive (subject and Subject behave the same).
  • Aggregation: during the build, Canopy scans each Manifest’s metadata[] and collects values (across all languages) for the configured labels.
    • Internal cache: .cache/iiif/facets.json (implementation detail; not served).
  • Output (IIIF-only API under site/api/facet/**):
    • /api/facet/index.json: top-level IIIF Collection listing all facet labels.
    • /api/facet/{label}.json: IIIF Collection of child value Collections for that label.
    • /api/facet/{label}/{value}.json: IIIF Collection of Manifests that have that label/value.
    • All id fields are absolute URLs (see Base URL notes below).
  • Items included in value Collections:
    • id: Manifest URI (IIIF Presentation URL for the work)
    • type: Manifest
    • label: { none: [title] }
    • thumbnail: optional, { id, type: 'Image' }
    • homepage: site page for the work (absolute URL), typed as Text

Base URL rules for IIIF ids

  • Absolute ids/links use this priority:
    1. GitHub Actions: auto‑detected in the Pages workflow (owner.github.io[/repo]).
    2. CANOPY_BASE_URL env var (e.g., https://canopy-iiif.github.io/canopy-iiif).
    3. canopy.ymlsite.baseUrl.
    4. Dev default http://localhost:5001 (or PORT env).

Why this is cool

  • From a single IIIF Collection, Canopy synthesizes many new IIIF-compliant Collections (one per facet label, plus one per label/value). These can be browsed, linked to, and reused by external tools that speak the IIIF Presentation API.

Thumbnails

  • Controls come from environment variables:
    • CANOPY_THUMBNAIL_SIZE (default 400) — target width/height in pixels when selecting a thumbnail.
    • CANOPY_THUMBNAILS_UNSAFE (true/1 to enable) — opt into an expanded strategy that may perform extra requests to find a representative image.
  • Behavior: during the IIIF build, a thumbnail URL is resolved for each Manifest and stored on its entry in .cache/iiif/index.json as thumbnail.
  • Safety: with the unsafe flag disabled, a simpler/safer selection is used; enabling it allows additional probing for better imagery when size requirements are stricter.

Interactive Components (SSR + Hydration)

Interactive components render safely in MDX and hydrate client-side:

  • Viewer: <Viewer iiifContent="…" /> — wraps @samvera/clover-iiif and hydrates client-side.
  • Image: <Image iiifContent="…" /> — wraps @samvera/clover-iiif/image to display a single IIIF resource with client-side hydration.
  • Interstitial hero: <Interstitials.Hero … /> — rotates featured IIIF works declared in canopy.yml → featured, falling back to cached manifest thumbnails. Accepts props such as headline, description, links, height, background="theme" | "transparent", index, and random to control the content and layout.
  • Slider: <Slider iiifContent="…" /> — wraps Clover’s slider; hydrates client-side via a separate bundle.
  • Related items: <RelatedItems top={3} iiifContent?="…" /> — facet-driven related sliders.
    • Without iiifContent (e.g., homepage): picks one top value per indexed facet label and renders one slider per label.
    • With iiifContent (work pages): reads the Manifest’s metadata, intersects with indexed facets, then picks one of the Manifest’s values at random for each label and renders exactly one slider per label. Facets not present on the Manifest are skipped.
  • Search (composable): place any of these where you want on the page and they hydrate client-side:
    • <SearchForm /> — search input + submit button wired to the shared store
    • <SearchSummary /> — summary text (query/type aware)
    • <SearchResults /> — results list
    • <SearchTabs /> — type tabs (e.g., work/pages/docs)
    • <SearchTotal /> — live count of visible results

How it works:

  • MDX is rendered on the server. Browser-only components emit a lightweight placeholder element.
  • The build injects site/scripts/react-globals.js and the relevant hydration script(s) into pages that need them.
  • On load, the hydration script finds placeholders, reads props (embedded as JSON), and mounts the React component.
    • Viewer runtime: site/scripts/canopy-viewer.js
    • Interstitial hero runtime: site/scripts/canopy-hero-slider.js (loaded only on pages that include <Interstitials.Hero />)
    • Slider runtime: site/scripts/canopy-slider.js (loaded only on pages that include <Slider />)
    • Related items runtime: site/scripts/canopy-related-items.js (loaded only on pages that include <RelatedItems />)

Usage examples:

// content/index.mdx
<Interstitials.Hero
  headline="Create fast & lightweight digital projects with Canopy IIIF"
  description="Canopy IIIF is an open-source static site generator for creating discovery-focused digital scholarship and collections websites with IIIF APIs."
  background="transparent"
  links={[
    { href: "/search", title: "Browse Collection", type: "primary" },
    { href: "/about", title: "About Canopy", type: "secondary" }
  ]}
/>

## Demo
<Viewer iiifContent="https://api.dc.library.northwestern.edu/api/v2/works/…?as=iiif" />

// content/search/_layout.mdx
# Search
<SearchTabs />
<SearchSummary />
<SearchResults />

Notes:

  • You do not need to import components in MDX; they are auto‑provided by the MDX provider from @canopy-iiif/ui.
  • The Viewer and Search placeholders render minimal HTML on the server and hydrate in the browser.
  • The search runtime (site/search.js) uses FlexSearch and supports filtering by type (e.g., work, page, docs). These subcomponents share a single client store so they stay in sync.

Search Results Layout (Grid vs List)

  • @canopy-iiif/ui exposes a Masonry Grid used by SearchResults.
  • You can control the layout via a prop on the MDX placeholder:
    • <SearchResults layout="grid" /> (default)
    • <SearchResults layout="list" />
  • The Grid is implemented with react-masonry-css and scoped styles; it renders columns client‑side after hydration.

SSR safety and bundling

  • Server render imports UI from @canopy-iiif/ui/server — a server‑safe entry that only exports MDX placeholders and other SSR‑compatible components.
  • Browser UI (@canopy-iiif/ui) is built as an ESM library with externals for heavy globals:
    • Externals: react, react-dom, react-dom/client, react-masonry-css, flexsearch.
  • The search runtime bundles the client UI, and provides shims so those externals resolve to browser globals:
    • React shims come from site/scripts/react-globals.js (injected when needed).
    • FlexSearch is also shimmed to window.FlexSearch in the runtime bundle.

Advanced layout (optional, future):

  • If you need full control over the search page layout, we'll expose a composable Search API (slots or render props) so you can place the form, summary, and results anywhere. Until then, <Search /> renders a sensible default.

Dot‑notation (future): we may also expose these as <Search.Form />, <Search.Results />, <Search.Summary />, <Search.Total /> if needed.

Deploy to GitHub Pages

  • Workflow: .github/workflows/deploy-pages.yml builds site/ and deploys to Pages.
  • Enable Pages: in repository Settings → Pages → set Source to "GitHub Actions" (or use the workflow’s automatic enablement if allowed).
  • Trigger: pushes to main (or run manually via Actions → "Deploy to GitHub Pages").
  • Output: the workflow uploads the site/ folder as the Pages artifact and deploys it.
  • CI tuning (optional):
    • Set CANOPY_CHUNK_SIZE and CANOPY_FETCH_CONCURRENCY to control fetch/build parallelism.
    • Env overrides (in workflow): CANOPY_CHUNK_SIZE, CANOPY_FETCH_CONCURRENCY, and CANOPY_COLLECTION_URI (use a small collection for faster CI).
  • Project Pages base path: links currently use absolute /…. If deploying under /<repo> you may want base‑path aware links; open an issue if you want this wired in.

Template Workflow

  • This repository (app) maintains a separate template repository (template).
  • On push to main, .github/workflows/release-and-template.yml publishes packages and, when a publish occurs, builds a clean template and force‑pushes it to canopy-iiif/template (branch main).
  • The workflow:
    • Excludes dev-only paths (.git, node_modules, packages, .cache, .changeset, internal workflows/docs).
    • Rewrites package.json to remove workspaces and depend on published @canopy-iiif/lib/@canopy-iiif/ui versions; sets build/dev scripts to run node app/scripts/canopy-build.mjs.
    • Streams the template’s deploy workflow without additional post-build verification to keep the deploy path lean.
    • Stages the generated template into .template-build/ (override with TEMPLATE_OUT_DIR) so nothing checked into this repo needs to stay in sync with the published template.
  • Setup:
    • Create the template repo under the canopy-iiif org (or your chosen owner) and add a TEMPLATE_PUSH_TOKEN secret (PAT with repo write access) to this repo’s secrets.
    • Optionally mark template as a Template repository so users can click “Use this template”.

Contributing

See CONTRIBUTING.md for repository structure, versioning with Changesets, release flow, and the template-branch workflow.

Troubleshooting

  • Dynamic require error: if you see “Dynamic require of 'react' is not supported” in the browser, ensure the UI build treats react, react-dom, react-dom/client, react-masonry-css, and flexsearch as externals. The search runtime supplies React and FlexSearch globals and shims imports to window.React*/window.FlexSearch.
  • SSR import safety: the server should import UI via @canopy-iiif/ui/server to avoid pulling browser‑only code (like the Masonry Grid) during MDX SSR.
  • No columns rendering: if Masonry renders a single row, verify that the DOM includes .canopy-grid_column wrappers. If not, the Masonry lib likely failed to load; check the build externals and that site/scripts/react-globals.js is injected on the page (it is when interactive components are present).

About

A revamped and trimmed down Canopy. WIP. do not use unless testing.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •