Conversation
Add a nullable string(2048) main_image_url column so each site can carry a guaranteed hero/thumbnail URL directly, eliminating the "No image" frontend display for sites that have fewer than two entries in world_heritage_site_images. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a nullable string(2048) main_image_url column so each site can carry a guaranteed hero/thumbnail URL directly, eliminating the "No image" frontend display for sites that have fewer than two entries in world_heritage_site_images. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat: add migration for main_image_url column on world_heritage_sites
In normalizeSiteRowImportReady, surface main_image_url on every newly created site row in world_heritage_sites.json. The lookup prefers the local dump's image_url key (UNESCO main_image_url is renamed to image_url by DumpUnescoWorldHeritageJson) and falls back to main_image_url for sources that keep the original UNESCO field name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In mergeSiteRowPreferExisting, fill main_image_url onto an already seen site row when the existing entry has none and a duplicate row provides a value. This keeps the existing-wins semantics consistent with the rest of the merge while ensuring transnational/duplicate rows do not silently drop the hero image. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…to-json Chore/import main image url into json
Map main_image_url from the split JSON onto the world_heritage_sites upsert payload so each imported site stores the hero image URL directly. The flush() method derives the upsert column list from the row's keys, so adding the field here is sufficient — no other mapping changes are required. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-url feat: persist main_image_url via ImportWorldHeritageSiteFromSplitFile
…ackfill Allows production (Aiven) to run the one-shot SQL UPDATE that populates main_image_url on the 1248 existing rows without regenerating the file via world-heritage:split-json. Mirrors the is_endangered backfill flow: no new artisan command, the exact command is documented in the PR description. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chore: Backfill `main_image_url`
…services Adds main_image_url to WorldHeritage::$fillable so the Eloquent model treats it as assignable, and includes world_heritage_sites.main_image_url in the explicit SELECT lists of WorldHeritageReadQueryService::findByIdsPreserveOrder and WorldHeritageQueryService::getAllHeritages so downstream layers can consume the value. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ery-service feat: surface main_image_url from WorldHeritage model and read query …
Adds an optional ?string $mainImageUrl constructor parameter and a getMainImageUrl() getter so downstream layers can read the raw main_image_url straight from the DTO. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reads main_image_url from the input array and forwards it to the WorldHeritageDto constructor so detail-path callers carry the value. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reads main_image_url from the input array and forwards it to the WorldHeritageDto constructor so list/summary callers carry the value. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…thumbnail Updates WorldHeritageViewModel::getThumbnailUrl to fall back as main_image_url ?? images[0]?->url ?? null so the exposed thumbnail_url reflects the new field. The view model summary test mock is updated to cover the new path via getMainImageUrl. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renames the existing 'thumbnail' summary key to 'thumbnail_url' and fills it with main_image_url ?? images[0]?->url ?? null so the list endpoint surfaces the new field with the same fallback semantics as the detail ViewModel. The collection summary test is updated to assert the new key name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WorldHeritageViewModelCollectionFactoryTest used to seed the legacy imageUrl ImageDto field from thumbnail_url. The view model now resolves thumbnail_url via main_image_url ?? images[0]?->url, so the mock is updated to pass mainImageUrl directly. Drops the now-unused ImageDto import. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…plication Chore: Add select query into application
…eritagePayload The list-path payload (used by getAllHeritages and the Algolia search result reshape) didn't pass main_image_url, so the SummaryFactory could never populate WorldHeritageDto::mainImageUrl and thumbnail_url stayed null in the API. Forwards $heritage->main_image_url so the new fallback chain in WorldHeritageViewModel resolves correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ById factory payload The detail-path WorldHeritageDetailFactory::build call was missing the 'main_image_url' key, so the DTO's mainImageUrl stayed null and the detail endpoint's thumbnail_url did not reflect the new field. Forwards $heritage->main_image_url so the ViewModel fallback resolves. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds world_heritage_sites.main_image_url to the chunked SELECT so the Algolia importer can read the new column off each model. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ects Each Algolia object now carries the raw main_image_url alongside the existing thumbnail_url so search consumers can render via the new field directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ritages Switches the Algolia object's thumbnail_url to main_image_url first, falling back to images[0]->url, so the search index mirrors the new fallback semantics now used by WorldHeritageViewModel and WorldHeritageDtoCollection::toSummaryArray. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Chore: import algolia indexer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
main_image_urlend-to-end so each World Heritage site carries the canonical image URL from the UNESCO source through the database, the read paths, the API response (thumbnail_url), and the Algolia search index.Frontend can now display the official image for ~99% of sites without falling back to the
world_heritage_site_imagesjoin. Built and merged as a sequence of small, focused sub-PRs; this PR is the rollup tomain.Motivation
thumbnail_urlfromworld_heritage_site_images[0], which is gallery data and not the authoritative "main" image of a site.image_urlper heritage that was being dropped during normalization.Changes
1. Schema —
world_heritage_sites.main_image_urlMigration:
2026_05_04_000000_add_main_image_url_to_world_heritage_sitesadds a nullablevarchar(2048)column right aftershort_description. (PR #449)2. Ingestion pipeline
SplitWorldHeritageJsonemitsmain_image_urlin both the normalize and merge paths so the normalized JSON carries the value. (PR Chore/import main image url into json #450)ImportWorldHeritageSiteFromSplitFilepersistsmain_image_urlvia the existing upsert. (PR feat: persist main_image_url via ImportWorldHeritageSiteFromSplitFile #451)unesco/normalized/world_heritage_sites.json, 1.7 MB / 1248 sites) is shipped in the repo so production can run the one-shot backfill without re-running the dump+split chain. (PR chore: Backfillmain_image_url#452)3. Read paths
WorldHeritage::$fillableincludesmain_image_url.WorldHeritageReadQueryService::findByIdsPreserveOrderandWorldHeritageQueryService::getAllHeritagesincludeworld_heritage_sites.main_image_urlin their explicit SELECTs. (PR feat: surface main_image_url from WorldHeritage model and read query … #453)WorldHeritageDtogains an optional?string $mainImageUrlconstructor parameter andgetMainImageUrl()getter.WorldHeritageDetailFactory::buildandWorldHeritageSummaryFactory::buildreadmain_image_urlfrom the input array.WorldHeritageViewModel::getThumbnailUrl()andWorldHeritageDtoCollection::toSummaryArray()resolvethumbnail_url = main_image_url ?? images[0]?->url ?? null, preserving the legacy fallback for any site that has nomain_image_urlin the source.WorldHeritageQueryService::getHeritageByIdandWorldHeritageQueryService::buildWorldHeritagePayloadforwardmain_image_urlinto the factory payloads so the value actually reaches the DTO. (PR Chore: Add select query into application #454, plus follow-up commits)4. Algolia search index
AlgoliaImportWorldHeritagesselectsmain_image_url, ships it as a top-level field on each indexed object, and switchesthumbnail_urltomain_image_url ?? images[0]?->urlso search results match the API fallback. (PR #)API response shape (no breaking changes)
GET /api/v1/heritages): each item gainsthumbnail_url(was previouslythumbnail; the value now derives frommain_image_urlfirst).GET /api/v1/heritages/{id}):thumbnail_urlreturns the same fallback chain.imagesarray still exposes the full gallery.nullable: true. ~12 of 1248 sites have no source image URL (UNESCO's own data is empty for those entries) and remainnull; the frontend's existing "no image" placeholder handles them.Operational checklist (post-merge)
After this PR is merged and deployed:
Backfill
main_image_urlon existing rows (Aiven)The migration adds the column as
NULLon all 1248 production rows. Run the documented one-shot SQL update from a host that has the deployed repo and network access to Aiven:Idempotent — re-running on already-populated rows is a no-op.
Expected: 1236 rows updated, 12 remain NULL (UNESCO source has no image URL for those entries).
Required so live searches surface
main_image_urland the updatedthumbnail_url.--truncateclears the existing index first to drop any stale objects.GET /api/v1/heritages/1returns"thumbnail_url": "https://whc.unesco.org/document/107413".main_image_urlandthumbnail_url.Verification (local)
world_heritage_sites: 1248 rows / 1236 withmain_image_url.image_url/images_urls:[140, 195, 227, 250, 335, 430, 452, 606, 783, 938, 955, 1272].thumbnail_urlfrommain_image_urlfor the 1236 populated entries; the 12 stay null and the frontend's existing "no image" placeholder handles them.main_image_url ?? images[0]fallback.objectID=1confirmed to carry bothmain_image_urlandthumbnail_url.App\Packages\Features\QueryUseCases\Tests\*— 32 tests / 278 assertions all green.Notes / non-goals