Go-Live Check 

Extension key

golive_check

Package name

webagentur-yahya/golive-check

Version

1.0

Language

en

Author

Aji Yahya, Webagentur Yahya

License

This document is published under the Creative Commons BY 4.0 license.

Rendered

Thu, 13 Aug 2026 10:01:34 +0000


The classic mistakes made when a TYPO3 website goes live, checked against the site itself: 23 automatic checks that name the value they actually found, say in one sentence why it matters, and link to the place where it is fixed — plus twelve points only a human can confirm, recorded with the user who confirmed them and when.


Table of Contents:

Introduction 

What it does 

A robots.txt taken along from staging hides the entire site. Nothing breaks, the pages look right, and the mistake surfaces weeks later when someone asks why the site is not in Google. The sitemap line in robots.txt is the only place in a TYPO3 configuration where the domain is spelled out, so it is the only one that does not travel along when the site moves. A 404 handler pointing at a hidden page turns every unknown address into a server error.

This extension adds one backend module that looks for those, and for about two dozen more. It reads the site configuration, the page tree and $GLOBALS['TYPO3_CONF_VARS'] , and it sends real HTTP requests to the site's own live address — because half of these mistakes are invisible from the inside.

Every finding names the value that was found, not a rule that was violated: the wrong base URL, the pages still flagged as no_index , the sender address still pointing at example.com.

What it is not 

Not a scanner
It checks the site it is installed in, over its own configured base URL. It cannot be pointed at a foreign domain.
Not a rule engine
The list of checks is fixed in code, and so are the word lists behind a few of them (placeholder text, template titles, common passwords). There is no configuration to tune, and no thresholds to set. Extensions can add their own checks.
Not a cron job
The report is built when the module is opened, in the request of the backend user who opened it. Nothing runs in the background, nothing is stored except the ticks of the manual checklist, and nothing is sent by e-mail.
Not a guarantee
A green report means the things this extension knows how to look at are in order. The twelve manual points exist precisely because the important remainder — a tested backup restore, imported redirects, consent before tracking — cannot be established by a machine.

Why it skips instead of passing 

A check that cannot reach its answer reports as not verifiable, never as passed. No loopback connection to the live URL, an extension that is not installed, a relative base URL that makes a domain comparison meaningless — all of these end in a grey result with the reason.

A false green is worse than no statement at all, because it replaces the very check it pretends to be.

Installation 

Target group: Administrators

Requirements 

  • TYPO3 13.4 LTS or 14
  • PHP 8.2, 8.3 or 8.4
  • An administrator account — the module is not available to editors

Two extensions are suggested, not required:

typo3/cms-seo
Provides the XML sitemap the indexing checks look for. Without it, the sitemap checks report as not verifiable.
typo3/cms-scheduler
Without it, the scheduler check is skipped instead of reporting a missing setup.

Installation with Composer 

composer require webagentur-yahya/golive-check
Copied!

Installation without Composer 

Install the extension from the TYPO3 Extension Repository in the Admin Tools > Extensions module, or download it there and upload the archive.

After installing 

Open Site Management > Go-Live Check. The report is built when the page loads; on a site with a reachable live URL this takes a few seconds because a dozen checks send real HTTP requests.

Nothing else has to be configured. The extension writes nothing to the database schema — the ticks of the manual checklist live in sys_registry under the namespace golive_check.

If every live check reports "not verifiable" 

The live checks are sent from the web server to the site's own base URL. That request has to work from the server, which is not the same as working from your browser:

  • In a local development environment (DDEV, Docker) the container often cannot resolve the production domain, and there is nothing to reach anyway. This is expected — the report is meant for the live installation.
  • A firewall or a load balancer may block a request that leaves and immediately comes back (hairpin NAT).
  • If the site's base has no host at all, the report falls back to the host of the current backend request and marks every comparison against a domain as not verifiable, because the answer would be a statement about the backend URL rather than about the site.

Each skipped check names its reason, so the report says which of these applies.

Using a report 

Target group: Administrators

Opening the module 

Site Management > Go-Live Check. The module answers a question about one website, so it has a site selector in its header; installations with a single site never see a choice to make.

Access is restricted to administrators. Several checks read backend user records, mail configuration and debug switches — values an editor has no business seeing.

When the checks run 

Every time the module is opened. There is no stored result, no cache and no background run: reloading the page is what re-checks the site.

Each URL is fetched only once per report — /robots.txt is read by three checks, and three separate requests could even contradict each other if someone saved in between. The HTTP timeout is five seconds per request, and at most 256 KB of a response body is read, which is why a multi-megabyte sitemap does not slow the report down.

