---
title: "5.0.0"
manual: "TYPO3 EXT:thuecat"
version: "main"
source: "Changelog/5.0.0.rst"
rendered: "2026-09-19T09:50:46+00:00"
---

# 5.0.0 {#5-0-0}

## Important {#important}

-   Backend relation fields offer only records of the edited record's site, and each record
    once rather than once per language. Both the dropdown and the suggest wizard are bounded,
    content element fields included. The wizard is bounded through a global
    `TCEFORM.suggest.default.addWhere`, which disables the per-field
    `foreign_table_where` fallback for every suggest field in the installation.
    See [Backend relation scoping](../BackendRelationScoping.html#backend-relation-scoping).

## Breaking {#breaking}

-   Support for TYPO3 v12 removed.
-   Category and keyword anchors moved from the import configuration to the target site, and are
    now scoped by import target. The flexform fields `categoryStoragePid`, `categoryParent`,
    `keywordStoragePid` and `keywordParent` are removed without replacement; values left in
    stored flexform XML are ignored. Configure the anchors in the site settings instead — eight
    settings, one set per target, because a site may hold an import configuration of each and
    their category trees have to stay apart:

    -   `import.thuecat.category.storagePid`, `import.thuecat.category.parent`,
        `import.thuecat.keywords.storagePid`, `import.thuecat.keywords.parent`
    -   `import.events.category.storagePid`, `import.events.category.parent`,
        `import.events.keywords.storagePid`, `import.events.keywords.parent`

    A site opts into the settings editor for them by listing the `werkraummedia/thuecat-import`
    set among its own `dependencies`. The anchors are no longer declared by `thuecat_ces` or
    `events_ces`. On installations without a site set they remain configurable instance-wide in
    the Extension Configuration, where the four `importCategory*` / `importKeywords*` keys are
    likewise replaced by eight `importThuecat*` / `importEvents*` ones.

    There is no upgrade wizard: **note the existing values before updating**, because an import
    whose anchors are not configured runs with category and keyword mapping switched off rather
    than failing.

    Check the `importTarget` of every import configuration while you are there. It decides which
    anchors a run resolves, and a configuration that writes event records without it set counts as
    the `thuecat` target — putting its categories in the ThueCat tree. A value matching no known
    target now aborts the run before it fetches anything, instead of importing with no categories.
    See [Category based anchors](../Configuration.html#import-category-based-anchors).
-   An imported record is identified within the site that imports it. Previously a `remote_id`
    matched any record carrying it anywhere in the installation, so two sites importing the same
    upstream object shared one record: the second site's import updated the first site's record in
    place — the record kept its page, so one site silently edited another's content — and the second
    site never got a record of its own. Each site now holds its own record, and relation targets
    resolve within the importing site too. On an installation where two sites import overlapping
    data, the first run after the update creates the records the second site was missing, which
    looks like duplication but is each site gaining what it should always have had. There is no
    migration: records a foreign site's import wrote into keep whatever it last wrote, and editors
    reconcile them.
-   API responses are cached persistently: `thuecat_fetchdata` now defaults to
    `Typo3DatabaseBackend` with a 900 second lifetime instead of dying with the process.
    A run is no longer guaranteed to hit the API — see [Fetch cache](../Configuration.html#fetch-cache) for the bypass options.
-   Imported images are now the original asset (`schema:contentUrl` over `schema:url`), stored
    under a filename derived from the download URL. Re-imported records point at a different file
    under a new name; the previously imported files stay on disk unreferenced. No migration pass.
    See [Import](../FrontendOutput/Media.html#frontend-output-media-import).
-   The list item moved from `Partials/TouristAttraction/ListItem.html` to
    `Templates/TouristAttraction/ListItem.html`, and the list templates now receive rendered
    items in `{items}` rather than iterating attractions. An override left in `Partials/` is
    silently ignored. See [Frontend output](../FrontendOutput/Index.html#frontend-output).
-   The category and keyword fields on tourist attractions and tourist information are bounded by
    the site's anchors instead of offering every `sys_category` in the installation. A site
    that configures no anchor now offers **nothing** in these fields, where it previously offered
    everything; configure `import.thuecat.category.parent` and `import.thuecat.keywords.parent`
    to restore a selection. The same bound applies to the keyword field of the attraction list
    plugin. See [Category based anchors](../Configuration.html#import-category-based-anchors).
-   Filter options are built by `SearchFilterOptionsService`. Removed without replacement:
    `TouristAttractionRepository::findCategoryTreeForSearchForm()`,
    `findKeywordsTreeForSearchForm()`, `findTownsInStorageSortedByTitle()`,
    `TownRepository::findAllForSearchFormSortedByTitle()`, and the frontend
    `CategoryRepository`. `\Dto\CategoryNode` is replaced by `\Dto\FilterOption`, which
    keeps its accessor names — template overrides need no change.

    The partial `Partials/TouristAttraction/CategoryTree.html` requires a `property`
    argument naming the demand property its checkboxes bind to; an override without it renders
    unbound checkboxes. Its element ids carry that property too, so a category and a keyword
    sharing a uid no longer collide.
-   Filter options are scoped by site, not by the list's storage pages. A value outside those pages
    but inside the site is now offered; a value from another site no longer is.
-   A record can be contained in more than one place. `town` is a multi-value relation instead of
    a single select, so `TouristAttraction::getTown()` is replaced by
    `getTowns()`, returning an `ObjectStorage`. Templates reading
    `{attraction.town.title}` must iterate `{attraction.towns}` instead. Existing rows
    keep the town they held; a row that held none reads as an empty relation. Applies to tourist
    attractions, tourist information and parking facilities alike.
    See [One property, several target tables](../ImporterArchitecture.html#import-contained-in-place).
-   Every transient bucket reports a reference it cannot relate. Previously only `containedInPlace`
    logged `referenceUnrelatable`; `managedBy` and `parkingFacilityNearBy` dropped a reference
    whose record imported into another table without saying so. Expect new `info` entries in the
    import log from those buckets on the first run — they name relations that were already being
    lost, not new failures.
-   The json-based media storage is removed in favour of FAL handling. The `Media` model and
    `Base::getMedia()` are gone, as is the `media` database column. Use the FAL properties
    on `Base` (`getMainImage()`, `getMediaFiles()`, `getEditorialImages()`) and
    re-run the import. See [FAL accessors](../FrontendOutput/Media.html#frontend-output-media-model).
-   `editorial_images` is handled as the FAL field it always has been: replace
    `{entity.media.editorialImages}` with `{entity.editorialImages}`.
-   The json-based opening hours storage is removed in favour of inline database records.
    `Place::getOpeningHours()`, `getMergedOpeningHours()`, `getSpecialOpeningHours()`
    and `getMergedSpecialOpeningHours()` are gone, as are the `OpeningHours`,
    `MergedOpeningHours` and `MergedOpeningHourWeekDay` models and the `opening_hours`
    and `special_opening_hours` columns. Use `getPerDayTable()` /
    `getSpecialPerDayTable()` and re-run the import.
    See [Computed model](../FrontendOutput/OpeningHours.html#frontend-output-opening-hours-model).
-   The json-based address storage is removed in favour of inline database records.
    `Place::getAddress()` and the `LegacyAddress` value object are gone, as is the
    `address` column. Use `getAddressInline()` for the full set, or `getFirstAddress()`
    where one address is expected, and re-run the import. The misspelled
    `Address::getLatitute()` is `getLatitude()` on the new model.

## Features {#features}

-   Support for TYPO3 v14 added.
-   Images and other files are imported via FAL; the target file directory is part of the import
    configuration. See [Import](../FrontendOutput/Media.html#frontend-output-media-import).
-   One asset is downloaded once per run however many records use it, deduplicated on the download
    URL before any request. Measured across two production configurations: 614 requests fell to 398,
    and 144 to 71.
-   Media relations are cleaned up when upstream stops supplying an image, for inline and referenced
    media alike. Re-importing an unchanged record no longer accumulates `sys_file_reference` rows.
    See [Which fields the import owns](../FrontendOutput/Media.html#frontend-output-media-ownership).
-   A failed download no longer removes the image it failed to fetch: only `404` and `410` count
    as the asset being gone. See [Which fields the import owns](../FrontendOutput/Media.html#frontend-output-media-ownership).
-   Events are imported with their images, both `schema:photo` and `schema:image` landing in
    `images`; the previous cap of eight images per event is gone.
-   Media uploaded directly to a record is imported rather than rejected, for every record type,
    with its author and licence. The configured API key is sent when fetching from the API host.
-   Addresses are imported as inline database records (`tx_thuecat_address`) related through
    `address_inline`, one row per address per language. Each field has its own column, so an
    address is readable and queryable without decoding a blob, and editors see labelled fields.
    A record may carry more than one address: `schema:address` is read as a list where upstream
    sends one. Addresses no longer supplied upstream are removed on re-import.
-   Opening hours are imported as inline database records (`tx_thuecat_opening_hours`) and
    exposed display-ready via `Place::getComputedOpeningHours()`.
    See [Opening hours](../FrontendOutput/OpeningHours.html#frontend-output-opening-hours).
-   Imports can be run without media via `--no_media`. See [Import without media](../Configuration.html#import-without-media).
-   Import runs are bounded and report their progress: per-request connect and read timeouts, retries
    for transient failures, an overall run budget, and progress output at three verbosity levels.
    Aborted and failed runs write an import log instead of vanishing. See [Import tuning](../Configuration.html#import-tuning) and
    [Recovered retries](../Configuration.html#recovered-retries).
-   Every import run reports the settings that drove it — storage page, file folder, API domain,
    the category anchors and the tuning settings — as the first entry of its import log, in the
    backend module's summary column, and on the console. The API key is never included.
    See [Effective settings of a run](../Configuration.html#effective-settings).
-   Attraction lists, list items and filter forms are cached in the `pages` cache group, so
    everything that already clears page caches clears these too.
    See [Caching](../FrontendOutput/Caching.html#frontend-output-caching).
-   An event imported without a single date is named in the import log as `eventWithoutDates`
    at severity `warning`.
-   `schema:keywords` is imported as `sys_category` records in their own tree, for tourist
    attractions, tourist information and events. Term references, typed ontology literals and
    editor-typed free text all resolve to a localized title, and the upstream group structure is
    mirrored as intermediate categories. Keywords stay separate from the categories derived from an
    object's type: separate anchor, separate relation, separate identifier. Editor renames survive
    re-import, and a keyword upstream stops supplying loses its relation while the category record
    remains. See [Keywords](../Configuration.html#import-keywords).
-   Imported keywords are usable in the frontend: the attraction list filters by keyword, the filter
    mask offers them grouped by their parent set, and the selection survives pagination. Selecting
    several keywords widens the result while other filters continue to narrow it. Which keywords a
    plugin offers is chosen per plugin and bounded by the site's keyword anchor, so a plugin without
    a keyword selected behaves exactly as before. See [Filtering by keyword](../FrontendOutput/Index.html#frontend-output-keywords).
-   A place detail view emits a `keywords` meta tag built from its related keyword categories.
    A place with no keyword relations emits no such tag.
-   `schema:containedInPlace` is imported in full. Upstream uses the property for whatever contains
    a record — towns, organisations and other places — while only the first town was kept and the
    rest discarded. Each reference now lands on a relation chosen by the kind of record it imported
    into: `town`, `contained_in_organisation`, or one of `contained_in_attraction`,
    `contained_in_tourist_information` and `contained_in_parking_facility`. One production
    configuration was dropping 127 such relations per run. A record contained in a kind no relation
    accepts is still reported as `referenceUnrelatable`. The detail view renders the containing
    organisations and places. See [One property, several target tables](../ImporterArchitecture.html#import-contained-in-place).
-   The town filter matches a record carrying several towns, and the search form offers every town
    its records carry. An attraction in two towns is returned once when both are selected.
-   A show content element can pin the record it displays. The attraction and trail detail elements
    gain a backend field selecting one record; when it is set that record is shown and the frontend
    query parameter is ignored entirely, so no URL can change what the page renders. Left empty, the
    element resolves the record from the query parameter exactly as before, which is what existing
    elements do. A pinned record that no longer resolves — hidden, deleted — renders the "no data"
    message rather than falling back to the parameter, because an editor's pick must not be replaced
    silently. The pinned uid is the default-language one and is the same in every language; the
    record renders translated where a translation exists.
-   Detail pages are discarded when the record they show changes. A page rendering no record is
    tagged from the uid its element pins, so it is discarded once that record exists again — without
    that tag nothing could ever flush it, and the page would keep saying "no data" after the record
    returned. **After deploying, clear the pages cache once**: entries cached by an earlier version
    carry no record tag and would keep serving stale content until they expire.
-   `thuecat:Trail` is imported into its own record type (`tx_thuecat_trail`), with its
    descriptive texts, route and metrics each in their own column. Way-type segments, current
    conditions and the start and end locations become inline records; seasons a multi-value field.
    Trails carry keywords and images like any other record. An attraction contained in a trail
    resolves to it through `contained_in_trail`. No frontend output yet — the records exist in the
    backend.

## Fixes {#fixes}

-   Imported coordinates keep their full precision. Latitude and longitude are stored as delivered
    rather than in a decimal column, which TYPO3 rounds to two decimal places on write — enough to
    move a point by roughly two kilometres. Re-run the import to correct stored coordinates.
-   A pinned record renders in the requested language. The record was looked up with a query matching
    its uid, which in a translated request also demands the row's own language: the default-language
    row failed the language test and its translation failed the uid test, so the detail view rendered
    "no data" in every language but the default.
-   Translated records keep their own address. Address text was read with a helper that understood
    only a single `{@language,@value}` object, so a record whose upstream address was a *list* of
    language variants — the shape used as soon as an address is translated — imported with street,
    postal code, city, phone, fax and email all empty. The same defect silently affected
    media copyright years and licences, and accessibility certification values.
-   Paging through an attraction list no longer shows the wrong page. Every pagination page and
    filter combination of one plugin previously shared a single page-cache entry.
-   The search mask no longer hydrates every record in storage to build its filter options. Measured
    on 649 attractions: 31.8s to 0.04s, and the tree expansion from 375 queries to a fixed number.
-   The search form offers its category options again on a page whose plugin names no storage page.
    The option tree was built with the default storage restriction, which resolves to page 0 and
    matches nothing, so the form rendered an empty category list.
-   The search form shows the levels below a category's top level. Child categories were looked up
    by comparing an object relation against uids, which matched nothing, so only the topmost entry
    of each tree was offered.
-   Filtering by a category that groups others returns the records filed under those others.
    Selecting a group previously matched only records related to the group itself — none, where a
    vocabulary relates records to the terms it ends in — so every group selection came back empty.
    Applies to the type categories and to keywords alike.
-   A referenced resource that cannot be fetched no longer aborts the whole root URL. Only the
    affected reference is dropped, logged per owning record as `referenceSkipped` at `warning`.
-   A media entry the import cannot interpret no longer costs the whole root URL.
-   An image that cannot be downloaded no longer costs the run its media. Previously one failed
    download discarded every image downloaded under all other roots.
-   Media served behind a redirect is imported. A `301` was previously logged as
    `Image could not be downloaded`, which cost every asset on a host that redirects — trail images
    and logos come from one. Redirects are followed up to five hops. Re-run the import to fetch the
    images earlier runs skipped. See [Import](../FrontendOutput/Media.html#frontend-output-media-import).
-   Staged media is promoted into the target folder even when the run recorded an error elsewhere.
-   A record no longer inherits relations from the record imported before it — town, organisation,
    parking, categories and images alike. Relations already stored from earlier runs are not
    corrected by this.
-   Hiding a storage folder no longer breaks the import that writes into it. Which pages count as
    belonging to a site was derived with the frontend's visibility rules, so a hidden folder — or one
    past its publication end time — dropped out of the site. Depending on which folder it was, the
    run aborted with `… is outside the storagePid's site` for a page plainly inside the site, or
    the import stopped recognising what it had already written and built a second copy beside it: a
    hidden keyword or category folder produced a whole duplicate category tree, splitting records'
    relations across both. Storage is now what decides, for records, categories and keywords alike;
    only deleted pages fall out of scope.
-   A schedule day that cannot seed a date series no longer aborts the event import; the value is
    logged as `scheduleDaySkipped`, and surplus usable days as `scheduleDayDropped`.
-   `schema:exceptDate` is honoured, for every frequency.
-   An event's stored dates are reconciled with its schedule on re-import, so occurrences that
    stopped being produced are removed. Only the import's own rows are affected; dates created in
    the backend survive.
-   A failed root URL is identifiable from the import log: `mappingError` and `fetchingError`
    now carry the root URL.
-   `thuecat:importviaconfiguration` prints a closing message stating whether the run
    completed, completed with warnings, or failed.

## Tasks {#tasks}

Nothing

## Deprecation {#deprecation}

Nothing
