This commit is contained in:
oliver
2026-08-28 10:11:11 -03:00
parent f533ff8e36
commit 585951c8ce
1380 changed files with 1540268 additions and 0 deletions
@@ -0,0 +1,88 @@
#requires -version 5.1
<#
kie-image.ps1 - generate hero/OG images via kie.ai (z-image by default).
Loads KIE_API_KEY from .env.local at repo root (or $env:KIE_API_KEY).
Usage:
powershell -File seo-audit-2026-05-27\scripts\kie-image.ps1 -Prompt "..." -OutPath "images/<slug>-hero.png" [-Model z-image] [-Aspect 16:9] [-Resolution 2K]
Models: z-image (default, fast), nano-banana-2 (premium fallback), nano-banana-pro.
Mechanism: POST /api/v1/jobs/createTask with callBackUrl -> ntfy.sh topic -> poll for result.
Note: The old /api/v1/jobs/getTaskDetails polling endpoint no longer exists (API change 2026-05).
Results are now delivered via callBackUrl (webhook). This script uses ntfy.sh as a
free anonymous webhook receiver since no public server is required.
#>
param(
[Parameter(Mandatory)][string]$Prompt,
[Parameter(Mandatory)][string]$OutPath,
[string]$Model = "z-image",
[string]$Aspect = "16:9",
[string]$Resolution = "2K",
[string]$Format = "png",
[int]$TimeoutSec = 300
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
if(-not $env:KIE_API_KEY){
$envFile = Join-Path $repoRoot ".env.local"
if(Test-Path $envFile){
$line = Get-Content $envFile -ErrorAction SilentlyContinue | Where-Object { $_ -match '^KIE_API_KEY=' } | Select-Object -First 1
if($line){ $env:KIE_API_KEY = ($line -replace '^KIE_API_KEY=','').Trim('"',"'") }
}
}
if(-not $env:KIE_API_KEY){ throw "KIE_API_KEY not set (env var or .env.local)" }
# Generate unique ntfy.sh topic for callback
$ntfyTopic = "kie-img-$(Get-Date -Format 'yyyyMMddHHmmss')-$(Get-Random -Maximum 9999)"
$callBackUrl = "https://ntfy.sh/$ntfyTopic"
$headers = @{ Authorization = "Bearer $($env:KIE_API_KEY)"; 'Content-Type' = 'application/json' }
$body = @{
model = $Model
input = @{ prompt = $Prompt; aspect_ratio = $Aspect; resolution = $Resolution; output_format = $Format }
callBackUrl = $callBackUrl
} | ConvertTo-Json -Depth 6
Write-Host "[kie-image] submit model=$Model aspect=$Aspect res=$Resolution"
Write-Host "[kie-image] callback=$callBackUrl"
$create = Invoke-RestMethod -Uri 'https://api.kie.ai/api/v1/jobs/createTask' -Method POST -Headers $headers -Body $body
if($create.code -ne 200){ throw "createTask failed: $($create | ConvertTo-Json -Depth 5 -Compress)" }
$taskId = $create.data.taskId
Write-Host "[kie-image] taskId=$taskId polling via ntfy.sh..."
$deadline = (Get-Date).AddSeconds($TimeoutSec)
$imageUrl = $null
while((Get-Date) -lt $deadline){
Start-Sleep -Seconds 8
try {
$r = Invoke-WebRequest -Uri "https://ntfy.sh/$ntfyTopic/json?poll=1" -UseBasicParsing -ErrorAction Stop
$text = [System.Text.Encoding]::UTF8.GetString($r.RawContentStream.ToArray())
# Parse NDJSON lines for kie.ai callback
$lines = $text -split '[\r\n]+' | Where-Object { $_ -match '"event":"message"' }
foreach ($line in $lines) {
try {
$msg = $line | ConvertFrom-Json
$payload = $msg.message | ConvertFrom-Json
if ($payload.code -eq 200 -and $payload.data.taskId -eq $taskId) {
$state = $payload.data.state
Write-Host "[kie-image] state=$state"
if ($state -eq 'success') {
$rj = $payload.data.resultJson | ConvertFrom-Json
$imageUrl = $rj.resultUrls[0]
break
}
if ($state -eq 'failed' -or $state -eq 'error') {
throw "task failed: $($payload | ConvertTo-Json -Depth 5 -Compress)"
}
}
} catch { }
}
} catch { }
if ($imageUrl) { break }
}
if(-not $imageUrl){ throw "timed out after $TimeoutSec s waiting for task $taskId" }
$outFull = Join-Path $repoRoot $OutPath
$outDir = Split-Path -Parent $outFull
if($outDir -and -not (Test-Path $outDir)){ New-Item -ItemType Directory -Force -Path $outDir | Out-Null }
Invoke-WebRequest -Uri $imageUrl -OutFile $outFull -UseBasicParsing
Write-Host "[kie-image] saved: $outFull"
Write-Output $outFull
@@ -0,0 +1,16 @@
$slugs = 'bestes-odoo-hosting-2026|cheap-odoo-hosting-2026|kostenloses-odoo-hosting-2026|odoo-hosting-deutschland-2026|odoo-hosting-oesterreich-2026|odoo-hosting-schweiz-2026|best-odoo-hosting-2026|free-odoo-hosting-2026|odoo-hosting-germany-2026|odoo-hosting-uk-2026|odoo-sh-alternatives-2026|odoo-vps-hosting-2026'
$repo = Split-Path -Parent $PSScriptRoot
$repo = Split-Path -Parent $repo
foreach ($f in @('sitemap-en.xml','sitemap-fr.xml','sitemap-pt.xml','sitemap-ar.xml')) {
$path = Join-Path $repo $f
if (-not (Test-Path $path)) { continue }
$c = [System.IO.File]::ReadAllText($path)
$re = '(?s)\s*<url>\s*<loc>https://www\.odoo-expertos\.com/(en|fr|pt|ar)/odoo-hosting/(' + $slugs + ')/</loc>.*?</url>'
$new = [regex]::Replace($c, $re, '')
if ($new -ne $c) {
[System.IO.File]::WriteAllText($path, $new)
Write-Host "Cleaned $f"
} else {
Write-Host "No change $f"
}
}
@@ -0,0 +1,17 @@
# W10: extract second inline <style> block from index.html to external CSS file
$repo = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$idx = Join-Path $repo 'index.html'
$c = [System.IO.File]::ReadAllText($idx)
$matches = [regex]::Matches($c, '(?s)<style[^>]*>(.*?)</style>')
if ($matches.Count -lt 2) { Write-Host "Not enough style blocks"; exit 0 }
$second = $matches[1]
$cssContent = $second.Groups[1].Value
$cssPath = Join-Path $repo 'css\homepage-extra.min.css'
[System.IO.File]::WriteAllText($cssPath, $cssContent)
# Replace the second <style>...</style> with link tag
$replacement = '<link rel="stylesheet" href="/css/homepage-extra.min.css">'
$newC = $c.Substring(0, $second.Index) + $replacement + $c.Substring($second.Index + $second.Length)
[System.IO.File]::WriteAllText($idx, $newC)
$newMatches = [regex]::Matches($newC, '(?s)<style[^>]*>(.*?)</style>')
$total = ($newMatches | ForEach-Object { $_.Groups[1].Value.Length } | Measure-Object -Sum).Sum
Write-Host "Extracted $($cssContent.Length) bytes to css/homepage-extra.min.css; inline CSS now: $total bytes"
+20
View File
@@ -0,0 +1,20 @@
$repo = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$excludePattern = '\\(\.git|node_modules|seo-audit-2026-05-27|website|directory)\\'
$files = Get-ChildItem $repo -Recurse -Filter index.html -File -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -notmatch $excludePattern }
$matched = @()
foreach ($file in $files) {
$c = [System.IO.File]::ReadAllText($file.FullName)
if (-not $c) { continue }
if ($c -match '"(?:url|item|@id|logo)":\s*"https://odoo-expertos\.com') {
$matched += $file.FullName
}
}
Write-Host "Matched count: $($matched.Count)"
$matched | Select-Object -First 3 | ForEach-Object { Write-Host $_ }
if ($matched.Count -gt 0) {
$c = [System.IO.File]::ReadAllText($matched[0])
$m = [regex]::Match($c, '"(?:url|item|@id|logo)":\s*"https://odoo-expertos\.com[^"]*"')
Write-Host "---SAMPLE MATCH---"
Write-Host $m.Value
}
@@ -0,0 +1,19 @@
$repo = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$excludePattern = '\\(\.git|node_modules|seo-audit-2026-05-27|website|directory)\\'
$files = Get-ChildItem $repo -Recurse -Filter index.html -File -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -notmatch $excludePattern }
$matched = @()
foreach ($file in $files) {
$c = [System.IO.File]::ReadAllText($file.FullName)
if (-not $c) { continue }
# CASE-SENSITIVE - same as REVIEW.ps1
if ([regex]::IsMatch($c, '"(?:url|item|@id|logo)":\s*"https://odoo-expertos\.com')) {
$matched += $file.FullName
}
}
Write-Host "Case-sensitive matched: $($matched.Count)"
if ($matched.Count -gt 0) {
$c = [System.IO.File]::ReadAllText($matched[0])
$m = [regex]::Match($c, '"(?:url|item|@id|logo)":\s*"https://odoo-expertos\.com[^"\r\n]*')
Write-Host "Sample: $($m.Value)"
}
@@ -0,0 +1,31 @@
# W3: fix dead domain + non-www JSON-LD URLs across all index.html files (case-insensitive)
$repo = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$excludePattern = '\\(\.git|node_modules|seo-audit-2026-05-27|website|directory)\\'
$files = Get-ChildItem $repo -Recurse -Filter index.html -File -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -notmatch $excludePattern }
$tpl = Join-Path $repo 'templates\pillar-page.html'
if (Test-Path $tpl) { $files += (Get-Item $tpl) }
$eeat = Join-Path $repo 'components\eeat.js'
if (Test-Path $eeat) { $files += (Get-Item $eeat) }
$opts = [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
$changed = 0
foreach ($file in $files) {
$c = [System.IO.File]::ReadAllText($file.FullName)
if (-not $c) { continue }
$orig = $c
# Normalize ALL occurrences of any case-variant of odoo-expertos.com to www.odoo-expertos.com
# Step 1: dead domain (no hyphen) -> www.odoo-expertos.com
$c = [regex]::Replace($c, 'https?://(?:www\.)?odooexpertos\.com', 'https://www.odoo-expertos.com', $opts)
# Step 2: protocol+(www.)?odoo-expertos.com -> https://www.odoo-expertos.com
$c = [regex]::Replace($c, 'https?://(?:www\.)?odoo-expertos\.com', 'https://www.odoo-expertos.com', $opts)
# Bare hostname (no protocol)
$c = [regex]::Replace($c, '(?<![./@\w-])(?:www\.)?odooexpertos\.com', 'www.odoo-expertos.com', $opts)
# Cleanup any accidental www.www.
$c = $c -replace 'www\.www\.odoo-expertos\.com', 'www.odoo-expertos.com'
if ($c -ne $orig) {
[System.IO.File]::WriteAllText($file.FullName, $c)
$changed++
}
}
Write-Host "W3 codemod: changed $changed of $($files.Count) files"
@@ -0,0 +1,59 @@
# W4: fix mislocated hreflang="es" hrefs + add hreflang block to 3 ES category pages
$repo = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$excludePattern = '\\(\.git|node_modules|seo-audit-2026-05-27|website|directory)\\'
$files = Get-ChildItem $repo -Recurse -Filter index.html -File -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -notmatch $excludePattern }
$opts = [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
$changed = 0
foreach ($file in $files) {
$c = [System.IO.File]::ReadAllText($file.FullName)
if (-not $c) { continue }
$orig = $c
# Strip /en/ /de/ /fr/ /pt/ /ar/ segment in any hreflang="es" or hreflang="x-default" href value
$c = [regex]::Replace($c, '(hreflang="(?:es|x-default)"[^>]*href="https://www\.odoo-expertos\.com)/(?:de|fr|pt|ar|en)/', '$1/', $opts)
# Also handle the inverse order: href=... hreflang=es
$c = [regex]::Replace($c, '(href="https://www\.odoo-expertos\.com)/(?:de|fr|pt|ar|en)/([^"]*"\s+hreflang="(?:es|x-default)")', '$1/$2', $opts)
if ($c -ne $orig) {
[System.IO.File]::WriteAllText($file.FullName, $c)
$changed++
}
}
Write-Host "W4 hreflang-es fix: changed $changed of $($files.Count) files"
# Inject hreflang block into ES category pages
$cats = @{
'odoo' = 'odoo/'
'odoo-hosting' = 'odoo-hosting/'
'odoo-ia' = 'odoo-ia/'
}
$injected = 0
foreach ($cat in $cats.Keys) {
$catPath = Join-Path $repo "$cat\index.html"
if (-not (Test-Path $catPath)) { continue }
$c = [System.IO.File]::ReadAllText($catPath)
if ($c -match 'hreflang=') { continue }
$sub = $cats[$cat]
$block = @"
<!-- Hreflang alternates -->
<link rel="alternate" hreflang="es" href="https://www.odoo-expertos.com/$sub">
<link rel="alternate" hreflang="en" href="https://www.odoo-expertos.com/en/$sub">
<link rel="alternate" hreflang="de" href="https://www.odoo-expertos.com/de/$sub">
<link rel="alternate" hreflang="fr" href="https://www.odoo-expertos.com/fr/$sub">
<link rel="alternate" hreflang="pt" href="https://www.odoo-expertos.com/pt/$sub">
<link rel="alternate" hreflang="ar" href="https://www.odoo-expertos.com/ar/$sub">
<link rel="alternate" hreflang="x-default" href="https://www.odoo-expertos.com/$sub">
"@
# Insert after canonical link
$new = [regex]::Replace($c, '(<link\s+rel="canonical"[^>]*>)', "`$1`r`n$block", $opts)
if ($new -eq $c) {
# fallback: inject before </head>
$new = $c -replace '</head>', "$block`r`n</head>"
}
if ($new -ne $c) {
[System.IO.File]::WriteAllText($catPath, $new)
$injected++
Write-Host "Injected hreflang into $cat/index.html"
}
}
Write-Host "W4 cat injection: $injected"
@@ -0,0 +1,103 @@
# W5: shorten titles >65 chars; guarantee unique meta descriptions across the corpus
$repo = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$excludePattern = '\\(\.git|node_modules|seo-audit-2026-05-27|website|directory)\\'
$files = Get-ChildItem $repo -Recurse -Filter index.html -File -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -notmatch $excludePattern }
function ToTitle([string]$slug) {
$s = $slug -replace '-', ' '
$s = (Get-Culture).TextInfo.ToTitleCase($s.ToLower())
$s = $s -replace '\bOdoo\b','Odoo'
$s = $s -replace '\bAi\b','AI'
$s = $s -replace '\bErp\b','ERP'
$s = $s -replace '\bIa\b','IA'
$s = $s -replace '\bAws\b','AWS'
$s = $s -replace '\bGcp\b','GCP'
$s = $s -replace '\bVps\b','VPS'
$s = $s -replace '\bSh\b','SH'
$s = $s -replace '\bUk\b','UK'
$s = $s -replace '\bUsa\b','USA'
$s = $s -replace '\bSaas\b','SaaS'
return $s
}
function PathLang([string]$fullPath, [string]$repo) {
$rel = $fullPath.Substring($repo.Length).TrimStart('\','/')
$parts = $rel -split '[\\/]'
if ($parts.Count -ge 1 -and ($parts[0] -in @('en','de','fr','pt','ar'))) {
return $parts[0]
}
return 'es'
}
# First pass — collect existing meta description counts (informational only)
$metaCounts = @{}
foreach ($file in $files) {
$c = [System.IO.File]::ReadAllText($file.FullName)
if (-not $c) { continue }
$m = [regex]::Match($c, '<meta\s+name="description"\s+content="([^"]*)"')
if ($m.Success) {
$d = $m.Groups[1].Value.Trim()
if ($d) {
if ($metaCounts.ContainsKey($d)) { $metaCounts[$d]++ } else { $metaCounts[$d] = 1 }
}
}
}
Write-Host "Pre-existing duplicate meta strings: $((${metaCounts}.GetEnumerator() | Where-Object {$_.Value -gt 1}).Count)"
$titleChanged = 0
$metaChanged = 0
foreach ($file in $files) {
$c = [System.IO.File]::ReadAllText($file.FullName)
if (-not $c) { continue }
$orig = $c
$dir = Split-Path -Parent $file.FullName
$slug = Split-Path -Leaf $dir
$pretty = ToTitle $slug
if ($pretty.Length -gt 42) { $pretty = $pretty.Substring(0,42).TrimEnd() }
$lang = (PathLang $file.FullName $repo).ToUpper()
# Title fix (shorten if >65 chars)
$tm = [regex]::Match($c, '<title>(.*?)</title>', 'Singleline')
if ($tm.Success) {
$curTitle = $tm.Groups[1].Value.Trim()
if ($curTitle.Length -gt 65) {
$base = $pretty
$newTitle = "$base | Odoo Expertos"
if ($newTitle.Length -gt 60) {
$maxBase = 60 - " | Odoo Expertos".Length
if ($maxBase -lt 5) { $maxBase = 5 }
$newTitle = $base.Substring(0, [Math]::Min($base.Length, $maxBase)).TrimEnd() + " | Odoo Expertos"
}
$c = $c.Substring(0, $tm.Index) + "<title>$newTitle</title>" + $c.Substring($tm.Index + $tm.Length)
$titleChanged++
}
}
# Always make meta description unique by appending " - [Slug] | [LANG]"
$dm = [regex]::Match($c, '<meta\s+name="description"\s+content="([^"]*)"')
if ($dm.Success) {
$curDesc = $dm.Groups[1].Value.Trim()
if ($curDesc) {
$suffix = " - $pretty | $lang"
# Only add suffix if it isn't already there (idempotent)
if (-not $curDesc.EndsWith($suffix)) {
$base = $curDesc
if (($base.Length + $suffix.Length) -gt 165) {
$base = $base.Substring(0, [Math]::Max(1, 165 - $suffix.Length)).TrimEnd()
}
$newDesc = $base + $suffix
$newDesc = $newDesc -replace '"', '&quot;'
$repl = '<meta name="description" content="' + $newDesc + '"'
$c = $c.Substring(0, $dm.Index) + $repl + $c.Substring($dm.Index + $dm.Length)
$metaChanged++
}
}
}
if ($c -ne $orig) {
[System.IO.File]::WriteAllText($file.FullName, $c)
}
}
Write-Host "W5: titles shortened=$titleChanged; meta descriptions modified=$metaChanged"
@@ -0,0 +1,76 @@
# W6: bake static nav into every page; remove createHeader/createFooter/createEEAT JS calls
$repo = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$excludePattern = '\\(\.git|node_modules|seo-audit-2026-05-27|website|directory)\\'
$files = Get-ChildItem $repo -Recurse -Filter index.html -File -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -notmatch $excludePattern }
function PathLang([string]$fullPath, [string]$repo) {
$rel = $fullPath.Substring($repo.Length).TrimStart('\','/')
$parts = $rel -split '[\\/]'
if ($parts.Count -ge 1 -and ($parts[0] -in @('en','de','fr','pt','ar'))) {
return $parts[0]
}
return 'es'
}
$labels = @{
'es' = @{ home='Inicio'; odoo='Odoo'; ia='Odoo IA'; hosting='Hosting Odoo'; sitemap='Mapa del Sitio' }
'en' = @{ home='Home'; odoo='Odoo'; ia='Odoo AI'; hosting='Odoo Hosting'; sitemap='Sitemap' }
'de' = @{ home='Start'; odoo='Odoo'; ia='Odoo KI'; hosting='Odoo Hosting'; sitemap='Sitemap' }
'fr' = @{ home='Accueil';odoo='Odoo'; ia='Odoo IA'; hosting='Hebergement Odoo'; sitemap='Plan du Site' }
'pt' = @{ home='Inicio'; odoo='Odoo'; ia='Odoo IA'; hosting='Hospedagem Odoo'; sitemap='Mapa do Site' }
'ar' = @{ home='Inicio'; odoo='Odoo'; ia='Odoo AI'; hosting='Odoo Hosting'; sitemap='Sitemap' }
}
function StaticHeader([string]$lang) {
$L = $labels[$lang]
if (-not $L) { $L = $labels['es'] }
if ($lang -eq 'es') { $prefix = '' } else { $prefix = "/$lang" }
$sm = if ($lang -eq 'es') { '/mapa-del-sitio/' } else { "$prefix/mapa-del-sitio/" }
return @"
<header class="site-header"><nav class="navbar"><div class="container"><div class="nav-brand"><a href="$prefix/" class="logo"><span class="logo-text">Odoo Expertos</span></a></div><button class="nav-toggle" aria-label="Toggle navigation"><span></span><span></span><span></span></button><div class="nav-menu"><ul class="nav-list"><li><a href="$prefix/">$($L.home)</a></li><li><a href="$prefix/odoo/">$($L.odoo)</a></li><li><a href="$prefix/odoo-ia/">$($L.ia)</a></li><li><a href="$prefix/odoo-hosting/">$($L.hosting)</a></li><li><a href="$sm">$($L.sitemap)</a></li></ul></div></div></nav></header>
"@
}
function StaticFooter([string]$lang) {
$L = $labels[$lang]
if (-not $L) { $L = $labels['es'] }
if ($lang -eq 'es') { $prefix = '' } else { $prefix = "/$lang" }
$sm = if ($lang -eq 'es') { '/mapa-del-sitio/' } else { "$prefix/mapa-del-sitio/" }
return @"
<footer class="site-footer"><div class="container"><div class="footer-cols"><div><strong>Odoo Expertos</strong></div><nav class="footer-nav"><a href="$prefix/">$($L.home)</a><a href="$prefix/odoo/">$($L.odoo)</a><a href="$prefix/odoo-ia/">$($L.ia)</a><a href="$prefix/odoo-hosting/">$($L.hosting)</a><a href="$sm">$($L.sitemap)</a></nav></div><div class="footer-bottom"><small>&copy; 2026 Odoo Expertos</small></div></div></footer>
"@
}
$opts = [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
$changed = 0
foreach ($file in $files) {
$c = [System.IO.File]::ReadAllText($file.FullName)
if (-not $c) { continue }
$orig = $c
$lang = PathLang $file.FullName $repo
$hdr = StaticHeader $lang
$ftr = StaticFooter $lang
# Inject static header inside <div id="header">...</div>
$c = [regex]::Replace($c, '<div\s+id="header"[^>]*>\s*(?:<!--[^>]*-->)?\s*</div>', '<div id="header">' + $hdr + '</div>', $opts)
$c = [regex]::Replace($c, '<div\s+id="header"[^>]*>.*?</div>', '<div id="header">' + $hdr + '</div>', $opts -bor [System.Text.RegularExpressions.RegexOptions]::Singleline)
# Same for footer
$c = [regex]::Replace($c, '<div\s+id="footer"[^>]*>\s*(?:<!--[^>]*-->)?\s*</div>', '<div id="footer">' + $ftr + '</div>', $opts)
$c = [regex]::Replace($c, '<div\s+id="footer"[^>]*>.*?</div>', '<div id="footer">' + $ftr + '</div>', $opts -bor [System.Text.RegularExpressions.RegexOptions]::Singleline)
# Remove the createHeader/createFooter/createEEAT JS lines (entire lines)
$c = [regex]::Replace($c, '(?m)^\s*document\.getElementById\([''"]header[''"]\)\.innerHTML\s*=\s*createHeader\(\)\s*;\s*\r?\n', '')
$c = [regex]::Replace($c, '(?m)^\s*document\.getElementById\([''"]footer[''"]\)\.innerHTML\s*=\s*createFooter\(\)\s*;\s*\r?\n', '')
$c = [regex]::Replace($c, '(?ms)\s*if\s*\(\s*typeof\s+createEEAT\s*===\s*[''"]function[''"]\s*\)\s*\{[^}]*createEEAT\(\)[^}]*\}\s*\r?\n', "`r`n")
# Catch any stray createHeader() / createFooter() / createEEAT() still in file
$c = [regex]::Replace($c, 'createHeader\(\)', '/*W6-static*/null')
$c = [regex]::Replace($c, 'createFooter\(\)', '/*W6-static*/null')
$c = [regex]::Replace($c, 'createEEAT\(\)', '/*W6-static*/null')
if ($c -ne $orig) {
[System.IO.File]::WriteAllText($file.FullName, $c)
$changed++
}
}
Write-Host "W6: files modified=$changed"
@@ -0,0 +1,39 @@
# W7: add <meta name="robots" content="noindex,follow"> to duplicate-canonical pages
$repo = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$dupFile = Join-Path $repo 'seo-audit-2026-05-27\gsc\duplicate-canonical.txt'
if (-not (Test-Path $dupFile)) { Write-Host "Missing $dupFile"; exit 1 }
$urls = (Get-Content $dupFile) | Where-Object { $_ -match '^https://' }
Write-Host "Total duplicate-canonical URLs: $($urls.Count)"
function UrlToLocal([string]$url, [string]$repo) {
$p = $url -replace '^https?://www\.odoo-expertos\.com', ''
$p = $p.Split('#')[0].Split('?')[0]
if ($p -eq '' -or $p -eq '/') { return (Join-Path $repo 'index.html') }
if ($p -match '\.html?$') { return (Join-Path $repo ($p.TrimStart('/').Replace('/','\'))) }
$p = $p.TrimEnd('/')
return (Join-Path $repo ($p.TrimStart('/').Replace('/','\') + '\index.html'))
}
$opts = [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
$noindexed = 0
$missing = 0
foreach ($u in $urls) {
$lf = UrlToLocal $u $repo
if (-not (Test-Path $lf)) { $missing++; continue }
$c = [System.IO.File]::ReadAllText($lf)
if (-not $c) { continue }
# Check if noindex already present
if ([regex]::IsMatch($c, 'name="robots"[^>]*noindex', $opts)) { continue }
# If a <meta name="robots" ...> exists, replace its content; else inject new meta
$existing = [regex]::Match($c, '<meta\s+name="robots"\s+content="[^"]*"\s*/?>', $opts)
if ($existing.Success) {
$repl = '<meta name="robots" content="noindex,follow">'
$c = $c.Substring(0, $existing.Index) + $repl + $c.Substring($existing.Index + $existing.Length)
} else {
# Inject before </head>
$c = [regex]::Replace($c, '</head>', '<meta name="robots" content="noindex,follow">' + "`r`n</head>", $opts)
}
[System.IO.File]::WriteAllText($lf, $c)
$noindexed++
}
Write-Host "Pages noindexed: $noindexed; missing local: $missing"