Reading a finding 

The header counts the findings by severity:

Blocker
Do not go live. The site is invisible, broken, or legally exposed.
Warning
Going live is possible, but it costs visibility, security or trust.
Notice
Cosmetics and polish.

The severity belongs to the check, not to the result — a missing privacy page is always a blocker, whether it is currently missing or not. One check deviates on purpose: an expired TLS certificate is a blocker, a certificate expiring in 25 days is a warning. It is the same check with two weights.

Below each finding there are up to three things:

  • The value that was found, unformatted and untranslated. This is the actual content of the configuration — the wrong URL, the affected page titles, SYS/devIPmask = * — and it is deliberately not localised, because the report has to show what really is in there.
  • A link to the place where it is fixed: the record, the page module, the site configuration, or the URL in the frontend. External targets open in a new tab so the report stays where it is.
  • A fix button, but only in the two cases described below.

The one-click fix 

Two findings can be repaired from the report: a robots.txt that locks search engines out, and a missing or wrong Sitemap: line in it. Both times the correct value follows necessarily from the site configuration, so there is nothing to guess.

This requires that robots.txt is served by TYPO3 from a static route in the site configuration. If the file is delivered by the web server from disk, the report says so and offers no button.

Everywhere else there is no button, and that is a decision rather than a gap. A control that quietly writes a guess into a configuration is worse than none, because afterwards nobody looks again.

The manual checklist 

Twelve points no machine can confirm — a restore actually tested, redirects from the previous site imported, nothing tracking before consent, the imprint text read by the customer.

Ticking one records the backend user and the timestamp, and the report shows both. That is the point of the list: it doubles as a handover protocol, and "ticked" without "by whom, when" is worthless once someone complains.

The ticks are stored per site in sys_registry (namespace golive_check, key manual.<site identifier>). They survive a cache flush and are not part of the page tree, so they do not travel with a content export.

Printing and handover 

Print / save as PDF opens the browser's print dialog, and every current browser can write that to a PDF. The printed version carries a document header — site, domain, date, the name of the user who printed it, and the TYPO3 version — that is invisible on screen.

There is no PDF library involved, on purpose: the extension stays free of a heavy dependency that every installation would have to carry along for a page that is printed twice in the lifetime of a project.

"Not verifiable" and what causes it 

Grey results are checks that could not reach their answer. The reason is always named; these are the ones that occur:

  • The site has no absolute base URL, so there is no live address to call.
  • The live URL did not answer (the message contains the transport error).
  • The base URL was only assumed from the current backend request, which makes a comparison against a domain meaningless.
  • There is no robots.txt or no sitemap.xml to look into — that is reported once, by the check that is about the file's existence, and the checks that would read its content skip.
  • The sitemap exists but contains no entries yet.
  • The base URL is not https, so the certificate and the HTTP-to-HTTPS redirect have nothing to say.
  • typo3/cms-scheduler is not installed.
  • The site has no pages, or there is no enabled administrator account.

What the report cannot see 

  • Anything that only happens in the browser: a tracking script fired before consent, a cookie banner that does not block, a layout that breaks on a phone.
  • Whether content is correct — only whether it still contains filler text.
  • Whether a backup can be restored. A backup nobody has restored is a folder of files, and the only way to know is to try it.

These are exactly the twelve points on the manual list.

What is checked 

Target group: Administrators, Integrators

Twenty-three automatic checks in five categories, plus twelve manual points. The categories are ordered by the damage of overlooking them, not alphabetically: whoever goes live invisible to search engines notices weeks later, a missing meta description never.

Each entry below names the severity and where the answer comes from — the local configuration, the database, or a real HTTP request to the site's live address.

Scope 

Checks that look at pages and content only look at the pages of the selected site. Another site root in the tree ends the descent: a subtree with its own config.yaml is a site of its own and is checked there, not here.

Indexing and sitemap 

robots.txt is delivered

Warning · HTTP request to /robots.txt

Fails on any status other than 2xx, and reports the status code that came back. Every other robots check on the page reads this same response.

Search engines are not locked out

Blocker · the robots.txt response

Fails when the file forbids all user agents the whole site (User-agent: * together with Disallow: /). This is the staging file that was taken along on launch.

Can be fixed from the report if robots.txt comes from a static route in the site configuration.

Sitemap line points to this domain

Warning · the robots.txt response

Fails when there is no Sitemap: line at all, and when a line names a host other than the site's own. The expected value is the site's base followed by /sitemap.xml.

The host comparison is skipped when the base URL was only assumed, because the finding would then be a statement about the backend host.

