feat(domains): add per-domain noindex support

Flagged domains are served with X-Robots-Tag: noindex, nofollow via
Traefik and Caddy routing labels, so an auto-generated technical domain
can be excluded from indexing while the production domain on the same
resource stays indexable.
This commit is contained in:
Alberto Rizzi committed 2026-07-12 13:59:09 +02:00
1 parent 3da9942bf3
commit 4afe131905
20 files changed
+617 -23

No files matched your search

+62 -4
View File
@@ -5,6 +5,7 @@ use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Models\ServiceApplication;
use App\Support\ValidationPatterns;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Spatie\Url\Url;
@@ -387,7 +388,21 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
return $payload;
}
function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null, string $redirect_direction = 'both', ?string $predefinedPort = null, bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null)
/**
* @param Collection<int, string>|null $noindex_domains
*/
function isNoindexDomain(string $domain, ?Collection $noindex_domains): bool
{
if (blank($noindex_domains)) {
return false;
}
return $noindex_domains
->map(fn (string $noindex_domain) => ValidationPatterns::normalizeApplicationDomainUrl($noindex_domain))
->contains(ValidationPatterns::normalizeApplicationDomainUrl($domain));
}
function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null, string $redirect_direction = 'both', ?string $predefinedPort = null, bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null)
{
$labels = collect([]);
if ($serviceLabels) {
@@ -419,7 +434,14 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
$port = $predefinedPort;
}
$labels->push("caddy_{$loop}={$schema}://{$host}");
$labels->push("caddy_{$loop}.header=-Server");
if (isNoindexDomain($domain, $noindex_domains)) {
// Caddy's header directive takes either inline arguments or a block,
// never both, so -Server has to move into the block alongside it.
$labels->push("caddy_{$loop}.header.0_-Server=");
$labels->push("caddy_{$loop}.header.1_X-Robots-Tag=\"noindex, nofollow\"");
} else {
$labels->push("caddy_{$loop}.header=-Server");
}
$labels->push("caddy_{$loop}.try_files={path} /index.html /index.php");
if ($port) {
@@ -445,7 +467,7 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
return $labels->sort();
}
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both', bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null)
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both', bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null)
{
$labels = collect([]);
$labels->push('traefik.enable=true');
@@ -518,6 +540,12 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels->push("caddy_{$loop}.handle_path.{$loop}_redir-ghost-{$uuid}.rewrite.replacement=/$1");
}
$noindex_name = "{$loop}-{$uuid}-noindex";
$is_noindex = isNoindexDomain($domain, $noindex_domains);
if ($is_noindex) {
$labels->push("traefik.http.middlewares.{$noindex_name}.headers.customresponseheaders.X-Robots-Tag=noindex, nofollow");
}
$to_www_name = "{$loop}-{$uuid}-to-www";
$to_non_www_name = "{$loop}-{$uuid}-to-non-www";
$redirect_to_non_www = [
@@ -562,6 +590,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if ($is_http_basic_auth_enabled) {
$middlewares->push($http_basic_auth_label);
}
if ($is_noindex) {
$middlewares->push($noindex_name);
}
$middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
$middlewares->push($middleware_name);
});
@@ -588,6 +619,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if ($is_http_basic_auth_enabled) {
$middlewares->push($http_basic_auth_label);
}
if ($is_noindex) {
$middlewares->push($noindex_name);
}
$middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
$middlewares->push($middleware_name);
});
@@ -606,8 +640,15 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels->push("traefik.http.services.{$http_label}.loadbalancer.server.port=$port");
$labels->push("traefik.http.routers.{$http_label}.service={$http_label}");
}
$middlewares = collect([]);
if ($is_noindex) {
$middlewares->push($noindex_name);
}
if ($is_force_https_enabled) {
$labels->push("traefik.http.routers.{$http_label}.middlewares=redirect-to-https");
$middlewares->push('redirect-to-https');
}
if ($middlewares->isNotEmpty()) {
$labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares->join(',')}");
}
} else {
// Set labels for http
@@ -640,6 +681,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if ($is_http_basic_auth_enabled) {
$middlewares->push($http_basic_auth_label);
}
if ($is_noindex) {
$middlewares->push($noindex_name);
}
$middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
$middlewares->push($middleware_name);
});
@@ -666,6 +710,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if ($is_http_basic_auth_enabled) {
$middlewares->push($http_basic_auth_label);
}
if ($is_noindex) {
$middlewares->push($noindex_name);
}
$middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
$middlewares->push($middleware_name);
});
@@ -698,6 +745,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
if ($pull_request_id === 0) {
if ($application->fqdn) {
$domains = str(data_get($application, 'fqdn'))->explode(',');
$noindexDomains = $application->noindexDomains();
$shouldGenerateLabelsExactly = $application->destination->server->settings->generate_exact_labels;
if ($shouldGenerateLabelsExactly) {
switch ($application->destination->server->proxyType()) {
@@ -713,6 +761,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled,
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
noindex_domains: $noindexDomains,
));
break;
case ProxyTypes::CADDY->value:
@@ -728,6 +777,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled,
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
noindex_domains: $noindexDomains,
));
break;
}
@@ -743,6 +793,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled,
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
noindex_domains: $noindexDomains,
));
$labels = $labels->merge(fqdnLabelsForCaddy(
network: $application->destination->network,
@@ -756,6 +807,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled,
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
noindex_domains: $noindexDomains,
));
}
}
@@ -765,6 +817,8 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
} else {
$domains = collect([]);
}
// Previews are ephemeral: every domain is noindex.
$noindexDomains = $domains;
$shouldGenerateLabelsExactly = $application->destination->server->settings->generate_exact_labels;
if ($shouldGenerateLabelsExactly) {
switch ($application->destination->server->proxyType()) {
@@ -779,6 +833,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled,
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
noindex_domains: $noindexDomains,
));
break;
case ProxyTypes::CADDY->value:
@@ -793,6 +848,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled,
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
noindex_domains: $noindexDomains,
));
break;
}
@@ -807,6 +863,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled,
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
noindex_domains: $noindexDomains,
));
$labels = $labels->merge(fqdnLabelsForCaddy(
network: $application->destination->network,
@@ -819,6 +876,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
is_http_basic_auth_enabled: $application->is_http_basic_auth_enabled,
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
noindex_domains: $noindexDomains,
));
}
}