enhance-web-seo — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited enhance-web-seo (Agent Skill) and scored it 45/100 (orange). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 1 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
A base64 string of 128+ characters appears in a documentation file. Encoded prompt injection hides the hostile instruction in base64 — invisible to keyword filters — and relies on the agent's ability to decode it at runtime. There is no normal authoring reason to embed a multi-hundred-byte base64 blob in skill docs.
*.sig, SIGNATURES) outside the documentation.Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Google does not rank pages it cannot read, understand, or trust. This skill finds every gap between your app and how search engines see it, then fixes them in order of impact.
Before ANY browser action, read `protocol-browser-anti-stall`.
package.json → Next.js Metadata API, remix-utils/seo, @vueuse/head, etc.
src/app/layout.tsx → existing metadata export (Next.js App Router)
src/app/head.tsx → legacy Head component
public/robots.txt → existing robots config
public/sitemap.xml → existing sitemapAlso check if a sitemap generator is configured (next-sitemap, astro-sitemap, SvelteKit's @sveltejs/adapter-static sitemap, etc.).
For every public-facing route, run the following:
// browser_evaluate after browser_navigate
const seo = await page.evaluate(() => ({
title: document.title,
description: document.querySelector('meta[name="description"]')?.content,
canonical: document.querySelector('link[rel="canonical"]')?.href,
og_title: document.querySelector('meta[property="og:title"]')?.content,
og_description: document.querySelector('meta[property="og:description"]')?.content,
og_image: document.querySelector('meta[property="og:image"]')?.content,
og_url: document.querySelector('meta[property="og:url"]')?.content,
twitter_card: document.querySelector('meta[name="twitter:card"]')?.content,
robots: document.querySelector('meta[name="robots"]')?.content,
jsonld: [...document.querySelectorAll('script[type="application/ld+json"]')]
.map(s => s.textContent),
}));const headings = await page.evaluate(() =>
[...document.querySelectorAll('h1,h2,h3,h4,h5,h6')]
.map(h => ({ tag: h.tagName, text: h.textContent.trim().slice(0, 60) }))
);Rules: exactly one <h1> per page; <h2> never skips to <h4> without <h3>.
const images = await page.evaluate(() =>
[...document.querySelectorAll('img')]
.filter(img => !img.alt || img.alt.trim() === '')
.map(img => img.src.slice(-60))
);// Measure LCP
const lcp = await new Promise(resolve => {
new PerformanceObserver(list =>
resolve(list.getEntries().at(-1)?.startTime)
).observe({ type: 'largest-contentful-paint', buffered: true });
setTimeout(() => resolve(null), 5000);
});
const cls = await page.evaluate(() => {
let clsScore = 0;
new PerformanceObserver(list => {
for (const entry of list.getEntries())
if (!entry.hadRecentInput) clsScore += entry.value;
}).observe({ type: 'layout-shift', buffered: true });
return new Promise(r => setTimeout(() => r(clsScore), 2000));
});Thresholds: LCP < 2.5 s ✅ / 2.5–4 s ⚠ / > 4 s ❌. CLS < 0.1 ✅ / 0.1–0.25 ⚠ / > 0.25 ❌.
browser_navigate to /robots.txt. Check:
Disallow: / unless intentional (private app)Sitemap: https://yourdomain.com/sitemap.xmlNavigate to /sitemap.xml or /sitemap-index.xml. Check:
/)<lastmod> dates are current, not hardcoded to a past dateValidate each JSON-LD block found in Phase 1a:
@context: "https://schema.org" present@type matches the page content (Article, Product, Organization, WebSite, BreadcrumbList, FAQPage, etc.)references/structured-data-types.md)<link rel="canonical"> pointing to its definitive URLCallMcpTool(server: "user-firecrawl", toolName: "firecrawl_search", arguments: {
"query": "Google SEO best practices Core Web Vitals ranking 2026",
"limit": 3,
"sources": [{ "type": "web" }]
})Also check for framework-specific SEO guidance:
CallMcpTool(server: "user-firecrawl", toolName: "firecrawl_search", arguments: {
"query": "<framework> SEO metadata structured data 2026",
"limit": 3,
"sources": [{ "type": "web" }]
})Every public page needs a unique, descriptive title (50–60 chars) and meta description (150–160 chars).
Next.js App Router:
// app/page.tsx
export const metadata: Metadata = {
title: 'Specific Page Title — Brand Name',
description: 'One or two sentences that describe this page for someone scanning search results.',
};Next.js dynamic routes:
export async function generateMetadata({ params }): Promise<Metadata> {
const item = await fetchItem(params.id);
return {
title: `${item.name} — Brand Name`,
description: item.summary,
openGraph: { title: item.name, description: item.summary, images: [item.coverImage] },
};
}Required for social sharing previews. Every page needs at minimum:
openGraph: {
title: 'Page Title',
description: 'Page description',
url: 'https://yourdomain.com/page',
siteName: 'Brand Name',
images: [{ url: '/og-image.png', width: 1200, height: 630 }],
type: 'website', // or 'article' for blog posts
},
twitter: {
card: 'summary_large_image',
title: 'Page Title',
description: 'Page description',
images: ['/og-image.png'],
},robots.txt (static file at public/robots.txt):
User-agent: *
Allow: /
Sitemap: https://yourdomain.com/sitemap.xmlSitemap — use the framework's built-in generator where available:
app/sitemap.ts returning MetadataRoute.Sitemap+server.ts returning XML responsesitemap[.]xml.ts resource routeAdd to the relevant page layout:
// For a website home page
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'Brand Name',
url: 'https://yourdomain.com',
potentialAction: {
'@type': 'SearchAction',
target: 'https://yourdomain.com/search?q={search_term_string}',
'query-input': 'required name=search_term_string',
},
};
// Inject via: <script type="application/ld+json">{JSON.stringify(jsonLd)}</script>See audit-bundle-size for JS bundle → LCP fixes. For CLS: ensure images have explicit width/height, fonts use font-display: swap, ads/embeds have reserved space.
After fixes, re-run Phase 1 checks on each modified page. Confirm:
document.title and meta description match the intentPer page:
- [ ] Unique descriptive title (50–60 chars)
- [ ] Meta description (150–160 chars), not cut off
- [ ] Exactly one <h1>
- [ ] Heading hierarchy correct (no skipped levels)
- [ ] All images have alt text
- [ ] Canonical URL present and correct
- [ ] OG title, description, image
- [ ] Twitter card meta
- [ ] JSON-LD structured data where relevant
Site-wide:
- [ ] robots.txt exists and correct
- [ ] Sitemap exists and up to date
- [ ] No important pages noindexed accidentally
- [ ] HTTPS enforced, no mixed content
- [ ] LCP < 2.5 s
- [ ] CLS < 0.1~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.