Files
VITEC-website/packages/vitec/Resources/Private/Templates/Import/Seo.html
T
orasche 8e67f5db23 Sub-solution workbook pipeline on layout 5, fix element order in both importers
- New, deliberately separate pipeline for sub-solutions (markets and
  solutions must never break each other): generator
  vitec:generate-subsolution-workbooks (list from the page tree below
  /solutions, 39 workbooks, every sub-solution matched to its record),
  SubsolutionWorkbookImporter + CLI vitec:import-subsolution-workbooks,
  and a "Sub-Solutions" tab in the VITEC Import module with its own
  inbox migrations/subsolutions_xlsx_in/
- Targets the existing template 5 "Solution Detail" without changing
  it: hero element in colPos 1, the remaining sections in fixed order in
  colPos 0, found again by CType on re-import; existing empty sub-solution
  pages are switched to layout 5, missing ones created hidden;
  detail_page set on the solution record
- Workbook per the Figma "07 Solution Detail page": hero without video
  teaser, intro with headline and without learn-more/portfolio, extended
  icon list, own page-layout reference image
- Shared content elements: market hero/intro/insights relabelled
  "Market/Solution", market intro gains the optional intro_headline
  field (new column, requires extension:setup)
- Fix: DataHandler puts every NEW record with a positive pid at the top
  of its column, so pages built in one run came out reversed. Both
  importers now track the page order per colPos and write
  tt_content.sorting explicitly after the DataHandler run; re-applying
  repairs pages imported earlier (verified on the control-room
  sub-solution and sub-market page 443)
- migrations/fill_test_subsolution.php: realistic test content from
  vitec.com copy and images for the control-room sub-solution
- Docs section 5z
2026-10-07 15:39:18 +02:00

242 lines
13 KiB
HTML

