Feature: Featured picture for cave entrances
Context
The GrottoCenter frontend currently has no way to display an attractive image on an entrance's main page. Images exist in the system — they are attached to documents, which are themselves linked to entrances — but there is no mechanism to designate one as the visual representative of an entrance.
The goal is to allow editors to pick one image among a document's files and mark it as the featured picture of an entrance. This image would then be displayed:
- on the entrance's main page (primary use case)
- later, in web previews and listing cards (Open Graph, search results, map tooltips, etc.)
Business validation rule
The selected file must belong to a document that is already linked to the entrance. It is not valid to point to an arbitrary file from anywhere in the system. This constraint must be checked explicitly: when an editor sets a featured file, the API verifies that the file's parent document is among the documents associated with that entrance. If not, the request is rejected with a clear error.
Data model change
One nullable foreign key column is added to the entrance table, pointing to the file table. No new junction table is needed.
| Table |
Change |
t_entrance |
Add nullable FK id_featured_file → t_file(id) |
API contract changes
GET /api/v1/entrances/:id — response shape
The entrance response gains a new top-level featuredFile field. When set, it is a fully populated file object (same shape as the file objects already present inside documents[].files[]). When not set, it is null.
{
"id": 42,
"name": "Entrance du Brudour",
"featuredFile": {
"id": 25,
"fileName": "brudour-entrance.jpg",
"completePath": "https://storage.grottocenter.org/files/brudour-entrance.jpg",
"isValidated": true
},
"documents": [
{
"id": 10,
"title": "Rapport 2021",
"files": [
{ "id": 25, "fileName": "brudour-entrance.jpg", "completePath": "https://...", "isValidated": true },
{ "id": 26, "fileName": "coupe-transversale.pdf", "completePath": "https://...", "isValidated": true }
]
}
]
}
The completePath field already contains the full public URL of the binary file — the frontend can use it directly in an <img src="..."> tag without any additional request.
PUT /api/v1/entrances/:id — request body additions
The existing update endpoint accepts two new optional fields:
| Field |
Type |
Description |
featuredFile |
integer | null |
ID of the file to set as featured. Must belong to a linked document. Pass null to clear. |
Response: the updated entrance object (same shape as GET, with featuredFile populated).
Error cases:
400 — the provided file ID does not belong to any document linked to this entrance.
404 — the file ID does not exist.
No new endpoint is needed.
Typical frontend call sequence
Displaying the featured image (read path)
1. GET /api/v1/entrances/:id
↳ if response.featuredFile !== null
→ display response.featuredFile.completePath as <img>
else
→ display placeholder / no image
The image URL is available immediately in the entrance response. No extra call.
Letting an editor pick a featured image (write path)
1. GET /api/v1/entrances/:id
↳ collect all files across documents[].files[]
↳ filter to image MIME types (jpg, png, webp…) — based on fileName extension or a future mimeType field
↳ pre-select response.featuredFile.id if already set
2. User picks a file from the list (or clears the selection)
3. PUT /api/v1/entrances/:id { "featuredFile": <id> | null }
↳ on success: update local state with response.featuredFile
↳ on 400: show "This image is not linked to this entrance"
How the actual image is served
completePath is the direct public URL to the binary file on the storage backend. The frontend does not call any API endpoint to get the image bytes — it passes completePath directly to the browser as the src attribute of an <img> tag (or as an Open Graph og:image meta tag). The browser fetches the file from the storage URL independently.
Alternatives considered
A. Flag is_featured on the document–entrance junction table
Mark a document (not a file) as featured for a given entrance, and let the frontend pick the first image from that document.
- Pro: no need to pick a specific file; works when a document has only one image.
- Con: indirect — the frontend still has to iterate document files to find an image. No control over which image is shown when a document has multiple files. Adds a column to the junction table rather than the entrance itself.
- Verdict: rejected — too indirect and gives less control to editors.
B. Auto-selection without storage
Return the first validated image file found among the entrance's linked documents, without storing any preference.
- Pro: zero migration, zero model change.
- Con: the result is non-deterministic (depends on insertion order), not controllable by editors, and can change unexpectedly when documents are added or removed.
- Verdict: acceptable as a short-term fallback, not suitable as a permanent solution since it removes editorial control.
C. Dedicated junction table j_entrance_featured_file(id_entrance, id_file)
A separate table mapping entrances to their featured file.
- Pro: clean separation, extensible to multiple featured images.
- Con: over-engineered for a single nullable value; adds join complexity for no immediate benefit.
- Verdict: rejected — premature abstraction.
D (chosen). Nullable FK id_featured_file on t_entrance
Direct, explicit, one column. Follows the same pattern as the existing nullable id_cave FK on the same table.
- Pro: simple query, clear ownership, consistent with existing patterns.
- Con: the file–document–entrance consistency constraint must be validated in application code (see business validation rule above).
- Verdict: selected.
Feature: Featured picture for cave entrances
Context
The GrottoCenter frontend currently has no way to display an attractive image on an entrance's main page. Images exist in the system — they are attached to documents, which are themselves linked to entrances — but there is no mechanism to designate one as the visual representative of an entrance.
The goal is to allow editors to pick one image among a document's files and mark it as the featured picture of an entrance. This image would then be displayed:
Business validation rule
The selected file must belong to a document that is already linked to the entrance. It is not valid to point to an arbitrary file from anywhere in the system. This constraint must be checked explicitly: when an editor sets a featured file, the API verifies that the file's parent document is among the documents associated with that entrance. If not, the request is rejected with a clear error.
Data model change
One nullable foreign key column is added to the entrance table, pointing to the file table. No new junction table is needed.
t_entranceid_featured_file → t_file(id)API contract changes
GET /api/v1/entrances/:id— response shapeThe entrance response gains a new top-level
featuredFilefield. When set, it is a fully populated file object (same shape as the file objects already present insidedocuments[].files[]). When not set, it isnull.{ "id": 42, "name": "Entrance du Brudour", "featuredFile": { "id": 25, "fileName": "brudour-entrance.jpg", "completePath": "https://storage.grottocenter.org/files/brudour-entrance.jpg", "isValidated": true }, "documents": [ { "id": 10, "title": "Rapport 2021", "files": [ { "id": 25, "fileName": "brudour-entrance.jpg", "completePath": "https://...", "isValidated": true }, { "id": 26, "fileName": "coupe-transversale.pdf", "completePath": "https://...", "isValidated": true } ] } ] }The
completePathfield already contains the full public URL of the binary file — the frontend can use it directly in an<img src="...">tag without any additional request.PUT /api/v1/entrances/:id— request body additionsThe existing update endpoint accepts two new optional fields:
featuredFileinteger | nullnullto clear.{ "featuredFile": 25 }Response: the updated entrance object (same shape as
GET, withfeaturedFilepopulated).Error cases:
400— the provided file ID does not belong to any document linked to this entrance.404— the file ID does not exist.No new endpoint is needed.
Typical frontend call sequence
Displaying the featured image (read path)
The image URL is available immediately in the entrance response. No extra call.
Letting an editor pick a featured image (write path)
How the actual image is served
completePathis the direct public URL to the binary file on the storage backend. The frontend does not call any API endpoint to get the image bytes — it passescompletePathdirectly to the browser as thesrcattribute of an<img>tag (or as an Open Graphog:imagemeta tag). The browser fetches the file from the storage URL independently.Alternatives considered
A. Flag
is_featuredon the document–entrance junction tableMark a document (not a file) as featured for a given entrance, and let the frontend pick the first image from that document.
B. Auto-selection without storage
Return the first validated image file found among the entrance's linked documents, without storing any preference.
C. Dedicated junction table
j_entrance_featured_file(id_entrance, id_file)A separate table mapping entrances to their featured file.
D (chosen). Nullable FK
id_featured_fileont_entranceDirect, explicit, one column. Follows the same pattern as the existing nullable
id_caveFK on the same table.