Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55f7f82a42 | ||
|
|
c90649fc1e |
No files matched your search
Generated
+321
-287
File diff suppressed because it is too large.
Load diff
@@ -6,7 +6,6 @@ dependencies:
|
||||
- typo3/form
|
||||
- typo3/seo-sitemap
|
||||
- friendsoftypo3/headless
|
||||
- friendsoftypo3/headless-mixed
|
||||
- nb-headless-content-blocks/headless-content-blocks
|
||||
- evomedien/vitecset
|
||||
- vitec/content-blocks-bundle
|
||||
|
||||
@@ -3,3 +3,4 @@ my.example.setting: Test
|
||||
menu.footer.pageUids: '1,2,3'
|
||||
menu.meta.pageUids: '5'
|
||||
megamenu.contentUid: 10366
|
||||
vitec.storyDetailPid: 26
|
||||
+9
-3
@@ -16,9 +16,15 @@ set -u
|
||||
cd "$(dirname "$0")" || exit 1
|
||||
|
||||
echo "[perms] Project root: $(pwd)"
|
||||
echo "[perms] Setting files 644 / directories 755 under Resources/Public + _assets ..."
|
||||
find public/_assets public/fileadmin/icons packages/vitec/Resources/Public -type f -exec chmod 644 {} \; 2>/dev/null
|
||||
find public/_assets public/fileadmin/icons packages/vitec/Resources/Public -type d -exec chmod 755 {} \; 2>/dev/null
|
||||
# public/fileadmin as a whole, not just icons: every editorial upload and every
|
||||
# importer output lands there (success_stories_import, blog-import, downloads,
|
||||
# user_upload/products ...) and hits the same 403 otherwise. Found 2026-09-11,
|
||||
# when the imported success-story images stayed invisible in the megamenu.
|
||||
TARGETS="public/_assets public/fileadmin packages/vitec/Resources/Public"
|
||||
|
||||
echo "[perms] Setting files 644 / directories 755 under $TARGETS ..."
|
||||
find $TARGETS -type f -exec chmod 644 {} \; 2>/dev/null
|
||||
find $TARGETS -type d -exec chmod 755 {} \; 2>/dev/null
|
||||
|
||||
echo "[perms] Flushing TYPO3 caches ..."
|
||||
if [ -x vendor/bin/typo3 ]; then
|
||||
|
||||
@@ -204,6 +204,14 @@ class ProductListJsonRenderer
|
||||
'layout' => (string)($settings['layout'] ?? '0'),
|
||||
'showToolbar' => (bool)($settings['showtoolbar'] ?? false),
|
||||
'products' => $productsData,
|
||||
// Success stories the editor attached to these categories
|
||||
// (sys_category, Options tab). They ride along with the list
|
||||
// so the front end can hide them with the same category
|
||||
// filter it applies to the products.
|
||||
'successStories' => $this->successStoriesFor(
|
||||
$allProducts ? [] : $categoryUids,
|
||||
(int)($settings['storysinglepid'] ?? 0)
|
||||
),
|
||||
];
|
||||
|
||||
if ($debugMode) {
|
||||
@@ -222,6 +230,141 @@ class ProductListJsonRenderer
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Success stories attached to the given categories, in the order the
|
||||
* categories were resolved and, inside one category, in the order the
|
||||
* editor arranged them. A story linked from several of the categories
|
||||
* appears once and carries all of them in `forCategories`, which is what
|
||||
* the front end filters on: the product finder writes ?cat= / ?subcat=,
|
||||
* and a card stays visible while one of its categories is selected.
|
||||
*
|
||||
* Cards use the shared story shape (UsecaseSerializer), so the front end
|
||||
* can render them with the same component as every other story list.
|
||||
* `detailUrl` follows the same rule as the story list: the story's own
|
||||
* detail page wins, otherwise the Single PID configured on this element
|
||||
* plus the slug - and null when neither is available.
|
||||
*
|
||||
* Returned in the list envelope of 7.3 - `layout` plus the array - so the
|
||||
* front end treats these cards like every other story list. The layout is
|
||||
* an editorial setting on the category, next to the selection itself.
|
||||
*
|
||||
* @param int[] $categoryUids
|
||||
* @return array{layout:string,stories:array<int,array<string,mixed>>}
|
||||
*/
|
||||
private function successStoriesFor(array $categoryUids, int $singlePid): array
|
||||
{
|
||||
if ($categoryUids === []) {
|
||||
return ['layout' => 'grid', 'stories' => []];
|
||||
}
|
||||
|
||||
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||
->getQueryBuilderForTable('sys_category');
|
||||
$rows = $queryBuilder->select('uid', 'tx_vitec_success_stories', 'tx_vitec_success_stories_layout')->from('sys_category')
|
||||
->where(
|
||||
$queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($categoryUids, Connection::PARAM_INT_ARRAY)),
|
||||
$queryBuilder->expr()->eq('deleted', 0),
|
||||
$queryBuilder->expr()->eq('hidden', 0)
|
||||
)->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$listByCategory = [];
|
||||
$layoutByCategory = [];
|
||||
foreach ($rows as $row) {
|
||||
$listByCategory[(int)$row['uid']] = (string)($row['tx_vitec_success_stories'] ?? '');
|
||||
$layoutByCategory[(int)$row['uid']] = (string)($row['tx_vitec_success_stories_layout'] ?? '') ?: 'grid';
|
||||
}
|
||||
|
||||
$order = [];
|
||||
$forCategories = [];
|
||||
$layout = 'grid';
|
||||
foreach ($categoryUids as $categoryUid) {
|
||||
foreach (explode(',', $listByCategory[(int)$categoryUid] ?? '') as $raw) {
|
||||
$storyUid = (int)trim($raw);
|
||||
if ($storyUid <= 0) {
|
||||
continue;
|
||||
}
|
||||
if ($order === []) {
|
||||
// The first category that actually contributes stories sets
|
||||
// the layout - that is the top-most one the element is
|
||||
// configured with, which is where an editor expects the
|
||||
// setting to live.
|
||||
$layout = $layoutByCategory[(int)$categoryUid] ?? 'grid';
|
||||
}
|
||||
if (!in_array($storyUid, $order, true)) {
|
||||
$order[] = $storyUid;
|
||||
}
|
||||
$forCategories[$storyUid][] = (int)$categoryUid;
|
||||
}
|
||||
}
|
||||
if ($order === []) {
|
||||
return ['layout' => 'grid', 'stories' => []];
|
||||
}
|
||||
|
||||
$storyQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
|
||||
->getQueryBuilderForTable('tx_vitec_domain_model_usecase');
|
||||
$storyRows = $storyQueryBuilder->select('*')->from('tx_vitec_domain_model_usecase')
|
||||
->where(
|
||||
$storyQueryBuilder->expr()->in('uid', $storyQueryBuilder->createNamedParameter($order, Connection::PARAM_INT_ARRAY)),
|
||||
$storyQueryBuilder->expr()->eq('deleted', 0),
|
||||
$storyQueryBuilder->expr()->eq('hidden', 0)
|
||||
)->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$byUid = [];
|
||||
foreach ($storyRows as $storyRow) {
|
||||
$byUid[(int)$storyRow['uid']] = $storyRow;
|
||||
}
|
||||
|
||||
// Same base as the story list: the detail page's PARENT path, because
|
||||
// SuccessStoryPathRewrite maps /success-stories/<slug> onto the detail
|
||||
// subpage at request time. Falls back to the site setting
|
||||
// `vitec.storyDetailPid`, so the editor does not have to configure the
|
||||
// page on every single product list.
|
||||
if ($singlePid <= 0) {
|
||||
$singlePid = $this->storyDetailPidFromSite();
|
||||
}
|
||||
$detailBase = '';
|
||||
if ($singlePid > 0) {
|
||||
$detailPath = rtrim(\Evomedien\Vitec\Service\LinkResolver::pageUrl($singlePid) ?? '', '/');
|
||||
$parent = str_contains($detailPath, '/') ? substr($detailPath, 0, (int)strrpos($detailPath, '/')) : '';
|
||||
$detailBase = $parent !== '' ? $parent : $detailPath;
|
||||
}
|
||||
|
||||
$serializer = GeneralUtility::makeInstance(\Evomedien\Vitec\Service\UsecaseSerializer::class);
|
||||
$stories = [];
|
||||
foreach ($order as $storyUid) {
|
||||
if (!isset($byUid[$storyUid])) {
|
||||
continue; // hidden or deleted meanwhile - simply drops out
|
||||
}
|
||||
$item = $serializer->serializeListItem($byUid[$storyUid]);
|
||||
if (($item['detailUrl'] ?? null) === null && $detailBase !== '' && ($item['slug'] ?? '') !== '') {
|
||||
$item['detailUrl'] = $detailBase . '/' . ltrim((string)$item['slug'], '/');
|
||||
}
|
||||
// NOT the story's own topic categories (those stay in `categories`)
|
||||
// but the product categories it was attached to.
|
||||
$item['forCategories'] = array_values(array_unique($forCategories[$storyUid]));
|
||||
$stories[] = $item;
|
||||
}
|
||||
|
||||
return ['layout' => $layout, 'stories' => $stories];
|
||||
}
|
||||
|
||||
/**
|
||||
* Site-wide fallback for the success story detail page, 0 when unset.
|
||||
* Read from the site settings rather than hard-coded, so the page can be
|
||||
* moved without touching code.
|
||||
*/
|
||||
private function storyDetailPidFromSite(): int
|
||||
{
|
||||
try {
|
||||
$site = ($GLOBALS['TYPO3_REQUEST'] ?? null)?->getAttribute('site');
|
||||
if ($site === null || !method_exists($site, 'getSettings')) {
|
||||
return 0;
|
||||
}
|
||||
return (int)$site->getSettings()->get('vitec.storyDetailPid', 0);
|
||||
} catch (\Throwable) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The given category uids plus every descendant category uid, DEPTH first
|
||||
* over sys_category.parent with siblings in sys_category.sorting order -
|
||||
|
||||
@@ -100,6 +100,16 @@
|
||||
<default>0</default>
|
||||
</config>
|
||||
</settings.showtoolbar>
|
||||
<settings.storysinglepid>
|
||||
<label>Success stories: detail page</label>
|
||||
<description>Needed only so the success-story cards below this list get a link. Pick the same story detail page the Success Stories list uses. Leave empty and the cards render without a link.</description>
|
||||
<config>
|
||||
<type>group</type>
|
||||
<allowed>pages</allowed>
|
||||
<size>1</size>
|
||||
<maxitems>1</maxitems>
|
||||
</config>
|
||||
</settings.storysinglepid>
|
||||
</el>
|
||||
</ROOT>
|
||||
</sDEF>
|
||||
|
||||
@@ -29,6 +29,13 @@ settings:
|
||||
type: bool
|
||||
default: false
|
||||
|
||||
vitec.storyDetailPid:
|
||||
label: 'Success story detail page'
|
||||
description: 'The page that renders a single success story. Used to build story links where no page is configured on the element itself - the story URL is that page''s parent path plus the story slug, which is what SuccessStoryPathRewrite expects. 0 = no fallback.'
|
||||
category: VITEC
|
||||
type: int
|
||||
default: 0
|
||||
|
||||
menu.footer.pageUids:
|
||||
label: 'Footer menu — page UIDs'
|
||||
description: 'Comma-separated list of page UIDs to display in the footer menu (in order).'
|
||||
|
||||
@@ -32,6 +32,47 @@ $customSysCategoryColumns = [
|
||||
'eval' => 'trim',
|
||||
],
|
||||
],
|
||||
// Success stories shown below the product list of this category. Lives on
|
||||
// the category rather than on the plugin so one editorial decision serves
|
||||
// every list that renders the category (decision 2026-09-11).
|
||||
'tx_vitec_success_stories' => [
|
||||
'exclude' => true,
|
||||
'label' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:sys_category.tx_vitec_success_stories',
|
||||
'description' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:sys_category.tx_vitec_success_stories.description',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'renderType' => 'selectMultipleSideBySide',
|
||||
'foreign_table' => 'tx_vitec_domain_model_usecase',
|
||||
// No ORDER BY in foreign_table_where: TYPO3 appends its own
|
||||
// conditions after this fragment, which turns an ORDER BY here
|
||||
// into a syntax error. Sorting belongs in `sortItems`.
|
||||
'foreign_table_where' => 'AND {#tx_vitec_domain_model_usecase}.{#sys_language_uid} IN (0,-1)',
|
||||
'sortItems' => ['label' => 'asc'],
|
||||
'size' => 8,
|
||||
'minitems' => 0,
|
||||
'maxitems' => 20,
|
||||
'enableMultiSelectFilterTextfield' => true,
|
||||
],
|
||||
],
|
||||
// Same vocabulary as the story and market lists (grid|list|carousel|50-50),
|
||||
// so the front end renders these cards with the component it already has.
|
||||
'tx_vitec_success_stories_layout' => [
|
||||
'exclude' => true,
|
||||
'displayCond' => 'FIELD:tx_vitec_success_stories:REQ:true',
|
||||
'label' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:sys_category.tx_vitec_success_stories_layout',
|
||||
'description' => 'LLL:EXT:vitec/Resources/Private/Language/locallang_db.xlf:sys_category.tx_vitec_success_stories_layout.description',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'renderType' => 'selectSingle',
|
||||
'default' => 'grid',
|
||||
'items' => [
|
||||
['label' => 'Grid', 'value' => 'grid'],
|
||||
['label' => 'List', 'value' => 'list'],
|
||||
['label' => 'Carousel', 'value' => 'carousel'],
|
||||
['label' => '50 / 50', 'value' => '50-50'],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Add the fields to the TCA
|
||||
@@ -43,4 +84,15 @@ $customSysCategoryColumns = [
|
||||
'class, filetype, type',
|
||||
'',
|
||||
'after:description'
|
||||
);
|
||||
|
||||
// Success stories go into the existing "Options" tab. That tab comes from
|
||||
// EXT:news (it holds `images`), so anchoring after that field puts our field
|
||||
// INSIDE it - adding our own --div--;...options would produce a second tab of
|
||||
// the same name. EXT:news loads before EXT:vitec, so the anchor exists.
|
||||
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes(
|
||||
'sys_category',
|
||||
'tx_vitec_success_stories, tx_vitec_success_stories_layout',
|
||||
'',
|
||||
'after:images'
|
||||
);
|
||||
@@ -3,7 +3,7 @@
|
||||
| | |
|
||||
|---|---|
|
||||
| **Document identifier** | EVO‑VITEC‑HL‑001 |
|
||||
| **Version** | 1.15 |
|
||||
| **Version** | 1.17 |
|
||||
| **Status** | Released |
|
||||
| **Date** | 2026‑09‑11 |
|
||||
| **Applies to** | `evomedien/vitec` on TYPO3 v14.3 (headless) |
|
||||
@@ -26,9 +26,10 @@
|
||||
| 1.10 | 2026‑08‑18 | **Interface change (additive).** `tx_vitec_domain_model_product` gained five fields: `heroimage` (multiple FAL images, detail payload only), the richtext fields `description2`, `capabilities` and `textrelatedproducts`, and `portfolio` (TCA `link`) — emitted as a **resolved URL** through the new `LinkResolver::typolinkUrl()`. The product payloads are specified for the first time (7.15). Links in `contentelement` / `contentelementcta` that point at a **container** now resolve its children (`items`, page‑level shape) and `background` (7.4). Two record link handlers (`download`, `product`) added to the link browser, resolved server‑side per the new Clause 9.12; new middleware `vitec/download-file` streams `/download/file/<uid>` as a forced download (5.3), file lookup consolidated into `DownloadFileResolver` — first step towards the B‑4 target (10.2). Backend‑only: `relatedprodukt` moved from the Misc tab to General. Editorial: the document footer had been stuck at v1.7 since v1.8. |
|
||||
| 1.11 | 2026‑08‑21 | **New interface: site search.** Apache Solr 10 (dedicated VPS behind an HTTPS reverse proxy) with `apache-solr-for-typo3/solr` 14.0.0-RC1. The EXT:solr results plugin `solr_pi_results` on the search page is rendered headless by `SearchJsonRenderer` (payload key `search`) - request/response contract in the new Clause 7.16. Indexed corpus: pages plus product, market (`detail_page` only), use-case, news and download records; result `type` vocabulary `page\|product\|market\|story\|news\|download`. Downloads gained the canonical route `/download/<slug>` (uid route kept for the record links) and `private_download` is now enforced by `DownloadFileResolver` (5.3, 9.12). The VITEC Set now declares the solr set as a dependency - overriding a foreign set’s TypoScript requires loading after it (5.2). Editorial: the header table had been stuck at v1.9 since v1.10. |
|
||||
| 1.12 | 2026‑08‑24 | **Interface change (additive).** The search endpoint (7.16) gained a type filter and facet counts: request parameter `filter` (a value from the `type` vocabulary; the natural name `type` is unavailable - it is TYPO3’s reserved page-type parameter), response keys `filter` (active filter or null) and `facets.type` (per-type document counts with `active` flags; counts stay complete while a filter is active, except when the filtered result is empty). Editorial baseline of 76 managed synonyms imported into `core_en` (codecs, acquired-brand names such as `exterity => avedia`, UK/US spellings, common misspellings) - synonyms apply at query time, no re-index. New autocomplete endpoint: the EXT:solr suggest plugin as lean JSON page type 7384, deliberately USER_INT (7.16). |
|
||||
| 1.13 | 2026‑09‑11 | **New plugin** `vitec_productfinder` (`ProductFinderJsonRenderer`, payload key `productfinder`): the product category hierarchy on its own, as the data source for the filter selects above the product lists (7.15.3, 8). No change to `vitec_productlist` — filtering happens client side over the products already delivered, keyed on the category uids this payload carries. |
|
||||
| 1.14 | 2026‑09‑11 | **Interface change (additive).** Secondary search facets: stories index their market titles (`market_stringM`, MM relation), products their sys_category titles (`category_stringM`); the search endpoint accepts `market` and `category` GET parameters (values verbatim from `facets.<name>[].value`; an unknown value simply yields zero results) and echoes them as `activeFacets`. Visibility fix: the usecases queue now honours `no_index`/`hideonwebsite`, products defensively `hideonwebsite` - and the excluded story’s document was removed explicitly, because re-indexing never deletes. |
|
||||
| 1.15 | 2026‑09‑11 | **Corpus change (additive, index side).** Download documents now carry the extracted file text: Solr 10 has no ExtractingRequestHandler, so a dedicated Apache Tika container on the Solr VPS (Caddy route `/tika/*`, own basic-auth credential) extracts PDF/Office contents during indexing (`TikaDownloadContentIndexer` on `BeforeDocumentIsProcessedForIndexingEvent`, fail-soft per 9.5, cached in `var/tika-cache` keyed on path+size+mtime). Datasheet specifications are now searchable ("625i", "genlock"). No interface change. |
|
||||
| 1.16 | 2026‑09‑11 | **New plugin** `vitec_productfinder` (`ProductFinderJsonRenderer`, payload key `productfinder`): the product category hierarchy on its own, as the data source for the filter selects above the product lists (7.15.3, 8). No change to `vitec_productlist` — filtering happens client side over the products already delivered, keyed on the category uids this payload carries. |
|
||||
| 1.17 | 2026‑09‑11 | **Interface change (additive).** The product list payload gained `successStories` (7.15.2.1): stories an editor attaches to a `sys_category` (tab *Options*, with its own layout), emitted in the list envelope beside `products`. Each card carries `forCategories` — the product categories it came from — so the front end can hide it with the same `?cat=`/`?subcat=` filter it applies to the products. Story links reuse the 7.5 rule, with the detail page taken from the element or the new site setting `vitec.storyDetailPid`. |
|
||||
|
||||
This document is drafted in the style of, and adopts the terminology conventions of,
|
||||
ISO/IEC/IEEE 42010 (architecture description), ISO/IEC/IEEE 26514 (information for
|
||||
@@ -907,6 +908,51 @@ rarely shows. The `layout` vocabulary remains `0`–`3` (see the v1.9 note).
|
||||
|
||||
---
|
||||
|
||||
#### 7.15.2.1 Success stories below a product list
|
||||
|
||||
The list payload carries a second array beside `products`: the success stories
|
||||
an editor attached to the categories this element renders. The link lives on
|
||||
`sys_category` (tab *Options*), not on the plugin, so one decision serves every
|
||||
list that shows the category.
|
||||
|
||||
```json
|
||||
"successStories": {
|
||||
"layout": "grid",
|
||||
"stories": [
|
||||
{
|
||||
"uid": 12,
|
||||
"title": "Abu Dhabi Islamic Bank",
|
||||
"detailUrl": "/success-stories/abu-dhabi-islamic-bank",
|
||||
"cardImage": { },
|
||||
"categories": [ ],
|
||||
"forCategories": [58]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Envelope per 7.3 (`layout` plus the array); `layout` uses the story-list
|
||||
vocabulary `grid|list|carousel|50-50` and comes from the first category that
|
||||
contributes stories. Cards are the shared story shape of 7.5, so the same
|
||||
front-end component renders them.
|
||||
|
||||
`forCategories` is the addition: the **product** categories the story was
|
||||
attached to — not to be confused with `categories`, which stays the story's own
|
||||
topic categories. The front end filters on it, using the same `?cat=` /
|
||||
`?subcat=` parameters the product finder (7.15.3) writes: a card stays visible
|
||||
while one of its categories is selected. Stories are collected over the
|
||||
configured categories **and their descendants**, deduplicated, each carrying
|
||||
every category it came from. An element configured to show all products has no
|
||||
category context and returns the empty envelope.
|
||||
|
||||
`detailUrl` follows 7.5: the story's own `detail_page` wins; otherwise the URL
|
||||
is built from the detail page's parent path plus the story slug — the form
|
||||
`SuccessStoryPathRewrite` maps back. That page is taken from the element's
|
||||
FlexForm, falling back to the site setting `vitec.storyDetailPid`, and
|
||||
`detailUrl` is `null` when neither is set.
|
||||
|
||||
---
|
||||
|
||||
#### 7.15.3 Product Finder payload (`vitec_productfinder`, key `productfinder`)
|
||||
|
||||
The filter above the product lists is fed by its own element. It carries the
|
||||
|
||||
@@ -27,6 +27,18 @@
|
||||
<trans-unit id="tx_vitec_domain_model_product.slug.description" resname="tx_vitec_domain_model_product.slug.description">
|
||||
<source>Slug</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sys_category.tx_vitec_success_stories" resname="sys_category.tx_vitec_success_stories">
|
||||
<source>Success Stories</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sys_category.tx_vitec_success_stories.description" resname="sys_category.tx_vitec_success_stories.description">
|
||||
<source>Shown as cards below the product list of this category. The order set here is the order they appear in.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sys_category.tx_vitec_success_stories_layout" resname="sys_category.tx_vitec_success_stories_layout">
|
||||
<source>Layout</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sys_category.tx_vitec_success_stories_layout.description" resname="sys_category.tx_vitec_success_stories_layout.description">
|
||||
<source>Display variant for these story cards - same options as the success story list.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -362,3 +362,12 @@ CREATE TABLE tx_vitec_product_workbook (
|
||||
tstamp int(11) DEFAULT '0' NOT NULL,
|
||||
PRIMARY KEY (product_uid)
|
||||
);
|
||||
|
||||
#
|
||||
# Success stories shown below the product list of a category (2026-09-11).
|
||||
# Comma-separated usecase uids, order = the editor's arrangement.
|
||||
#
|
||||
CREATE TABLE sys_category (
|
||||
tx_vitec_success_stories varchar(1024) DEFAULT '' NOT NULL,
|
||||
tx_vitec_success_stories_layout varchar(30) DEFAULT 'grid' NOT NULL
|
||||
);
|
||||
+27
-27
@@ -1,27 +1,27 @@
|
||||
RewriteEngine On
|
||||
RewriteBase /_frontend/
|
||||
|
||||
RewriteRule ^index\.html$ - [L]
|
||||
|
||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||
RewriteCond %{REQUEST_FILENAME} -d
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
RewriteRule ^ index.html [L]
|
||||
|
||||
# ── Cache headers ────────────────────────────────────────────────────────────
|
||||
|
||||
# index.html: never cache — always fetch fresh so the browser picks up
|
||||
# the latest hashed JS/CSS filenames after a deploy.
|
||||
<Files "index.html">
|
||||
Header set Cache-Control "no-store, no-cache, must-revalidate"
|
||||
Header set Pragma "no-cache"
|
||||
Header set Expires "0"
|
||||
</Files>
|
||||
|
||||
# Hashed assets (JS, CSS, fonts, images inside /assets/):
|
||||
# Vite appends a content hash to every filename, so these are immutable.
|
||||
# Cache them for 1 year — a new deploy produces new filenames automatically.
|
||||
<FilesMatch "\.(js|css|woff2?|ttf|eot|otf)$">
|
||||
Header set Cache-Control "public, max-age=31536000, immutable"
|
||||
</FilesMatch>
|
||||
RewriteEngine On
|
||||
RewriteBase /_frontend/
|
||||
|
||||
RewriteRule ^index\.html$ - [L]
|
||||
|
||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||
RewriteCond %{REQUEST_FILENAME} -d
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
RewriteRule ^ index.html [L]
|
||||
|
||||
# ── Cache headers ────────────────────────────────────────────────────────────
|
||||
|
||||
# index.html: never cache — always fetch fresh so the browser picks up
|
||||
# the latest hashed JS/CSS filenames after a deploy.
|
||||
<Files "index.html">
|
||||
Header set Cache-Control "no-store, no-cache, must-revalidate"
|
||||
Header set Pragma "no-cache"
|
||||
Header set Expires "0"
|
||||
</Files>
|
||||
|
||||
# Hashed assets (JS, CSS, fonts, images inside /assets/):
|
||||
# Vite appends a content hash to every filename, so these are immutable.
|
||||
# Cache them for 1 year — a new deploy produces new filenames automatically.
|
||||
<FilesMatch "\.(js|css|woff2?|ttf|eot|otf)$">
|
||||
Header set Cache-Control "public, max-age=31536000, immutable"
|
||||
</FilesMatch>
|
||||
@@ -25,8 +25,8 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<title>VITEC</title>
|
||||
<script type="module" crossorigin src="/_frontend/assets/index-CrOmtYLr.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/_frontend/assets/index-CrFFiPFX.css">
|
||||
<script type="module" crossorigin src="/_frontend/assets/index-BhQWo9uW.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/_frontend/assets/index-CQJ1iyvA.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="page"></div>␍
|
||||
|
||||
Reference in new issue
Block a user