EXT:thuecat
ThüCAT is ¨Thüringer Content Architektur Tourismus¨. This is an extension for TYPO3 CMS (https://typo3.org/) to integrate ThüCAT. The existing API is integrated and allows importing data into the system.
ThüCAT is ¨Thüringer Content Architektur Tourismus¨. This is an extension for TYPO3 CMS (https://typo3.org/) to integrate ThüCAT. The existing API is integrated and allows importing data into the system.
Import of specific resources via static URLs. This allows import of specific tourist attractions and towns.
The following data types can be imported (not all properties are supported):
Allows to create new import configurations.
Allows to inspect:
Figure 1-1: Overview of currently available configurations and Organisations.
Figure 1-2: Overview of executed imports and their results.
A dedicated content element is provided to display tourist attraction.
A dedicated Page Type is provided to reference tourist attraction.
The extension can be installed via composer, see t3gettingstarted:extensions-management. The composer package can be found at packagist.org.
Integrators can include and configure TypoScript once it is installed and enabled. See Include TypoScript from extensions. All available configurations can be found at Configuration.
At least one import configuration needs to be created and imported. See Import configuration.
Some API requests are only possible by providing an API Key. This key can be configured via "Extension Configuration".
The configuration of imports is stored within TYPO3 records.
Those records can be created via backend module.
Each new record is stored on a default page. The default is 0.
That would prevent editors from creating and editing records.
This page uid can be configured via TypoScript:
module {
tx_thuecat {
settings {
newRecordPid {
tx_thuecat_import_configuration = 10
}
}
}
}
Each import is defined via a special import configuration record. This record can be created via TYPO3 backend module.
There are different configurations available:
syncScopeId to the configuration to update the given resources
for that specific sync scope.
This requires an configured API Key.All configurations also provide an input to define the page where records should be stored and updated. This page uid is also used to fetch accordingly site configuration. The related languages are used during the import.
EXT:thuecat registers Extbase controller actions and ships template stubs, but no content elements. You define the content elements in your own extension or sitepackage and point them at the registered plugins, so rendering stays under your control.
The extension name is ThueCat. Five content elements are intended, built from
four plugins (the filtered list is the list plugin with an editor preset). Each
plugin renders one template:
| Plugin name | Content element | Template |
|---|---|---|
TouristAttractionList | List | List |
TouristAttractionList | Filtered list | List |
TouristAttractionListSelected | Selected list | SelectedList |
TouristAttractionSearch | Search-and-filter | SearchForm |
TouristAttractionShow | Detail | Show |
The plain and filtered list are the same TouristAttractionList plugin and
therefore share the List template; the filtered variant only adds an editor
preset in its FlexForm (see Filtered list). The search element is a combined
search and filter form.
Set up these pages once and configure their ids (see Site settings):
On the list / search page neither plugin is optional: the search-and-filter form submits into the list, and the list renders the result. A page with only one of the two does not work.
Register one content element per plugin in your own extension or sitepackage, for
example with Content Blocks. Each content element
wires its CType to a plugin via the ThueCat extension name:
tt_content.myvendor_attractionlist =< lib.contentBlock
tt_content.myvendor_attractionlist {
20 =< lib.contentBlock.20
20 {
pluginName = TouristAttractionList
}
}
The list content element points at the storage folder through its pages field.
A full, ready-to-use content element definition is shown in ContentElementExample.
The shipped stubs under EXT:thuecat/Resources/Private/Templates/ are
registered at templateRootPaths.10. Override them by adding a higher index:
plugin.tx_thuecat.view {
templateRootPaths.20 = EXT:my_extension/Resources/Private/Templates/
partialRootPaths.20 = EXT:my_extension/Resources/Private/Partials/
layoutRootPaths.20 = EXT:my_extension/Resources/Private/Layouts/
}
The templates are
TouristAttraction/ListTouristAttraction/SelectedListTouristAttraction/SearchFormTouristAttraction/ShowSome fields carry richer data than a single value and have their own model accessors and partials. They are covered on their own pages:
Provide a site set that maps site settings onto the plugin configuration and include it in your site. Fill in the page ids under Settings in the site configuration.
Settings definition (Configuration/Sets/<YourSet>/settings.definitions.yaml):
settings:
page.pid.thuecat_attraction_show:
label: 'Detail Page for Tourist Attractions'
description: 'The page providing the detail pages for tourist attractions'
category: 'page.pids'
type: 'int'
default: 0
page.pid.thuecat_attraction_search:
label: 'Search Result Page for Tourist Attractions'
description: 'The page providing the list of tourist attractions, used as target for search form submissions'
category: 'page.pids'
type: 'int'
default: 0
list.itemsPerPage:
label: 'Tourist Attractions per Page'
description: 'Number of tourist attractions shown per page in the list view'
category: 'list'
type: 'int'
default: 20
Mapping (Configuration/Sets/<YourSet>/setup.typoscript):
plugin.tx_thuecat.settings {
page.pid {
thuecat_attraction_show = {$page.pid.thuecat_attraction_show}
thuecat_attraction_search = {$page.pid.thuecat_attraction_search}
}
itemsPerPage = {$page.settings.itemsPerPage}
}
The search-and-filter form adapts to what shares its page:
page.pid.thuecat_attraction_search).After a search the form re-populates with the submitted values, so the visitor keeps their input.
A filtered list carries an editor preset in its FlexForm (for example a fixed set of towns). The list re-applies the preset on every request: a visitor search refines within the preset but can never widen it, even with a tampered URL.
The search-and-filter form does not read its own settings to learn the preset. A resolver inspects the current page for a list content element, reads its FlexForm, and returns the active preset:
The lookup is language- and overlay-aware, because it reads the stored content element through the frontend's record retrieval.
The form is rendered by TouristAttractionSearch but submits under
TouristAttractionList, so the demand travels in the list's namespace. On
re-render the form adopts that demand to re-populate its fields, and the editor
preset is forced onto the locked fields, so the hidden inputs always carry the
editor's values.
Opening hours are imported as inline database records (one row per weekday and time span, with an optional validity range). You do not work with those rows directly: the model computes a display-ready shape from them.
A tourist attraction exposes two computed accessors:
computedOpeningHours -- the regular hours.computedSpecialOpeningHours -- deviating hours, for example public holidays.Both return the same structure: a list of periods (validity ranges), each listing weekdays Monday-first, each weekday carrying all of its time spans. A day without hours is marked closed, so every weekday is present in the output. Past periods are dropped; the period covering today is flagged as current.
Note
Whether the attraction is open right now is intentionally not computed server-side -- it depends on the visitor's time zone. Resolve the current open/closed state in client-side logic if you need it.
The extension ships a partial that renders the computed shape as a per-day table
(one row per weekday, all spans listed, closed days shown as closed). Render it
from your Show template, passing the computed hours and a heading label:
<f:render partial="OpeningHours/PerDayTable" arguments="{
openingHours: attraction.computedOpeningHours,
heading: 'LLL:EXT:thuecat/Resources/Private/Language/locallang.xlf:content.openingHours'
}" />
<f:render partial="OpeningHours/PerDayTable" arguments="{
openingHours: attraction.computedSpecialOpeningHours,
heading: 'LLL:EXT:thuecat/Resources/Private/Language/locallang.xlf:content.specialOpeningHours'
}" />
The partial takes two arguments:
| Argument | Meaning |
|---|---|
openingHours | A computed opening hours object (one of the accessors above). |
heading | A translation key for the section heading. |
The partial renders nothing when there are no periods, so a missing heading or empty section never appears.
Note
The partial set will grow with further output designs. Names such as
OpeningHours/PerDayTable may still change while the set settles -- check
this section after upgrades.
Images and other files are imported into FAL and related to the record. The target file directory is part of the import configuration.
A tourist attraction exposes its media as native Extbase FAL relations:
| Accessor | Meaning |
|---|---|
mainImage | The primary image (main_image), a single file reference. |
mediaFiles | Additional images and files (media_files). |
`editorial | Editorially curated images (editorial_images), maintained in the backend. |
mainImage returns a single file reference or none; mediaFiles and
editorialImages return a (possibly empty) collection.
The FAL relations are rendered with the standard Fluid image view helper, so processing (cropping, scaling) and metadata (copyright, alternative text) are available:
<f:if condition="{attraction.mainImage}">
<figure>
<f:image image="{attraction.mainImage}" />
<f:if condition="{attraction.mainImage.originalResource.properties.copyright}">
<figcaption>{attraction.mainImage.originalResource.properties.copyright -> f:format.htmlspecialchars()}</figcaption>
</f:if>
</figure>
</f:if>
<f:for each="{attraction.mediaFiles}" as="image">
<f:image image="{image}" />
</f:for>
A complete, ready-to-use content element built with
content_blocks:Index. This example is the selected list:
an editor picks a fixed set of attractions, rendered in the chosen order. Replace
myvendor with your own vendor name.
Definition (ContentBlocks/ContentElements/attraction-list-selected/config.yaml):
name: myvendor/attraction-list-selected
group: Thuecat Attraction
prefixFields: true
prefixType: vendor
fields:
- identifier: TYPO3/Header
type: Basic
- identifier: 'pi_flexform'
type: 'FlexForm'
useExistingField: true
fields:
- identifier: 'settings.selectedRecords'
type: 'Select'
renderType: 'selectMultipleSideBySide'
foreign_table: 'tx_thuecat_tourist_attraction'
foreign_table_where: 'AND {#tx_thuecat_tourist_attraction}.{#sys_language_uid} IN (0, -1)'
Wiring (ContentBlocks/ContentElements/attraction-list-selected/setup.typoscript):
tt_content.myvendor_attractionlistselected =< lib.contentBlock
tt_content.myvendor_attractionlistselected {
20 =< lib.contentBlock.20
20 {
pluginName = TouristAttractionListSelected
}
}
The settings.selectedRecords FlexForm field lets the editor choose the
attractions; the TouristAttractionListSelected plugin renders them via the
SelectedList template in the picked order.
The other content elements follow the same shape: a FlexForm field for their settings (a town preset for the filtered list, none for the plain list, none for the search-and-filter form) wired to the matching plugin name from Frontend output.
The list item moved from a partial to a template:
Resources/ is now
Resources/. The name is unchanged; only the
directory differs.
Integrators overriding the list item must move their override accordingly. An override left in
Partials/ is silently ignored — the file is no longer read from there, and the shipped template renders
instead. Nothing errors, so check this explicitly when upgrading.
The reason is caching: the attraction list now renders each item separately and caches the resulting HTML per attraction, so an item is rendered once and reused by every list showing it. A separately rendered item is a template rather than a partial.
This also changes what the list templates receive. List. and Selected no longer
iterate attractions — the controller renders the items and assigns them as ready HTML in {items}:
<f:for each="{items}" as="item">
<f:format.raw>{item}</f:format.raw>
</f:for>
An override still iterating {list.paginatedItems} or {attractions} renders nothing, without an error.
{list} remains assigned for pagination; {attractions} is gone from the curated list entirely.
Both actions share one cache, so an attraction shown by a filtered list and a curated one is rendered once.
Template paths are unchanged otherwise: the item is resolved through the plugin's configured
templateRootPaths, so an override in an extension or sitepackage keeps working once it sits in
Templates/.
Attraction lists are cached, so a visitor rarely waits for one to be built twice.
Three caches do the work, all in the pages cache group:
tx_thuecat_teaser
tx_thuecat_list
tx_thuecat_searchmask
Because they belong to the pages group, everything that already clears page caches clears these
too: the "Clear cache" buttons in the backend, and any editor saving a record. Editors need do
nothing differently — a changed attraction, town or category takes its cached output with it and the
next visitor sees the new content.
Entries are discarded by the records they show rather than by age, so nothing goes stale waiting for a lifetime to run out. A filter combination nobody has requested before is still built from scratch, but only its unseen items cost anything: the rest come from the teaser cache.
The three caches therefore configure a lifetime of one year. This is not a staleness setting —
invalidation by tag is what keeps content current — it exists because TYPO3's default would otherwise
expire entries after an hour. Override it per cache in ext_ or
config/ via
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'][<identifier>]['options']['defaultLifetime'],
though shortening it only costs rebuilds without making anything fresher.
Note that an import currently discards the caches of every record it visits, whether or not that record changed, so lists are rebuilt after each import run. A known limitation, to be addressed separately; it costs speed, never correctness.
schema:photo and schema:image both land in the
images field of
tx_events_domain_model_event
; events have no separate main image, so the source
ranking decides the order — the photo comes first. The previous cap of eight images per event is removed:
every image upstream publishes is imported.Media uploaded directly to a record is imported. Alongside the established form, where a record points at a reusable media resource, upstream also lets editors upload a file into the record itself. Such a file was previously rejected before any download was attempted, and — because the rejection was fatal — it cost the whole root URL every one of its records. It is now imported like any other image, with its author and licence.
This applies to every record type, not only events. A point of interest carrying a direct upload imports it
into main_image / media_files as it would a referenced image.
A directly uploaded file is identified by its file name in the delivered address, which stays stable across runs, so re-importing reuses the stored file instead of downloading it again.
These files are served from the API host, which refuses anonymous requests, so the configured API key is sent when an image is fetched from that host. Images on other hosts — every referenced media resource so far — are requested unchanged, without a key.
tx_thuecat_opening_hours
) and expose a display-ready,
computed shape via
Domain\Model\Frontend\Place::getComputedOpeningHours()
and
getComputedSpecialOpeningHours()
. The shape groups records into validity periods, lists every weekday
Monday-first, keeps all time spans of a day and
marks days without hours as closed. A OpeningHours/PerDayTable partial renders it. The current open/closed
status is intentionally left to client-side logic.An event imported without a single date is named in the import log. Such an event is stored but cannot appear
anywhere dates are shown, and until now nothing said so — the absence had to be noticed in the frontend and
traced back by hand. Each one now gets a
tx_ row of the new type
eventWithoutDates at severity warning, naming the event and the record to look at.
The reason is deliberately not distinguished: an event publishing no schedule, one whose schedule yields no usable day, and one whose dates are all excepted are reported alike, because the outcome is the same. The event still imports and a run whose only findings are dateless events exits 0.
Events whose dates have all passed are included, so a finished event will produce this warning. That is intended — the log entry says "this event shows nothing", which is true of a past event as well — but it means the type is expected to appear routinely on sites with older data.
Paging through an attraction list no longer shows the wrong page.
Every pagination page and every filter combination of one list plugin shared a single page-cache
entry, because currentPage and demand were excluded from the cache identifier. Whichever
page was requested first was then served for all of them, until that entry expired.
The symptom only appeared once a page had been cached, so it was reliably invisible on a developer machine and in tests, where every request starts from a cold cache.
The list and search actions are now non-cacheable and serve their output from the caches described under Features, each keyed by the plugin, the filter selection, the pagination page and the language. Filter and pagination URLs are unchanged.
A referenced resource that cannot be fetched no longer aborts the whole root URL. Previously any failure while resolving a reference (a resource deleted upstream, a server error, a malformed response, a network failure) propagated out of the resolver and the importer discarded every record belonging to that root. Now only the affected reference is dropped: the owning record is imported without that relation, and all other records under the same root are unaffected.
Each affected parent record gets its own
tx_ row of the new type
referenceSkipped at severity warning, carrying the owner's remote_id and table_name alongside the
reference URL, the target field and the failure reason — so the log names the records to fix, not merely the URL
that failed. A URL already found to be failing is not fetched again for the rest of the run, but every parent that
loses a reference is still logged.
Because these runs report warning rather than error,
thuecat: now exits 0
for them. Failures of the outer calls — the sync-scope/contains-place listing and the root URL fetches — are
unchanged and still fail the run, so a genuine upstream outage remains distinguishable.
A record no longer inherits relations from the record imported before it.
Records that said nothing about a relation kept whatever the previous record in the same run had declared. The effect was visible with images — several events ending up with the same picture, none of which named it — but it applied to every imported relation: town, responsible organisation, nearby parking, categories. A record silent about a relation is now imported without it.
Relations already stored from earlier runs are not corrected by this. The import stops writing them; removing one whose source is gone is a separate matter, and images that were wrongly related need to be removed by hand.
tx_thuecat_import_log_entry
of type referenceSkipped at severity
warning names the record and the entry. A run whose only problem is an entry of this kind exits 0.An image that cannot be downloaded no longer costs the run its media. Previously the download raised on any error status, the exception escaped the resolver and the importer discarded the whole root URL; because promotion of downloaded files was gated on the run's overall severity, that single failure also deleted every image already downloaded under all other roots. A run could finish having imported thousands of records and not one image.
Now only the affected image is dropped: the owning record is imported without that file, its other images are
related as usual, and records under the same root are unaffected. This holds for every failure mode — an error
status, a refused connection, a timeout, or an empty body. Each affected record gets a
tx_ row of type referenceSkipped at severity warning, naming the record and
the URL that could not be fetched, so runs whose only problems are unfetchable images exit 0.
The image download now sends through the PSR-18 client rather than
Request, whose Guzzle
default raises on a 4xx/5xx before the response can be inspected. JSON-LD fetches are unchanged and still fail
loudly.
error now leaves promoted files where it previously left none. A promoted file that no record ended
up referencing is reused by name on the next run rather than downloaded again. The per-run staging folder is still
discarded on every exit path, so a crashed run leaves no orphans.
thuecat:importviaconfiguration
now prints a closing message stating whether the run completed, completed
with warnings, or failed. It previously exited silently, so a non-zero exit arrived with no explanation.A schedule day that cannot seed a date series no longer aborts the event import.
schema:byDay may carry values that are valid upstream but name no weekday — schema:PublicHolidays among
them. These were passed on for date expansion, where they raised and cost the whole root URL its records. Such a
value is now skipped: the event imports with the dates its remaining weekdays produce, and a
tx_ of the new type scheduleDaySkipped at severity warning names the event
and the value. Editors keep the information that upstream asked for something the import cannot express.
Where a schedule supplies more usable weekdays than the import can carry — a monthly schedule holds one — the
surplus days are recorded separately as scheduleDayDropped, also at warning. The two are deliberately
distinct: one is an upstream value we cannot use, the other a usable value our own import discards, and they call
for different editor action.
schema:exceptDate is honoured.
Dates a schedule explicitly excludes were imported like any other occurrence. They are now dropped, for every frequency. The comparison is by calendar day in the schedule's timezone, since an exception is date-only while occurrences carry a time.
An event's stored dates are reconciled with its schedule on re-import.
Only new occurrences were ever written; one that stopped being produced stayed in the database indefinitely. Dates the import owns are now compared against what the current schedule yields, and the difference is deleted. This is the observable change of this release: occurrences that became excepted, fell after a shortened series end, or vanished with a changed schedule disappear on the next run, where they previously lingered.
Only the import's own rows are affected — ownership is established through the remote_id the import assigns —
so dates created in the backend survive. The reconciliation is scoped to the events actually imported by the run:
an event outside the run, or one whose root URL was abandoned, keeps its dates.
A failed root URL is identifiable from the import log.
mappingError and fetchingError entries recorded the exception's message and origin but not which
configured URL produced them, so a run with several roots left the failure unattributable. Both now carry the root
URL in remote_id and repeat it in the entry's context alongside the existing detail. Severity is unchanged:
an abandoned root remains an error and still fails the run.
The failing record itself is not named. It is not reliably known at the point the failure is caught — parsing may raise before any record is identified — and naming the wrong record would mislead more than naming none.
Nothing
place. The next import will copy and relate the files as needed. With the next fitting version, the json-based fields will be removed.
Deprecated, triggering
E_USER_DEPRECATEDon use:
Domain\,Model\ Frontend\ Media:: get Main Image () get,Images () get,Extra Images () getandEditorial Images () get(the json-blob read accessors).All Images () Domain\(hands out the json-blob carrier).Model\ Frontend\ Base:: get Media () Use the FAL fields instead, available as native Extbase properties on
Base:get(Main Image () main_),image get(Media Files () media_) andfiles get(Editorial Images () editorial_). Re-run the import to populateimages main_andimage media_;files editorial_is maintained in the backend. Templates should switch fromimages {record.to these properties.media.*}
{entity.media.editorialImages}
to
{entity.editorialImages}
to make use of the full FAL provided
functionality.The json-based opening hours storage has been superseeded by inline database records. The json-based output logic is still in place. The next import will create the inline records as needed. With the next fitting version, the json-based fields will be removed.
Deprecated, triggering E_USER_DEPRECATED on use:
Domain\Model\Frontend\Place::getOpeningHours()
,
getMergedOpeningHours()
,
getSpecialOpeningHours()
and
getMergedSpecialOpeningHours()
(the json-blob read accessors).The legacy
\Domain\,
Merged and
Merged models are deprecated alongside them.
Use
get /
get instead. Re-run the import to
populate the inline records. Templates should switch from
{record. /
{record. to the OpeningHours/PerDayTable partial.
Nothing
Allow none admin users to import.
There was a broken configuration of corresponding TCA tables. It forced to insert log entries on root, whiteout allowing users to insert on root.
Nothing
Nothing
Nothing
Prevent Error: Call to undefined method TYPO3CMSCore with newer TYPO3 v13 versions.
We now check for the old API, falling back to the new.
All of this is still @internal TYPO3 API.
The corresponding core change was commit 45a50e455955c78f6baa2aec3af3865101ee06b9.
We also need to update codappix/ dev dependency for the same reason.
composer.lock file.
As this is an extension without a lock state.
We have different TYPO3 versions as supported versions and it should be easy to switch between them for testing and development.phpstan.neon formatting.Nothing
Removed content element.
No Content element is provided any longer. We recommend to build your own tailored content elements instead.
Nothing
Nothing
Nothing
Nothing
Nothing
typo3/cms-install .
As this provides the upgrade wizard feature.Nothing
Nothing
getSlogans is added which will return the array of slogans.Nothing
Nothing
Nothing
Nothing
Add support for additional images added via TYPO3. Some installations might need to add further images to records imported from ThüCAT. The records are now extended to support adding images by editors. The images are not touched during import. The images are also ignored during clean ups, the editor is in full control.
This feature for now is only added to tourist attractions by default.
The feature is implemented in a way that all objects extending the WerkraumMedia\ThueCat\Domain\Model\Frontend\Base class are usable by adding an editorial_images field to their table.
AccessibilityCertification.
That prevents mapping exceptions for objects containing the corresponding certification with more info than a single value.Nothing
schema:containsPlace entries.
Each of them will be imported.Filter and sort opening hours. Filter out opening hours from the past, they are not available to the template anymore. Sort opening hours from early to later based on their end timing.
This should improve the UX of website visitors. It is not possible yet to sort opening hours by hand within the thuecat backend.
getExtraImages() which will return everything from
getImages() except the getMainImage().
We now also filter out the main image from other images, it will not exist twice
anymore.getParkingFacilitiesNearBySortedByAlphabet().types was added. This is an array of types, e.g. CityBus or
Streetcar.
These can be used with f:translate ViewHelper to provide proper none technical labels.schema:givenName and schema:familyName of schema:author for media.
We only respected schema:name until now.copyrightAuthor holding the actual author for convenience.
It will pick the author value falling back to license.author.Handle multiple thuecat:offerType values within Offer.
The API is none breaking, the models still return only a single offer.
They will filter down to the first offer which contains Offer within the value.
Examples:
Given: Childcare and CourseOffer will result in CourseOffer.
Given: Childcare will result in Childcare.
Existing imported data is still handled.
: in German translation of content.distanceToPublicTransport.
This was the only label with :.Nothing
Nothing
Nothing
Nothing
Changed Fluid templates:
Nothing
Nothing
Nothing
Use proper extension name for translation within templates.
This was sitepackage and was changed to Thuecat.
Changed Fluid templates:
Improved composer authors:
Nothing
Nothing
Nothing
Nothing
Nothing
Nothing
Nothing
Nothing
Nothing
List of changes that need to be done for maintenance reasons. Those affect the extension itself, not users of the extension.
E.g. changes once we drop a certain TYPO3 version. We might have new code backported for compatibility in older TYPO3 versions. Those changes are documented so we know what to do once we drop an older version.
searchFields Drop TCA ctrl/. Those are kept for TYPO3 v13 backwards compatibility
and can be dropped once we drop v13 support.
Necessary for Extbase/Symfony. Those are necessary (at least with TYPO3 v12) because of Extbase and the underlying Symfony component.
Extbase uses the PHPDocExtractor first, before using the Reflection, both part of Symfony property-info package.
The Reflection will check the mutator followed by accessors prior checking the property itself.
Some of our properties have different return values by accessors than the stored value that is set to the property.
We therefore need to keep the PHPDoc block as this is checked first.