Can be fixed from the report under the same condition as above.

sitemap.xml delivers a sitemap

Blocker · HTTP request to /sitemap.xml

Fails on a status other than 2xx — and also on status 200 when the body is not a sitemap. A 404 handler that serves a page answers 200 with HTML, so the body has to start with <?xml and contain <urlset or <sitemapindex.

Sitemap entries point to this domain

Blocker · the sitemap.xml response

Reads the first 50 <loc> entries and fails when any of them names a different host, listing up to five of them. Entries on a foreign domain make a search engine index the staging site, or discard the sitemap as not belonging to this property.

Pages excluded from search

Notice · database, table pages

Lists the visible pages of this site with no_index = 1 , up to twelve, each with a link to the record. It is a notice rather than a blocker because on a thank-you page the flag is right — only you know which is meant here.

Base address is the live domain

Blocker · site configuration, base

Fails when the base has no host at all, when the host is a development one (localhost, .ddev.site, .local, .test, .localhost, 127.0.0.1), and when the scheme is not https.

Every absolute address TYPO3 builds comes from here — canonical tags, hreflang, og:image, sitemap — and a wrong base makes all of them wrong at once without anything looking broken.

Website title belongs to the customer

Notice · site configuration, websiteTitle

Fails when it is empty or still one of the known template titles (New TYPO3 site, Bootstrap Package, Example and similar).

TYPO3 configuration 

Production context

Blocker · application context

Anything other than Production fails. In Development, TYPO3 answers errors with a full stack trace: file paths, class names and, depending on the error, configuration values.

404 handler works

Blocker · site configuration, errorHandling

Fails when there is no handler for error code 404. If the handler is of type Page and points at t3://page?uid=…, the target page has to exist and must not be hidden — a handler pointing at a deleted or hidden page turns every unknown address into a server error.

Other handler types and external URLs pass without further inspection.

Mail actually leaves the server

Blocker · $GLOBALS['TYPO3_CONF_VARS']['MAIL']

Fails on transport null or debug, on an SMTP server that is a mail catcher (mailpit, mailhog, mailcatcher, localhost, 127.0.0.1, ::1), on an empty defaultMailFromAddress, and on a sender address from a template domain (example.com, example.org, example.ch, domain.tld, localhost).

The contact form says thank you either way, and nobody misses an enquiry they never heard about.

No build-time extensions active

Warning · installed packages

Reports styleguide, adminpanel and bootstrap_package_demo while they are still active. Demo content is the fastest way to build a site, which is exactly why it stays in: its pages are reachable, appear in the sitemap and compete with the real ones.

Scheduler runs

Warning · database, table tx_scheduler_task

Skipped when typo3/cms-scheduler is not installed. Fails when no enabled task exists, when nothing has ever run, and when the last execution is more than 24 hours old — that last case is the cron job nobody set up.

Security 

Debug output is off

Blocker · $GLOBALS['TYPO3_CONF_VARS']

Reports SYS/devIPmask = *, SYS/displayErrors = 1, BE/debug and FE/debug. A * as the developer IP mask means every visitor on earth counts as a developer, and every error hands them the stack trace.

Host header is restricted

Warning · SYS/trustedHostsPattern

Tested by behaviour, not by wording: the pattern is matched against a host it must never accept. That catches .* as well as .*.*, (.*), .+ and ^.*$ — there are arbitrarily many spellings for "everything". The special value SERVER_NAME passes.

An empty or syntactically invalid pattern is not reported: TYPO3 then rejects every host, which is broken but not open, and this check only reports what is open.

No build-time password left

Blocker · database, table be_users

Takes up to ten enabled administrator accounts and tests their password hashes against password, admin, 123456, typo3, test, demo and Password1, using TYPO3's own hash implementation.

The verdict is cached against a fingerprint of the hash, so the hashing runs again only for an account whose password changed. A hash no known implementation can read is not reported — a false alarm on an administrator account costs more trust than it buys.

Document root is set correctly

Blocker · HTTP requests to four paths

Requests /.env, /composer.json, /config/system/settings.php and /config/system/additional.php. A hit counts only when the body looks like the file itself rather than like an HTML page, so a 404 page answering with status 200 does not raise a false alarm.

If the web server points at the project directory instead of public/, then .env and the database credentials are publicly readable.

http redirects to https

Warning · one HTTP request, redirects not followed

Calls the http:// variant of the base URL once. Fails when the answer is not a redirect, and when the Location does not point to https. Skipped when plain http does not answer at all, and when the base URL is not https to begin with — that is reported by the base check instead.

TLS certificate is valid

