---
title: "Frontend output"
manual: "TYPO3 EXT:thuecat"
version: "main"
source: "FrontendOutput/Index.rst"
rendered: "2026-09-19T09:50:46+00:00"
---

# Frontend output {#frontend-output}

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.

## Plugins and templates {#plugins-and-templates}

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](#filtered-list)). The search element is a combined
**search and filter** form.

## Required pages {#required-pages}

Set up these pages once and configure their ids (see [Site settings](#site-settings)):

-   **Storage folder** -- a sysfolder holding the tourist attraction records.
-   **Detail page** -- carries the detail content element; target for links to a
    single attraction.
-   **List / search page** -- carries **both** the list content element and the
    search-and-filter content element.

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.

## Content elements {#content-elements}

Register one content element per plugin in your own extension or sitepackage, for
example with [Content Blocks](https://docs.typo3.org/p/friendsoftypo3/content-blocks/main/en-us/Index.html#start). Each content element
wires its `CType` to a plugin via the `ThueCat` extension name:

```typoscript
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
[Content element example](../ContentElementExample.html).

## Templates {#templates}

The shipped stubs under `EXT:thuecat/Resources/Private/Templates/` are
registered at `templateRootPaths.10`. Override them by adding a higher index:

```typoscript
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/List`
-   `TouristAttraction/SelectedList`
-   `TouristAttraction/SearchForm`
-   `TouristAttraction/Show`

## Further topics {#further-topics}

Some fields carry richer data than a single value and have their own model
accessors and partials; rendered output is cached. These are covered on their own
pages:

-   [Opening hours](OpeningHours.html#opening-hours)
-   [Media](Media.html#media)
-   [Caching](Caching.html#caching)

## Site settings {#site-settings}

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`):

```yaml
settings:
  thuecat.pois.pid_show:
    label: 'Detail Page for Tourist Attractions'
    description: 'The page providing the detail pages for tourist attractions'
    category: 'page.pids'
    type: 'int'
    default: 0
  thuecat.pois.pid_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
  thuecat.settings.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`):

```typoscript
plugin.tx_thuecat.settings {
    page.pid {
        thuecat_attraction_show = {$thuecat.pois.pid_show}
        thuecat_attraction_search = {$thuecat.pois.pid_search}
    }
    itemsPerPage = {$thuecat.settings.itemsPerPage}
}
```

## Search and list on one page {#search-and-list-on-one-page}

The search-and-filter form adapts to what shares its page:

-   **With a list (plain or filtered) on the page** the form posts to the same page
    and the list re-renders with the result.
-   **Without a list on the page** the form targets the configured list / search
    page (`page.pid.thuecat_attraction_search`).
-   **On a filtered list** the preset fields are not shown in the search form, but rendered as hidden fields to preserve
    the pre-selection. The
    visitor refines the remaining fields but cannot widen past the preset.

After a search the form re-populates with the submitted values, so the visitor
keeps their input.

## Filtered list {#filtered-list}

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.

## Registering a filter field {#frontend-output-filter-fields}

Which filters the search mask offers is not a list in the controller. A filter
field is a tagged service implementing
`\WerkraumMedia\ThueCat\Service\FilterField\FilterFieldDefinition`, tagged
`search.filter.field`.
`\WerkraumMedia\ThueCat\Service\SearchFilterOptionsService` discovers every
implementation through that tag and builds it. Another extension adds a filter
without editing any class here.

Two storage shapes are supported, each with an abstract base to extend. Pick the
one matching where the values live. Both live in
`Classes/Service/FilterField/`; the examples are the shipped fields, in the
same directory.

Values in a comma-separated uid column on the record itself, i.e. a TCA select
without `MM` — base
`\WerkraumMedia\ThueCat\Service\FilterField\CommaColumnField`:

```php
<?php

namespace Vendor\Extension\Service\FilterField;

use WerkraumMedia\ThueCat\Service\FilterField\CommaColumnField;

final class TownFilterField extends CommaColumnField
{
    public function __construct()
    {
        parent::__construct(
            name: 'towns',
            recordColumn: 'town',
            optionTable: 'tx_thuecat_town',
        );
    }
}
```

Values in an MM table over a parent/child option table, offered as the tree below
a configured anchor — base
`\WerkraumMedia\ThueCat\Service\FilterField\HierarchicalMmField`:

```php
<?php

namespace Vendor\Extension\Service\FilterField;

use WerkraumMedia\ThueCat\Import\Settings\CategoryAnchorSetting;
use WerkraumMedia\ThueCat\Service\FilterField\HierarchicalMmField;

final class CategoryFilterField extends HierarchicalMmField
{
    public function __construct()
    {
        parent::__construct(
            name: 'categories',
            mmTable: 'sys_category_record_mm',
            mmFieldName: 'categories',
            optionTable: 'sys_category',
            parentColumn: 'parent',
            anchorSetting: CategoryAnchorSetting::CategoryParent,
        );
    }
}
```

With `autoconfigure` enabled the tag is applied by the interface, so no entry
in `Services.yaml` is needed.

### What each argument means {#what-each-argument-means}

-   **`name`**

    The demand property and the view key. The template binds the option set under
    this name, so a field named `towns` is rendered from `{towns}`. It is not
    the MM field name: the two happen to coincide today but answer to different
    owners.

-   **`recordColumn`**

    The column holding the comma-separated uids. A record without a value carries
    the placeholder `0`, which is never offered as an option.

-   **`mmFieldName`**

    Which field of a shared MM table the relation belongs to. Categories and
    keywords share `sys_category_record_mm` and are told apart by this alone,
    so it is part of every query.

-   **`anchorSetting`**

    A case of
    `\WerkraumMedia\ThueCat\Import\Settings\CategoryAnchorSetting`, naming the
    site setting that holds the category the offered tree starts below. The
    declaration carries the *setting*, never a resolved uid — anchors are per site
    and are resolved per request. An unconfigured anchor offers nothing.

The record table is deliberately absent. It belongs to the scope, not to the
field, so one field definition serves every record kind that offers it.

### What a field does not decide {#what-a-field-does-not-decide}

A field declares where its values are read from and nothing else. Scoping is
resolved once per request and applied to every field alike:

-   Only values carried by records the sibling list can return are offered. With
    no list on the page, each field offers what its storage holds.
-   Values are bound to the site. A value living outside the list's storage pages
    is still offered; one belonging to another site is not.
-   Option sets never depend on the visitor's current selection. Choosing one town
    does not remove the others from the form.

### Adding a further storage shape {#adding-a-further-storage-shape}

If neither base fits, the shape needs its own provider: implement
`\WerkraumMedia\ThueCat\Service\FilterField\OptionProvider\FilterOptionProvider`,
tagged `search.filter.option.provider`, returning `true` from
`supports()` for the field class it reads. Providers are discovered by that
tag, so the service picks one up without being edited.

Extending
`\WerkraumMedia\ThueCat\Service\FilterField\OptionProvider\AbstractOptionProvider`
supplies the restriction handling every option query needs — deleted, disabled,
language, storage pages and the site bound — which is easy to get subtly wrong
when writing a provider from scratch. The shipped providers,
`CommaColumnOptionProvider` and `HierarchicalOptionProvider`, sit in the
same namespace as worked examples.

## Filtering by keyword {#frontend-output-keywords}

Places can be filtered by the [keywords](../Configuration.html#import-keywords) the import stored
on them. The filter behaves like the category filter beside it: selecting several
keywords widens the result, while adding a town or a category narrows it.

Which keywords a plugin offers is chosen **per plugin**, in the FlexForm of the
attraction list. A plugin with no keyword selected offers no keyword control and
applies no keyword constraint, so existing plugins keep behaving as they did.

The selection is bounded by the site's keyword anchor: the field offers the terms
below [import.thuecat.keywords.parent](../Configuration.html#import-category-based-anchors) and
nothing else, so the type-category tree cannot be picked here by mistake. A site
that configures no keyword anchor therefore offers nothing to choose.

Because the anchor belongs to the site, one site running several imports into the
same keyword parent shares one vocabulary across its plugins. Pointing separate
imports at separate keyword parents gives each list its own.

The chosen keywords survive pagination, and the filter mask shows them grouped by
their parent set so long vocabularies stay navigable.

## Keyword meta tags {#keyword-meta-tags}

A place detail view emits a `keywords` meta tag built from the record's related
keyword categories, joined by `", "`. A place with no keyword relations emits no
keyword meta tag.

## How it works {#how-it-works}

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:

-   No list on the page -- the form targets the configured list / search page.
-   A list on the page -- the form stays on the page; a filtered list additionally
    contributes the preselected fields.

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.
