5.0.0 

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.

  • 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 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.
  • 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.
  • 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.
  • 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.
  • 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.

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.
  • 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.
  • 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.
  • 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.
  • Imports can be run without media via --no_media . See 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 and 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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 

  • 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.
  • 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.
  • 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 

Nothing

Deprecation 

  • The json-based media storage is superseded by FAL handling. The json read accessors Media::getMainImage() , getImages() , getExtraImages() , getEditorialImages() , getAllImages() and Base::getMedia() trigger E_USER_DEPRECATED. Use the FAL properties on Base instead and re-run the import. See FAL accessors.
  • 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 superseded by inline database records. Place::getOpeningHours() , getMergedOpeningHours() , getSpecialOpeningHours() and getMergedSpecialOpeningHours() trigger E_USER_DEPRECATED, as do the legacy OpeningHours , MergedOpeningHours and MergedOpeningHourWeekDay models. Use getComputedOpeningHours() / getComputedSpecialOpeningHours() and re-run the import. See Computed model.
  • The json-based address storage is superseded by inline database records. Place::getAddress() triggers E_USER_DEPRECATED and returns the legacy LegacyAddress value object. Use getAddressInline() for the full set, or getFirstAddress() where one address is expected, and re-run the import. The address column is kept and still readable so records not yet re-imported keep rendering; it is no longer written and is removed in a later major. The misspelled Address::getLatitute() is getLatitude() on the new model; the legacy object keeps the old spelling.