Blocker, Warning below 30 days · TLS connection to the base host

Reads the peer certificate and reports the subject, the issuer and the expiry date. Expired, an untrusted chain, or fewer than seven days left is a blocker; fewer than 30 days is a warning, because that is when Let's Encrypt renews.

This is the one check whose severity depends on its finding: expired means the site is unreachable, expiring in 25 days means it belongs in a calendar.

Protective headers are sent

Warning · HTTP request to the home page

Looks for X-Content-Type-Options: nosniff, Referrer-Policy, and protection against framing — either X-Frame-Options or frame-ancestors in a Content Security Policy, whichever is present. Strict-Transport-Security is only required on an https site, where it has an effect.

Content 

No filler text on visible pages

Warning · database, table tt_content

Searches header and bodytext of the visible content elements on this site's pages for lorem ipsum, dolor sit amet, Musterstrasse, Musterstraße, Mustermann, Musterfirma, Platzhalter, example.com, Ihr Text hier and your text here. Lists up to ten hits, each linking to the page module.

Filler text survives exactly where nobody looks any more: the third column, the collapsed accordion, the page that exists only for its menu entry.

Pages have a meta description

Notice · database, table pages

Lists visible standard pages of this site that are indexable and have an empty description, up to ten. Page types that never appear in a search result — folders, shortcuts, spacers — are not counted.

The twelve manual points 

These cannot be established by a machine. Ticking one records the backend user and the timestamp; see The manual checklist.

  • Redirects from the previous site imported — blocker
  • Search Console set up and sitemap submitted — warning
  • Staging protected or taken offline — warning
  • DNS and mail records switched over — blocker
  • Backup set up and a restore actually tested — blocker
  • Uptime and certificate monitoring in place — warning
  • Contact form tested end to end — blocker
  • Imprint and privacy text checked by the customer — blocker
  • Nothing tracks before consent — blocker
  • Image licences clarified — warning
  • Tested on phone, tablet and desktop — warning
  • Content proofread and links clicked — warning

Adding your own check 

Target group: Developers

A check is one class. Any extension can contribute checks to the same report, and this extension does not need to know about them.

The class 

Implement CheckInterface . The container tags every implementation automatically, so there is no registration list to edit:

EXT:my_extension/Classes/GoLive/CookieBannerCheck.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\GoLive;

use WebagenturYahya\GoLiveCheck\CheckCategory;
use WebagenturYahya\GoLiveCheck\CheckContext;
use WebagenturYahya\GoLiveCheck\CheckInterface;
use WebagenturYahya\GoLiveCheck\CheckLink;
use WebagenturYahya\GoLiveCheck\CheckResult;
use WebagenturYahya\GoLiveCheck\CheckSeverity;

final class CookieBannerCheck implements CheckInterface
{
    public function getIdentifier(): string
    {
        return 'cookieBanner';
    }

    public function getCategory(): CheckCategory
    {
        return CheckCategory::Legal;
    }

    public function getSeverity(): CheckSeverity
    {
        return CheckSeverity::Blocker;
    }

    public function run(CheckContext $context): CheckResult
    {
        if (!$context->hasLiveUrl()) {
            return CheckResult::skipped('skip.noLiveUrl');
        }

        $response = $context->fetch('/');

        if (!$response->reachable) {
            return CheckResult::skipped('skip.unreachable', [$response->uri, $response->error]);
        }

        if (str_contains($response->body, 'data-consent')) {
            return CheckResult::ok('check.cookieBanner.ok');
        }

        return CheckResult::failed(
            'check.cookieBanner.fail',
            'check.cookieBanner.solution',
            details: [$response->uri],
            links: [new CheckLink('link.siteConfiguration', $context->siteConfigurationUri())],
        );
    }
}
Copied!

The identifier ends up in the translation keys, in the DOM anchor and in the fix request, so it must not change after the check has been shipped.

Registration 

Your extension needs the same _instanceof block in its own Configuration/Services.yaml. Nothing else:

EXT:my_extension/Configuration/Services.yaml
services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false

  _instanceof:
    WebagenturYahya\GoLiveCheck\CheckInterface:
      tags: ['golive.check']

  MyVendor\MyExtension\:
    resource: '../Classes/*'
Copied!

This assumes your extension requires webagentur-yahya/golive-check. If it has to work without it as well, exclude the check classes from the YAML — both from the _instanceof block and from the resource glob that would otherwise autowire them — and register them behind a guard in Configuration/Services.php, which is evaluated while the container is built:

EXT:my_extension/Configuration/Services.php
<?php

declare(strict_types=1);