<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:layout name="Backend/Default" />
<f:section name="main">
<div class="module-docheader-bar module-docheader-bar-navigation">
<div class="module-docheader-bar-column-left">
<h2 class="t3js-title-inlineedit">VITEC Import &mdash; SEO Research</h2>
</div>
</div>
<!-- Tabs -->
<ul class="nav nav-tabs" style="margin-bottom:1rem;">
<f:for each="{models}" as="m">
<li class="nav-item">
<a class="nav-link" href="{f:be.uri(route: 'web_vitecimport', parameters: {model: m.key})}">{m.label}</a>
</li>
</f:for>
<li class="nav-item">
<a class="nav-link" href="{f:be.uri(route: 'web_vitecimport.submarkets')}">Sub-Markets</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{f:be.uri(route: 'web_vitecimport.subsolutions')}">Sub-Solutions</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="{f:be.uri(route: 'web_vitecimport.seo')}">SEO Research</a>
</li>
</ul>
<!-- Upload -->
<div class="card" style="margin-bottom:1rem;">
<div class="card-body">
<form action="{f:be.uri(route: 'web_vitecimport.seo_upload')}" method="post" enctype="multipart/form-data" class="row row-cols-auto align-items-center g-2">
<div class="col">
<input type="file" name="csvfile" accept=".csv,text/csv" class="form-control" required="required" />
</div>
<div class="col">
<button type="submit" class="btn btn-primary">Store delivery</button>
</div>
<div class="col form-text">
Every upload is kept as a delivery and compared against the previous one.
</div>
</form>
</div>
</div>
<f:if condition="{latest}">
<p class="text-muted">
Latest delivery: <strong>{latest.filename}</strong>,
<f:format.date format="d.m.Y H:i">@{latest.crdate}</f:format.date>
({analysis.structure.pages.total} rows)
<f:if condition="{previous}">
&mdash; compared against <f:format.date format="d.m.Y H:i">@{previous.crdate}</f:format.date>
</f:if>
</p>
<!-- Diff -->
<f:if condition="{analysis.diff}">
<div class="card" style="margin-bottom:1rem;">
<div class="card-header"><strong>Changes since previous delivery</strong></div>
<div class="card-body">
<f:if condition="{analysis.diff.added}">
<p><strong>New pages ({analysis.diff.added -> f:count()})</strong></p>
<ul><f:for each="{analysis.diff.added}" as="line"><li>{line}</li></f:for></ul>
</f:if>
<f:if condition="{analysis.diff.removed}">
<p><strong>Removed pages ({analysis.diff.removed -> f:count()})</strong></p>
<ul><f:for each="{analysis.diff.removed}" as="line"><li>{line}</li></f:for></ul>
</f:if>
<f:if condition="{analysis.diff.changed}">
<p><strong>Changed rows ({analysis.diff.changed -> f:count()})</strong></p>
<ul><f:for each="{analysis.diff.changed}" as="line"><li>{line}</li></f:for></ul>
</f:if>
<f:if condition="!{analysis.diff.added} && !{analysis.diff.removed} && !{analysis.diff.changed}">
<p class="text-muted">No differences.</p>
</f:if>
</div>
</div>
</f:if>
<!-- Structure check -->
<div class="card" style="margin-bottom:1rem;">
<div class="card-header"><strong>Structure check &mdash; CSV vs. TYPO3</strong></div>
<div class="card-body">
<p>
<span class="vitec-badge vitec-badge-update">Pages: {analysis.structure.pages.found} / {analysis.structure.pages.total} exist</span>
<span class="vitec-badge vitec-badge-update">Markets: {analysis.structure.market.matched} / {analysis.structure.market.total}</span>
<span class="vitec-badge vitec-badge-update">Solutions: {analysis.structure.solution.matched} / {analysis.structure.solution.total}</span>
<span class="vitec-badge vitec-badge-update">Products: {analysis.structure.product.matched} / {analysis.structure.product.total}</span>
</p>
<p class="form-text">
Markets/Solutions: every row below the section root, including sub-markets and
sub-solutions. Products: level x.y and deeper (the top product rows are
categories). Matching: record slug against the last URL segment, falling back
to a normalized title comparison. The Page Ref column shows the hierarchy.
A row the matcher misses can be <strong>linked by hand</strong> via its select
&mdash; the link is stored per model (shared with the import tabs) and re-applied
on every future delivery. Linked rows show up under &quot;Both&quot; with a badge
and can be re-pointed or removed there.
</p>
<div class="row">
<f:for each="{analysis.structure}" key="area" as="check">
<f:if condition="{area} != 'pages'">
<div class="col-md-4">
<h4 style="text-transform:capitalize;">{area}</h4>
<form action="{f:be.uri(route: 'web_vitecimport.seo_aliases')}" method="post">
<input type="hidden" name="model" value="{area}" />
<f:if condition="{check.both}">
<p><strong>Both in CSV and TYPO3 ({check.both -> f:count()})</strong></p>
<ul>
<f:for each="{check.both}" as="pair" iteration="bIt">
<li>
<code>{pair.ref}</code> {pair.name}
<small class="text-muted">&rarr; uid {pair.uid} ({pair.title})</small>
<f:if condition="{pair.via} == 'link'">
<span class="vitec-badge vitec-badge-info">linked</span>
<input type="hidden" name="aliaskey[b{bIt.index}]" value="{pair.name}" />
<select name="alias[b{bIt.index}]" class="form-select form-select-sm" style="margin:2px 0 6px;">
<option value="0">&mdash; remove link &mdash;</option>
<f:for each="{check.linkable}" as="rec">
<option value="{rec.uid}" {f:if(condition: '{rec.uid} == {pair.uid}', then: 'selected')}>{rec.title} (uid {rec.uid})</option>
</f:for>
</select>
</f:if>
</li>
</f:for>
</ul>
</f:if>
<f:if condition="{check.missing}">
<p><strong>Missing in TYPO3 ({check.missing -> f:count()})</strong></p>
<ul>
<f:for each="{check.missing}" as="row" iteration="mIt">
<li>
<code>{row.ref}</code> {row.name}
<f:if condition="{check.linkable}">
<input type="hidden" name="aliaskey[m{mIt.index}]" value="{row.name}" />
<select name="alias[m{mIt.index}]" class="form-select form-select-sm" style="margin:2px 0 6px;">
<option value="0">&mdash; really missing &mdash;</option>
<f:for each="{check.linkable}" as="rec">
<option value="{rec.uid}">{rec.title} (uid {rec.uid})</option>
</f:for>
</select>
</f:if>
</li>
</f:for>
</ul>
</f:if>
<f:if condition="{check.missing} || {check.both}">
<p><button type="submit" class="btn btn-default btn-sm">Save record links</button></p>
</f:if>
</form>
<f:if condition="{check.extra}">
<p><strong>Only in TYPO3 ({check.extra -> f:count()})</strong></p>
<ul>
<f:for each="{check.extra}" as="rec">
<li>{rec.title} <small class="text-muted">uid {rec.uid}</small></li>
</f:for>
</ul>
</f:if>
<f:if condition="!{check.missing} && !{check.extra}">
<p class="text-muted">Complete match &mdash; every CSV row has its record and vice versa.</p>
</f:if>
</div>
</f:if>
</f:for>
</div>
<f:if condition="{analysis.structure.pages.missing}">
<details>
<summary><strong>Pages missing in TYPO3 ({analysis.structure.pages.missing -> f:count()})</strong></summary>
<ul>
<f:for each="{analysis.structure.pages.missing}" as="row">
<li><code>{row.url}</code> &mdash; {row.name}</li>
</f:for>
</ul>
</details>
</f:if>
</div>
</div>
<!-- Quick wins -->
<div class="card" style="margin-bottom:1rem;">
<div class="card-header"><strong>Quick wins</strong> <span class="vitec-badge vitec-badge-new">{analysis.quickWins -> f:count()}</span></div>
<div class="card-body">
<table class="table table-striped table-sm">
<thead><tr><th>Page</th><th>Primary keyword</th><th>Vol (Global)</th><th>GSC Pos</th><th>GSC Impr.</th></tr></thead>
<tbody>
<f:for each="{analysis.quickWins}" as="row">
<tr>
<td>{row.name}<br /><small class="text-muted">{row.url}</small></td>
<td>{row.primary}</td>
<td>{row.volGlobal}</td>
<td>{row.gscPos}</td>
<td>{row.gscImpr}</td>
</tr>
</f:for>
</tbody>
</table>
</div>
</div>
<!-- Shared terms -->
<div class="card" style="margin-bottom:1rem;">
<div class="card-header"><strong>Shared terms (cannibalization risk)</strong> <span class="vitec-badge vitec-badge-info">{analysis.sharedTerms -> f:count()}</span></div>
<div class="card-body">
<f:for each="{analysis.sharedTerms}" as="group">
<p style="margin-bottom:4px;"><strong>{group.keyword}</strong></p>
<ul>
<f:for each="{group.pages}" as="row">
<li>{row.name} <small class="text-muted">{row.url}</small></li>
</f:for>
</ul>
</f:for>
</div>
</div>
<!-- Already ranking -->
<div class="card" style="margin-bottom:1rem;">
<div class="card-header"><strong>Already ranking</strong> <span class="vitec-badge vitec-badge-unchanged">{analysis.alreadyRanking -> f:count()}</span></div>
<div class="card-body">
<ul>
<f:for each="{analysis.alreadyRanking}" as="row">
<li>{row.name} <small class="text-muted">{row.url}</small> &mdash; {row.primary} (GSC Pos {row.gscPos})</li>
</f:for>
</ul>
</div>
</div>
</f:if>
<f:if condition="!{latest}">
<p class="text-muted">No delivery stored yet - upload the keyword research CSV above.</p>
</f:if>
</f:section>
</html>