feat(wordpress): EMCP Tools workflow skills + widget catalogs + pending wordpress skills
- emcp-page-building, emcp-content-ops, emcp-site-audit: EMCP Tools MCP workflows verified live (atomic/legacy interplay, apply-template overwrite risk, create-theme-template goes live immediately, false-positive malware pattern in scan-security, change ledger + rollback) - elementor-pro-widgets: 30+5 native Elementor Pro widgets (curated catalog) - elementskit-widgets / powerpack-widgets: 42 + 97 third-party widgets, widgetType extracted from plugin source (not guessed by convention) - plugin.json bumped 1.2.0 -> 1.3.0, keywords + description updated - commits pending wordpress skills already present as untracked files (emcp-tools, wordfence, wp-activity-log, seguranca-descomplicar, webp-express, wp-fastest-cache, wp-font-perf, wp-meteor, wp-activity-log, redis-object-cache, app-for-cloudflare) and pending edits (rank-math, wp-cli, wp-content-seo-gate)
This commit is contained in:
@@ -0,0 +1,437 @@
|
||||
---
|
||||
name: wp-font-perf
|
||||
description: Cortar o peso de webfonts e de imagens em background CSS em sites WordPress/Elementor da frota Descomplicar, com medição real de Performance. Cobre o subsetting de fontes de ícones (IconSax, Font Awesome, eicons, elementskit) com `pyftsubset` e rede de segurança por `unicode-range` complementar, a substituição de folhas do Elementor via `style_loader_tag` (e porque um `wp_dequeue_style` não chega), WebP em `background-image` que o WebP Express não cobre, preload de fonte variável para travar CLS, e as duas armadilhas que partem o site ou invalidam medições (purge do Cloudflare avariado, apagar `wpfc-minified`). Usar quando "performance wordpress", "lighthouse", "LCP alto", "CLS", "fontes pesadas", "subset de fontes", "icon fonts", "webfonts", "peso da página", "optimizar site", "core web vitals", "PageSpeed".
|
||||
---
|
||||
|
||||
# /wp-font-perf — Peso de webfonts e imagens CSS, com medição real
|
||||
|
||||
Reduzir peso real de páginas WordPress/Elementor na frota Descomplicar. Validado
|
||||
em piloto em `emanuelalmeida.pt` a 17-08-2026: fontes de ícones **1256KB → 10KB**,
|
||||
peso total **3125KB → 1258KB**, Performance mobile **60 → 68**, desktop **79 → 88**,
|
||||
LCP mobile **14,2s → 6,1s**, CLS **0 estável**.
|
||||
|
||||
**Fonte:** `04-Stack/02.04-Sistemas/71.Seguranca/BUNDLE-Excelencia-WP.md` §2.2.1
|
||||
e §2.2.2 (relato completo, incluindo o incidente).
|
||||
|
||||
---
|
||||
|
||||
## 0. Antes de tudo: medir Performance a sério
|
||||
|
||||
**O `lighthouse_audit` do MCP chrome-devtools NÃO mede a categoria Performance.**
|
||||
Exclui-a por desenho. Um relatório "100/100/100/100" desse MCP pode conviver com
|
||||
um LCP de 14 segundos — aconteceu, duas sessões seguidas.
|
||||
|
||||
```bash
|
||||
# Instalar/usar Lighthouse local (não depende da quota da API do PSI)
|
||||
npx -y lighthouse@12 "https://SITE/" --only-categories=performance \
|
||||
--form-factor=mobile --screenEmulation.mobile --throttling-method=simulate \
|
||||
--output=json --output-path=/tmp/lh.json --chrome-flags="--headless=new --no-sandbox" --quiet
|
||||
|
||||
# desktop
|
||||
npx -y lighthouse@12 "https://SITE/" --only-categories=performance \
|
||||
--form-factor=desktop --screenEmulation.disabled --throttling-method=simulate \
|
||||
--throttling.rttMs=40 --throttling.throughputKbps=10240 --throttling.cpuSlowdownMultiplier=1 \
|
||||
--output=json --output-path=/tmp/lhd.json --chrome-flags="--headless=new --no-sandbox" --quiet
|
||||
```
|
||||
|
||||
**Regra de evidência: mediana de 5 corridas, nunca uma.** A variância entre
|
||||
corridas isoladas da mesma configuração chega a **25 pontos** (medido: 69 e 62
|
||||
para o mesmo estado). Uma corrida não é evidência de nada.
|
||||
|
||||
Peso por tipo (é aqui que se decide onde atacar):
|
||||
|
||||
```bash
|
||||
python3 -c "
|
||||
import json, collections
|
||||
lr=json.load(open('/tmp/lh.json'))
|
||||
r=(lr['audits']['network-requests'].get('details') or {}).get('items',[])
|
||||
by=collections.Counter(); n=collections.Counter()
|
||||
for x in r:
|
||||
t=x.get('resourceType') or 'other'; by[t]+=x.get('transferSize',0); n[t]+=1
|
||||
print('total %.0fKB / %d pedidos' % (sum(by.values())/1024, len(r)))
|
||||
for t,v in by.most_common(): print(f' {t:12} {n[t]:>3} {v/1024:>7.0f}KB')
|
||||
print('perf', round(lr['categories']['performance']['score']*100))
|
||||
for k in ('largest-contentful-paint','cumulative-layout-shift','first-contentful-paint'):
|
||||
print(' ', k, lr['audits'][k].get('displayValue'))
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Fontes de ícones: o suspeito número um
|
||||
|
||||
Sites Elementor com icon packs acumulam megabytes para desenhar dezenas de
|
||||
glifos. No piloto: **1,2MB para 27 glifos**.
|
||||
|
||||
|Fonte|Original|Subset|Glifos usados / existentes|
|
||||
|---|---|---|---|
|
||||
|IconSax|355.632 B **TTF**|3.172 B|12 / 890|
|
||||
|IconSax-Fino|355.692 B **TTF**|536 B|1 / 906|
|
||||
|eicons|108.000 B|636 B|3 / 515|
|
||||
|fa-brands-400|81.612 B|1.028 B|4|
|
||||
|fa-solid-900|78.196 B|780 B|8|
|
||||
|elementskit (ekiticons)|252KB|**0** (suprimido)|0 / 12|
|
||||
|
||||
Icon packs custom carregados pelo Elementor (`uploads/elementor/custom-icons/`)
|
||||
vêm frequentemente em **TTF/WOFF sem woff2** — formato sem compressão web.
|
||||
|
||||
### 1.1 Levantar os glifos em uso em TODO o site
|
||||
|
||||
Nunca só na homepage. Varrer todos os permalinks publicados:
|
||||
|
||||
```bash
|
||||
ssh server "sudo -u USER /usr/local/bin/wp post list --post_type=page,post \
|
||||
--post_status=publish --field=url --path=WEBROOT 2>/dev/null"
|
||||
```
|
||||
|
||||
Depois `curl` a cada URL e recolher as classes (`icon-sax-*`, `fa-*`, `eicon-*`).
|
||||
Mapear classe → codepoint nos CSS (`content:"\eXXX"`).
|
||||
|
||||
**Método mais fiável, no browser** (apanha o que vem de CSS de widgets e não só
|
||||
de classes no HTML) — itera os pseudo-elementos e lê o `content` computado:
|
||||
|
||||
```js
|
||||
const want = /Font Awesome|eicons|IconSax|elementskit/i;
|
||||
const hits = {};
|
||||
for (const el of document.querySelectorAll('*'))
|
||||
for (const ps of ['::before','::after']) {
|
||||
const cs = getComputedStyle(el, ps);
|
||||
if (!want.test(cs.fontFamily || '')) continue;
|
||||
for (const ch of (cs.content||'').replace(/^["']|["']$/g,'')) {
|
||||
const cp = ch.codePointAt(0);
|
||||
if (cp >= 0xE000) (hits[cs.fontFamily.replace(/["']/g,'').split(',')[0].trim()+'|'+cs.fontWeight] ??= new Set()).add(cp.toString(16));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 Gerar os subsets
|
||||
|
||||
Requer `fonttools` + `brotli` (`pyftsubset` em `~/.local/bin`):
|
||||
|
||||
```bash
|
||||
pyftsubset FONTE.ttf --unicodes=U+E915,U+E969,... --flavor=woff2 \
|
||||
--layout-features= --no-hinting --desubroutinize --output-file=NOME.subset.woff2
|
||||
|
||||
# fonte completa convertida para woff2 (fallback mais leve que o TTF original)
|
||||
pyftsubset FONTE.ttf --unicodes=U+E000-F8FF --flavor=woff2 \
|
||||
--layout-features= --no-hinting --output-file=NOME.full.woff2
|
||||
```
|
||||
|
||||
### 1.3 Rede de segurança obrigatória: `unicode-range` complementar
|
||||
|
||||
**Nunca servir só o subset.** Declarar duas `@font-face` para a mesma família:
|
||||
o subset com o `unicode-range` dos glifos em uso, e a fonte completa com o
|
||||
`unicode-range` **complementar**. Assim um ícone novo adicionado no Elementor
|
||||
não fica "tofu" — o browser vai buscar a completa só para esse codepoint — e
|
||||
enquanto isso não acontecer não se paga um byte por ela.
|
||||
|
||||
```python
|
||||
def complement(used, lo=0xE000, hi=0xF8FF):
|
||||
u=sorted(int(c,16) for c in used); out=[]; s=lo
|
||||
for c in u:
|
||||
if c>s: out.append((s,c-1))
|
||||
s=c+1
|
||||
if s<=hi: out.append((s,hi))
|
||||
return out # formatar como U+XXXX ou U+XXXX-YYYY
|
||||
```
|
||||
|
||||
**Confirmar que a completa não é descarregada:**
|
||||
|
||||
```js
|
||||
[...document.fonts].filter(f=>/IconSax|Font Awesome|eicons/.test(f.family))
|
||||
.map(f=>f.family+'|'+f.weight+'|'+f.status)
|
||||
// esperado: subsets 'loaded', completas 'unloaded'
|
||||
```
|
||||
|
||||
### 1.4 Substituir a folha, não acrescentar `@font-face` depois
|
||||
|
||||
**Um `wp_dequeue_style` não alcança estas folhas** e um `@font-face` acrescentado
|
||||
na fase de enqueue perde: o Elementor enfileira as folhas de icon library
|
||||
**durante o render do body** (há widgets com esses ícones no `_elementor_data`),
|
||||
pelo que `wp_style_is()` devolve `false`, o CSS nosso sai **antes** do original no
|
||||
documento, perde o desempate "última declaração vence" e **o TTF continua a ser
|
||||
descarregado** (medido, não presumido).
|
||||
|
||||
Padrão correcto:
|
||||
1. Copiar a folha do Elementor, **manter todas as regras de classe** e trocar só
|
||||
os `@font-face`. Verificar que não há `url()` fora dos `@font-face` antes de
|
||||
cortar (backgrounds embutidos).
|
||||
2. Enfileirar a cópia com `wp_enqueue_style`.
|
||||
3. Suprimir a original na **impressão**, imune à ordem de enqueue:
|
||||
|
||||
```php
|
||||
add_filter( 'style_loader_tag', function ( $tag, $handle ) {
|
||||
if ( ! is_admin() && in_array( $handle, array(
|
||||
'elementor-icons-IconSax', 'elementor-icons-IconSax-Fino',
|
||||
'elementor-icons-ekiticons', 'elementor-icons-fa-brands',
|
||||
'elementor-icons-fa-solid', 'elementor-icons',
|
||||
), true ) ) { return ''; }
|
||||
return $tag;
|
||||
}, 10, 2 );
|
||||
```
|
||||
|
||||
Handles reais confirmam-se no HTML: `grep -oE "id='[^']*(icon|ekit)[^']*-css'"`.
|
||||
|
||||
### 1.5 Icon packs 100% mortos
|
||||
|
||||
`elementor-icons-ekiticons` traz `elementskit.woff` (252KB) e um CSS que o
|
||||
Lighthouse dá como 100% não usado: define 12 ícones do **painel** do ElementsKit
|
||||
(tiktok, x-twitter, scroll-reveal, smart-post-list…). Antes de suprimir,
|
||||
confirmar que as classes presentes no HTML não têm regra:
|
||||
|
||||
```bash
|
||||
ssh server "grep -rlE '(icon-checked|icon-menu-11):before' WEBROOT/wp-content/plugins/ WEBROOT/wp-content/themes/"
|
||||
# sem resultado = classes órfãs de versão antiga, não desenham glifo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. CLS: preload da fonte de TEXTO, não das de ícones
|
||||
|
||||
Ao reduzir o preload, o heading do hero volta a re-layoutar quando a fonte de
|
||||
texto chega (no piloto: **CLS 0,213**, com 0,2055 atribuídos a um único `h4`).
|
||||
|
||||
**Não preloadar tudo** — foi o erro que criou o problema de LCP. Preloadar **um**
|
||||
ficheiro: as Google Fonts modernas são **variáveis**, e um só subset latin cobre
|
||||
os pesos 200-900.
|
||||
|
||||
```bash
|
||||
# descobrir qual ficheiro serve latin + todos os pesos
|
||||
ssh server "grep -oE '@font-face\{[^}]*\}' WEBROOT/wp-content/uploads/elementor/google-fonts/css/nunito.css" \
|
||||
# procurar o que tem unicode-range U+0000-00FF (latin) e font-style: normal
|
||||
```
|
||||
|
||||
39KB resolveram o CLS que 1410KB resolviam antes. Confirmar com **5 corridas**,
|
||||
não uma.
|
||||
|
||||
---
|
||||
|
||||
## 3. WebP em `background-image`: ponto cego do WebP Express
|
||||
|
||||
**O WebP Express reescreve as tags `<img>` do HTML mas não os `background-image`
|
||||
do CSS.** E em CWP o **nginx serve os estáticos directamente, sem passar pelo
|
||||
`.htaccess` do plugin**, pelo que também não há negociação por `Accept`.
|
||||
|
||||
Resultado no piloto: `foto-1.png` servia **458KB** (36% da página, e era o recurso
|
||||
LCP) apesar de existir `webp-express/webp-images/uploads/foto-1.png.webp` com
|
||||
181KB, gerado pelo plugin e nunca servido.
|
||||
|
||||
Detectar:
|
||||
|
||||
```bash
|
||||
# candidatos: imagens grandes referenciadas em CSS do Elementor
|
||||
ssh server "grep -rl 'NOME.png' WEBROOT/wp-content/uploads/elementor/css/"
|
||||
ssh server "find WEBROOT/wp-content/webp-express -name 'NOME*' -printf '%s %p\n'"
|
||||
```
|
||||
|
||||
Corrigir com `image-set()` (o PNG fica como segundo candidato para browsers sem
|
||||
suporte). O `!important` é necessário porque o `post-N.css` do Elementor é
|
||||
enfileirado depois:
|
||||
|
||||
```php
|
||||
$css = sprintf(
|
||||
'%s{background-image:image-set(url("%s") type("image/webp"),url("%s") type("image/png")) !important}',
|
||||
$selector, $webp_url, $png_url
|
||||
);
|
||||
wp_register_style( 'desc-lcp-webp', false, array(), '1.0.0' );
|
||||
wp_enqueue_style( 'desc-lcp-webp' );
|
||||
wp_add_inline_style( 'desc-lcp-webp', $css );
|
||||
```
|
||||
|
||||
Preloadar também a imagem LCP (`background-image` só é descoberta depois de o CSS
|
||||
ser descarregado e parseado — Load Delay medido de 8,2s = 87% do LCP):
|
||||
|
||||
```php
|
||||
printf('<link rel="preload" as="image" fetchpriority="high" href="%s">', esc_url($url));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Outros cortes verificados
|
||||
|
||||
- **`dashicons` no frontend:** 750ms de render-blocking + 35KB para **zero**
|
||||
glifos `dashicons-*` no HTML. Remover só para visitantes não autenticados (a
|
||||
admin bar precisa dele). `grep -c 'dashicons-'` na página confirma o uso real.
|
||||
- **Google Fonts remoto duplicado:** `astra-google-fonts` pede o Nunito a
|
||||
`fonts.googleapis.com` (render-blocking ~770ms + ligação a terceiros) quando o
|
||||
Elementor já o serve local. Confirmar que o local cobre os pesos antes de
|
||||
desenfileirar (`grep -c 'font-weight' nunito.css`).
|
||||
|
||||
---
|
||||
|
||||
## 5. 🔴 Armadilhas que partem o site ou invalidam medições
|
||||
|
||||
### 5.1 O purge do Cloudflare reporta sucesso sem purgar
|
||||
|
||||
`wp app-for-cf purge-cache` devolve sempre `Success: Cloudflare cache purged.` e
|
||||
**não purga**. Medido em `emanuelalmeida.pt` (17-08-2026):
|
||||
|
||||
```
|
||||
antes: cf-cache-status: HIT age: 27
|
||||
comando: Success: Cloudflare cache purged.
|
||||
depois: cf-cache-status: HIT age: 34 -> 37 -> 40 # nunca invalidou
|
||||
```
|
||||
|
||||
**É bug do comando, não das credenciais.** O token do plugin verifica `active` e
|
||||
faz `purge_everything` pela API sem problema (`success: true`, `MISS` na visita
|
||||
seguinte). Com `cfPageCachingSeconds=21600`, as alterações ficam **até 6 horas
|
||||
invisíveis** — medições no URL limpo medem HTML pré-alteração.
|
||||
|
||||
**Purge que funciona** (ferramenta da frota; lê as credenciais do próprio plugin,
|
||||
resolve a zona real e confirma o efeito, com exit≠0 se continuar `HIT`):
|
||||
|
||||
```bash
|
||||
04-Stack/02.04-Sistemas/71.Seguranca/tools/cf-purge.py \
|
||||
--account ealmeida --path /home/ealmeida/emanuelalmeida.pt \
|
||||
--url https://emanuelalmeida.pt/
|
||||
```
|
||||
|
||||
**⚠️ Armadilha ao diagnosticar: há vários plugins Cloudflare com credenciais no
|
||||
mesmo site.** Registei duas vezes a causa errada ("token inválido", "falta
|
||||
permissão `Zone → Cache Purge`") por testar as credenciais do plugin errado.
|
||||
|
||||
|Opção|Plugin|
|
||||
|---|---|
|
||||
|`app_for_cf.cloudflareAuth.token` (`cfut_…`, 53 car.) + `app_for_cf.cfZoneId`|**App for Cloudflare** — as que contam|
|
||||
|`swcfpc_config.cf_apitoken` / `cf_zoneid`|Super Page Cache — pode ter token inválido e zona errada|
|
||||
|`wp_turbosafe_cloudflare_api_token`|wp-turbosafe — cifrado, inutilizável|
|
||||
|
||||
**Confirmar sempre no código de onde vem a credencial** antes de a declarar
|
||||
inválida — `app-for-cf` usa `$this->option('cloudflareAuth.token')` em
|
||||
`src/DigitalPoint/Cloudflare/Api/Cloudflare.php`. E distinguir os erros da API:
|
||||
`Invalid API Token` (1000) = o valor não corresponde a nenhum token activo;
|
||||
falta de permissão tem outro código. Se houver `zone_id` divergentes,
|
||||
`GET /zones?name=<dominio>` desempata.
|
||||
|
||||
### 5.2 CLS: o CSS do Elementor sai **depois** de `</head>`
|
||||
|
||||
Em sites Elementor, o CSS de cada widget é enfileirado **durante o render do
|
||||
body** → o browser pinta o header sem estilos e volta a fazer layout quando as
|
||||
folhas chegam. Em `emanuelalmeida.pt` eram **78KB em 7 folhas** e dava
|
||||
**CLS 0,45–1,27 em 8 de 10 cargas** (limite bom = 0,1; pico medido 1,319).
|
||||
|
||||
**Diagnóstico em dois comandos.** Primeiro, contar folhas fora do head:
|
||||
|
||||
```bash
|
||||
python3 - <<'EOF'
|
||||
import re,subprocess,time
|
||||
h=subprocess.run(['curl','-s',f'https://SITE/?x={int(time.time())}'],capture_output=True,text=True).stdout
|
||||
he=h.find('</head>')
|
||||
L=[(m.start(),m.group(0)) for m in re.finditer(r"<link[^>]*rel=['\"]stylesheet['\"][^>]*>",h)]
|
||||
print("HEAD:",len([1 for p,_ in L if p<he]),"| BODY:",len([1 for p,_ in L if p>he]))
|
||||
for p,l in L:
|
||||
if p>he: print(" ", (re.search(r"id='([^']+)'",l) or re.search(r'id="([^"]+)"',l)).group(1))
|
||||
EOF
|
||||
```
|
||||
|
||||
Depois, medir o CLS **a sério** — não confiar no Lighthouse com
|
||||
`--throttling-method=simulate`, que devolvia 0 quase sempre neste caso. Usar
|
||||
`PerformanceObserver`, tab nova por corrida, 6 corridas:
|
||||
|
||||
```js
|
||||
await page.evaluateOnNewDocument(()=>{window.__s=[];
|
||||
new PerformanceObserver(l=>{for(const e of l.getEntries()){
|
||||
if(!e.hadRecentInput) window.__s.push({v:e.value,t:Math.round(e.startTime),
|
||||
src:(e.sources||[]).map(s=>s.node&&s.node.tagName+'.'+String(s.node.className||'').split(' ')[0])});
|
||||
}}).observe({type:'layout-shift',buffered:true});});
|
||||
await tab.goto(URL,{waitUntil:'networkidle2'});
|
||||
await new Promise(r=>setTimeout(r,3000));
|
||||
```
|
||||
|
||||
⚠️ **Dois erros de método a evitar** (cometi ambos): registar `evaluateOnNewDocument`
|
||||
várias vezes na mesma tab **soma observers** e inflaciona o total (vi 5,15 onde o
|
||||
shift real era 0,79); e um `first-paint` ~100ms antes do shift, com
|
||||
`domInteractive` no mesmo instante, é a assinatura de CSS no body — não de fontes.
|
||||
|
||||
**Correcção:** mu-plugin `elementor-css-head.php` (cópia canónica em
|
||||
`04-Stack/02.04-Sistemas/71.Seguranca/mu-plugins/`). Aprende que handles saíram
|
||||
depois do head (`wp_styles()->done` no fecho do `wp_head` vs. no `wp_footer`),
|
||||
guarda num transient por URL (12h) e antecipa-os para o head na visita seguinte.
|
||||
Resultado: **CLS 0 em 6/6**, 0 nas medianas Lighthouse mobile/desktop, 0 na
|
||||
auditoria MCP.
|
||||
|
||||
**❌ Não perder tempo com:** desligar `elementor_experiment-e_optimized_css_loading`
|
||||
(no Elementor 4.x já não desliga o carregamento condicional — as folhas continuam
|
||||
no body); e descartar o WP Meteor por A/B antes de acusar o defer de JS (`?wp-meteor-nooptimize=true`
|
||||
deu o mesmo CLS, logo não era ele).
|
||||
|
||||
**🔴 Aquecimento obrigatório de DUAS passagens depois do deploy** — no primeiro
|
||||
pedido de cada URL o mu-plugin ainda não aprendeu nada, e é esse HTML que fica
|
||||
em cache:
|
||||
|
||||
```bash
|
||||
wp post list --post_type=page,post --post_status=publish --field=url --path=$SITE > /tmp/urls.txt
|
||||
ssh server "rm -rf $SITE/wp-content/cache/all/*"
|
||||
while read -r u; do curl -s -o /dev/null "$u?p1=$(date +%s%N)"; done < /tmp/urls.txt # aprende
|
||||
ssh server "rm -rf $SITE/wp-content/cache/all/*"
|
||||
while read -r u; do curl -s -o /dev/null "$u?p2=$(date +%s%N)"; done < /tmp/urls.txt # HTML bom
|
||||
while read -r u; do curl -s -o /dev/null -w "%{http_code} $u\n" "$u"; done < /tmp/urls.txt
|
||||
```
|
||||
|
||||
**Custo aceite:** as folhas passam a bloquear no head em vez do body — mesmos
|
||||
bytes, mais cedo. Performance mediana mobile 68→64, desktop 88→86 (dentro da
|
||||
variância). CLS é Core Web Vital e estava 4× a 12× acima do limite: a troca paga-se.
|
||||
|
||||
### 5.3 NUNCA apagar `wpfc-minified`
|
||||
|
||||
Com `MinifyCss` do WP Fastest Cache ligado, os `<link>` apontam a
|
||||
`wp-content/cache/wpfc-minified/<hash>/hcd9a.css`. Apagar essa pasta enquanto
|
||||
existe HTML cacheado (no WPFC **ou no Cloudflare**) a referenciar os hashes
|
||||
antigos → **CSS a 404 em massa, site sem estilos**. Foi assim que o piloto
|
||||
apareceu desfigurado ao utilizador.
|
||||
|
||||
```bash
|
||||
# ERRADO
|
||||
rm -rf WEBROOT/wp-content/cache/wpfc-minified/*
|
||||
# CERTO — forçar HTML novo sem tocar nos ficheiros minificados
|
||||
rm -rf WEBROOT/wp-content/cache/all/*
|
||||
```
|
||||
|
||||
### 5.4 Não filtrar a saída do `wp_head` com `ob_start`
|
||||
|
||||
Tentar remover uma tag já emitida com `ob_start`/`ob_get_clean` no `wp_head`
|
||||
partiu o `<head>`. Se a origem de um preload órfão não se encontra, **deixar
|
||||
documentado em vez de mexer em buffers** — no piloto eram 38KB, não valia.
|
||||
|
||||
---
|
||||
|
||||
## 6. Verificação de fecho (obrigatória)
|
||||
|
||||
1. **Todas as folhas do HTML devolvem conteúdo** — foi isto que apanhou o
|
||||
incidente sem ambiguidade:
|
||||
|
||||
```bash
|
||||
python3 - <<'EOF'
|
||||
import subprocess, re
|
||||
h=subprocess.run(['curl','-s','https://SITE/'],capture_output=True,text=True).stdout
|
||||
links=re.findall(r"<link rel='stylesheet' id='([^']+)' href='([^']+)'", h)
|
||||
bad=[]
|
||||
for hid,href in links:
|
||||
u = href if href.startswith('http') else 'https:'+href
|
||||
o=subprocess.run(['curl','-s','-o','/dev/null','-w','%{http_code} %{size_download}',u],capture_output=True,text=True).stdout.split()
|
||||
if o[0]!='200' or int(o[1])==0: bad.append((hid,o))
|
||||
print('folhas:',len(links),'| com problema:',len(bad),bad)
|
||||
EOF
|
||||
```
|
||||
|
||||
2. **Zero glifos em falta, em várias páginas** — medir a largura de cada
|
||||
codepoint em uso contra a largura de um codepoint ausente ("tofu"):
|
||||
|
||||
```js
|
||||
const c=document.createElement('canvas').getContext('2d');
|
||||
const w=(fam,wt,cp)=>{c.font=wt+' 32px "'+fam+'"';return c.measureText(String.fromCharCode(parseInt(cp,16))).width;};
|
||||
const tofu=w('IconSax','400','2764');
|
||||
// falha se w(fam,wt,cp) === tofu
|
||||
```
|
||||
|
||||
3. **Screenshot** do hero e da secção de ícones.
|
||||
4. **CLS em 5 corridas**, não uma.
|
||||
|
||||
---
|
||||
|
||||
## 7. Estado do rollout
|
||||
|
||||
Aplicado só em **`emanuelalmeida.pt`** (piloto, 17-08-2026). Pendente #19 do
|
||||
`BUNDLE-Excelencia-WP.md`: replicar nos outros 6 sites. O padrão do WebP em
|
||||
background CSS (§3) e o `dashicons` (§4) são os mais prováveis de existirem em
|
||||
todos.
|
||||
Reference in New Issue
Block a user