kraenkvisuell / kraenk-statamic-reps
Shared backend for kraenkvisuell's artist representation sites (Statamic 6): content model, SEO, legacy Nova import and re-sync, Bunny storage.
Package info
github.com/kraenkvisuell/kraenk-statamic-reps
Type:statamic-addon
pkg:composer/kraenkvisuell/kraenk-statamic-reps
Requires
- php: ^8.3
- barryvdh/laravel-dompdf: ^3.1
- daun/statamic-multisite-field: ^1.3
- league/flysystem-aws-s3-v3: ^3.0
- statamic/cms: ^6.0
- statamic/eloquent-driver: ^5.12
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Statamic 6 addon with the backend shared by kraenkvisuell's artist representation sites
(kombinatrotweiss, wildfoxrunning, visualeyes, aiartists), which replaced their Laravel Nova 3 sites.
Frontends (Antlers, CSS, JS) stay in each site; this package holds the content model, the import and
re-sync from the old Nova sites, SEO, the lightbox and the portfolio queries. Everything a site does
differently is set in its config/kraenk-statamic-reps.php, which is merged over
config/kraenk-statamic-reps.php (every key has a default there;
associative arrays merge recursively, lists replace).
Slideshows field of the artist
The artist's slideshows field is a reps_slideshows field (an entries field, Fieldtypes\Slideshows)
with rows like the old Nova list: a position dropdown (plus drag), the categories under the title,
the works' thumbnails (cp.slideshow_thumbnails, videos play muted), the number of works (opens the
slideshow in a stack) and actions: edit, publish/unpublish, hide in / show in the artist overview
(feature overview); no unlink, a slideshow always belongs to its artist (only a slideshow that no longer exists can be unlinked). The actions save the slideshow right away
(Http\Controllers\Cp\SlideshowItemController); the order is saved with the artist. The row is
reps-slideshow-item in resources/js/cp.js, which extends Statamic's own relationship row.
Works of a slideshow
The collapsed work rows show what the old Nova list showed: the file as a large preview (videos
play), a badge per flag or category that is set ("Übersicht", "Portfolio-Bild",
"Übersicht: Portrait", "Kategorieseite: Portrait"), then the title. The on/off flags (show in overview, portfolio
image, start page image, discipline image – whichever the site uses) are switches (one per line,
label to the right) on top of the
expanded row, saved with the slideshow. Listeners\AdjustBlueprints::workRows moves and marks the
fields (reps_large_preview, reps_flag, replicator_preview: false for the rest);
resources/js/cp.js extends Statamic's assets, toggle and entries fieldtypes only for marked fields.
Translations (multi-site)
On a multi-site site (kombinatrotweiss) every language is edited in the default site's form; the
"Working in" panel and the site dropdown of the globals are hidden, and the edit page of a
localization opens its origin (feature translatable_fields, on by default, no effect on single-site
sites). Built on daun/statamic-multisite-field:
- every
localizable: truefield of the addon's blueprints (and of<blueprint>_extrafieldsets) becomes areps_translatablefield: one input per language behind a "Language" switch (in the sidebar, on global sets on top of each tab); untranslated values show the German text greyed out, titleandslugstay plain fields (Statamic needs them as strings), followed by "Title (English)" / "Slug (English)" (reps_site_value; empty: like German).
The data stays where Statamic keeps it: the German value on the origin, the English one on the
localization (the fields stay localizable, the eloquent driver stores only those as a
localization's own values). So templates, routing, SEO and reps:import / reps:resync are
unchanged. Translations\Listener writes the other languages to the localizations when the origin
is saved from the CP (an emptied field falls back to German); saves without these values (the
import) pass untouched.
Simple translations (one site, kombinatrotweiss)
With bastihilger/simple-statamic-translations
installed, a single site and the feature simple_translations on, every language lives in the same
entry instead of a site per language (Translations\SimpleTranslations):
- every localizable field of the addon's blueprints (and of
<blueprint>_extrafieldsets) is wrapped in a translated field of its own handle, so templates read it as before ({{ description }}is the current language's);titleandslugstay plain and gettitle_translation/slug_translation;robotsstays plain (the same in every language), - the language comes from the URL (
/de/…,/en/…,{language}at the start of every collection route), - PHP that reads values raw uses
SimpleTranslations::read($item, $key)(the SEO tag, menu labels), language links and hreflangSimpleTranslations::urls($entry), reps:import/reps:resyncwrite the other languages into the translated fields (SimpleTranslations::merge: other languages only where they differ, idempotent) instead of localizations;legacy.localesmaps the legacy locales to language codes (['de' => 'default', 'en' => 'en']),seo.localesthe og:locale per language.
Install in a site
composer require kraenkvisuell/kraenk-statamic-reps:^0.1 (from Packagist). For working
on the addon next to a site, point it at a path repository (../kraenk-statamic-reps, symlinked) locally
without committing that.
Releases
Versions are git tags (v0.1.0, …). Under 1.0 a minor bump (v0.2.0) means sites need changes, a
patch (v0.1.1) is a fix. Release: commit, run the sites' tests against the change, git tag -a v0.x.y -m v0.x.y && git push --follow-tags; then per site composer update kraenkvisuell/kraenk-statamic-reps
(widen the constraint for a minor bump), test, commit composer.lock.
Then: php artisan vendor:publish --tag=kraenk-statamic-reps-config, set the site's options,
php artisan reps:install (writes collections, globals, navigations, the asset container),
php artisan migrate, php artisan reps:import.
What it provides
- Content model. Collections
pages,artists,slideshows(works as replicator rows),categories,disciplines, optionalnews,productions,special_categories+special_projects,projects+skills,clients. Blueprints live inresources/collection-blueprints/and are registered as namespaces, so they are not copied into the sites;Listeners\AdjustBlueprintsinlines the addon fieldsets (reps::seo,reps::main_content,reps::work_common…), removes fields whosereps_featureis off and blocks not inpage_blocks, applies the labels and appends the site's own<blueprint>_extrafieldset. A site can still override a blueprint inresources/blueprints/vendor/collections/<handle>/. Global blueprints (site_settings,seo) are fallbacks. Computed values for routes:artist_slug(slideshows),special_category_slug,discipline_slug(artists),route_segment(categories). reps:installwrites the structure files the config describes, touching only the keys it manages. Repeatable.reps:importimports the old Nova site from its dump on thelegacyconnection (LEGACY_DB_*). Nova is the source until go-live: imported entries (found bylegacy_id, ids are deterministic) are made to match the dump, rows deleted in Nova are deleted, entries created in Statamic are never touched. Rich text → Bard, nova-cms blocks →main_contentsets, settings → globals (legacy.settings, incl. grids), menus → navigations, multi-locale → localizations. A second run reports nothing to do. Options--only=,--no-media,--keep-deleted,--dry-run.- Animated GIFs keep their original file (
imaging.original_gifs): the Glide tag returns the asset's CDN URL for GIFs, like Nova did. Glide re-encoded every frame (minutes per GIF), upscaled small ones and produced files too large for a single upload to Bunny.fmstill converts. - Media. An asset's path is the old media folder + file name, so it never changes between runs.
The local
assetsdisk (public/assets) mirrors the old originals the content uses (rsync with--files-fromfrom the server, an S3 disk or a public URL); with the container on Bunny only new and replaced files are uploaded, and only replaced files lose their Glide images. reps:resyncdumps the live Nova database again (read-only, ssh), reports the differences, and with--applybacks up the content tables, imports and syncs media incrementally, then renders only the pages of changed entries.--restore=<backup>undoes an apply.- SEO (instead of SEO Pro): the Nova SEO tab as
reps::seoon every routable blueprint, aseoglobal for the home page and listing pages (seo.index_pages),{{ reps:seo }}for the head (title, description, keywords, robots, canonical, hreflang, og/twitter with configurable og:image fallbacks),/sitemap.xml. - Routes:
/sitemap.xml, slideshow URLs with a trailing slide number, 301s from old id URLs (legacy.id_redirects), the lightbox endpoints under/!/reps/lightbox. On multi-site installs a 404 that is an entry's URL in another site (/en/<german slug>) redirects to the entry in the requested site (localized_slug_redirects). - German CP. Blueprint labels, instructions, options, collection/global/navigation titles are
written in English and translated in
lang/de.json(the CP passes them through__()). A site gets a German CP withAPP_LOCALE=de(default in itsconfig/app.php) and can override single strings in its ownlang/de.json. Keeplang/de.jsoncomplete when adding fields. - Lightbox: a per-visitor list of works in the session, sortable per artist, as PDF (dompdf, view from the site).
- Portfolio queries (
Portfolio,{{ reps:works }},{{ reps:artist_categories }},{{ reps:category_artists }},{{ reps:overview }},{{ reps:slideshows }},{{ reps:rows }}). - Storage: the
bunny-assets,bunny-glide-cache,assetsandlegacy-mediadisks (fromBUNNY_*/LEGACY_S3_*) unless the site defines them;reps:copy-assets-to-bunny,reps:warm-glide,reps:refresh-uris. - CP artist list like the old Nova index (
Cp\ArtistListing, computedreps_*fields in the artist blueprint that only appear in the list): the name with/slugunderneath (title and slug columns hidden by default), "Copy Link" / "Copy Link 2" buttons copyinga:<Nova id>/b:<Nova id>(featureartist_copy_links), portfolio images (cp.artist_portfolio_images, labellabels.portfolio_images), start page images (featurestartpage_images), disciplines. HTML cells use thereps_htmlfieldtype, whose index component is inresources/js/cp.js(plain JS, no build; published topublic/vendor/kraenk-statamic-repsbystatamic:install). - Asset metadata cache: every image a page renders needs its asset's metadata (
.meta/*.yamlon Bunny); uncached that's one Bunny request per image, minutes for an artist page. Sites give it a cache store of its own (asset_metainconfig/cache.php), whichcache:clear– run by composer'sstatamic:installon every install – leaves alone.reps:warm-asset-metaloads all metadata in parallel (about 5 minutes for 15,000 assets; seconds when everything is cached), in the deploy script beforestatic:warm. Metadata edited in another environment (the bucket is shared) stays cached untilphp artisan cache:clear asset_meta. - Static cache warming in tiers:
reps:warm [--queue] [--insecure] [--uncached] [--dry-run]runs Statamic'sstatic:warmonce per tier, so the pages visitors see first are cached first: the home page(s), every internal link in the home page's<nav>elements (the menu, whatever builds it), the published artists, then everything else (--excludeof the earlier tiers). Every language (simple translations) or site. Queued, Horizon renders the tiers in that order. Use it in the deploy script instead ofstatic:warm --queue. With--queueit first clears the warm queue (static_caching.warm_queue, give it a queue of its own, e.g.warm): jobs still waiting from an earlier run (a deploy shortly after another) would otherwise go before this run's tiers. Statamic's warm jobs are unique per URL andqueue:clearleaves their locks behind, so it releases the locks of the jobs it drops; without that, the new run's jobs for the same URLs would be dropped silently. - Deploy runs on their own queue: with
static_caching.warm_queueset in the addon config (e.g.warm),reps:warmqueues its runs there, with its own job (DeployWarmJob, so a waiting deploy job doesn't swallow a save's re-cache of the same URL), and Statamic'swarm_queue(e.g.recache) only carries the re-caching after saves. Let Horizon take the queues in order without balancing ('queue' => ['recache', 'default', 'warm'],'balance' => false): saves show up within seconds even during a long deploy run. - Slide pages in the static cache: the
slideshow_index_segmentroute (/…/<slideshow>/<n>) runs throughstatamic.web, so these pages are cached like Statamic's own; static:warm doesn't list them, so the addon adds the cached ones to every warm run (StaticWarmhookadditional). - Warm job with an expiring lock: the addon sets
static_caching.warm_job(when the site leaves it empty) toStaticCaching\WarmJob, Statamic's job with a one-hour unique lock. Statamic's lock never expires: a warm job removed from the queue without running blocked its URL for good.
License
MIT, see LICENSE.