use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use WebagenturYahya\GoLiveCheck\CheckInterface;
use MyVendor\MyExtension\GoLive\CookieBannerCheck;

return static function (ContainerConfigurator $configurator): void {
    if (!interface_exists(CheckInterface::class)) {
        return;
    }

    $configurator->services()
        ->set(CookieBannerCheck::class)
        ->autowire()
        ->tag('golive.check');
};
Copied!

The three results 

CheckResult::ok()
Passed. May carry details, which are shown as the confirmed value.
CheckResult::failed()
A finding. Takes a message key and a solution key, optional arguments for the message, details, links, and a severity that overrides the check's own for this one finding.
CheckResult::skipped()
The answer could not be reached. Use this whenever the check would otherwise have to guess — an unreachable URL, a missing table, an extension that is not installed. Never return ok() in that situation: a false green replaces the very check it pretends to be.

The $details are the raw values that were found and are never translated. They are what makes a finding actionable, and translating them would stop the report from showing what is really in the configuration.

What the context offers 

CheckContext is handed to run() so that a check does not fetch site, request and URI builder itself. That is also why a check can be unit tested without a TYPO3 bootstrap — build the context by hand and call run() .

getSite() , getSiteIdentifier() , getSiteConfiguration() , getSetting()
The site being checked and its configuration.
hasLiveUrl() , getBaseUrl() , isBaseUrlAssumed()
The resolved live address without a trailing slash. It is assumed when the site has a relative base and the host had to be taken from the current backend request — a finding based on it would not hold, so most checks skip then.
fetch() , fetchAbsolute() , url()
HTTP requests against the site, with a five second timeout, at most 256 KB of body, and 4xx or 5xx returned as a result rather than thrown. Each URL is fetched once per report, so several checks can read the same file.
recordEditUri() , pageLayoutUri() , siteConfigurationUri() , moduleUri()
Targets for CheckLink , so the report can point at the record, the page, or the site configuration instead of only describing the problem.

Texts 

The texts of a check live in the language file of its own extension, at Resources/Private/Language/locallang_golive.xlf. Which extension that is gets answered by the file the class lives in, so a check declares nothing and inherits nothing.

Required for every check:

EXT:my_extension/Resources/Private/Language/locallang_golive.xlf
<trans-unit id="check.cookieBanner.title">
    <source>Consent is asked before tracking</source>
</trans-unit>
<trans-unit id="check.cookieBanner.why">
    <source>One sentence on what it costs to get this wrong.</source>
</trans-unit>
Copied!

Plus every message and solution key the check returns — above: check.cookieBanner.ok, check.cookieBanner.fail and check.cookieBanner.solution. Placeholders are %s, filled from the message arguments in order.

The shared keys are found without you copying them: skip.*, link.*, severity.* and status.* fall back to this extension's language file. A key that resolves nowhere is shown verbatim in the report, which makes a forgotten text visible immediately rather than silently empty.

A translation into another language is a second file next to it, prefixed with the language key — de.locallang_golive.xlf.

A fix button 

Implement FixableCheckInterface as well — but only if the correct value follows necessarily from the configuration:

public function getFixLabelKey(): string
{
    return 'check.cookieBanner.fixLabel';
}

public function applyFix(CheckContext $context): string
{
    // Throw \RuntimeException if it cannot be applied after all.
    return 'check.cookieBanner.fixed';
}
Copied!

The returned key is the success message. applyFix() may assume that run() has just reported a finding; the controller verifies that before calling.

Everything where a human's intention is involved keeps the link to the place instead. A button that quietly writes a guess into a configuration is worse than none, because afterwards nobody looks again.

Changelog 

1.0.1 

No functional change. ext_emconf.php no longer opens with a declare(strict_types=1) statement, because the TER cannot read a file that does — it rejected 1.0.0 with "Details could not be extracted from the provided file".

1.0.0 therefore exists on Packagist only; in the TER, 1.0.1 is the first version. Both contain the same code.

1.0.0 

First public release.

  • Backend module Site Management > Go-Live Check, administrators only, with a site selector.
  • 23 automatic checks in five categories — indexing and sitemap, TYPO3 configuration, security, legal, content. Local configuration, database and real HTTP requests against the site's own live address.
  • 12 manual points, ticked with user and timestamp, stored per site in sys_registry .
  • One-click fix for the two robots.txt findings whose correct value follows from the site configuration.
  • Print view with a document header for the handover.
  • Checks are collected by DI tag, so other extensions can contribute their own — see Adding your own check.
  • English and German.
  • TYPO3 13.4 and 14, PHP 8.2 to 8.4.

Sitemap