📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
|
||||
const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const PUBLIC_DIR = path.join(ROOT_DIR, 'public');
|
||||
const SKILLS_JSON = path.join(PUBLIC_DIR, 'skills.json');
|
||||
const SEO_LANDING_PAGES_JSON = path.join(ROOT_DIR, 'src', 'data', 'seoLandingPages.json');
|
||||
const OUTPUT_PATH = path.join(PUBLIC_DIR, 'sitemap.xml');
|
||||
const BASE_PATH =
|
||||
(process.env.VITE_BASE_PATH || '/').trim().replace(/\/+$/, '');
|
||||
@@ -81,6 +82,24 @@ export function selectTopSkillEntries(skills, topCount = TOP_SKILL_COUNT) {
|
||||
return dedupedEntries;
|
||||
}
|
||||
|
||||
export function getSeoLandingPaths() {
|
||||
if (!fs.existsSync(SEO_LANDING_PAGES_JSON)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(SEO_LANDING_PAGES_JSON, 'utf-8');
|
||||
const pages = JSON.parse(raw);
|
||||
|
||||
if (!Array.isArray(pages)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return pages
|
||||
.map((page) => String(page?.slug || '').trim())
|
||||
.filter(Boolean)
|
||||
.map((slug) => `/topics/${encodeURIComponent(slug)}`);
|
||||
}
|
||||
|
||||
export function generateSitemapXml({ baseUrl, paths, lastmod = DEFAULT_LASTMOD }) {
|
||||
const normalizedBase = String(baseUrl).replace(/\/$/, '');
|
||||
const uniquePaths = [...new Set(paths)];
|
||||
@@ -106,9 +125,10 @@ function readSkillsCatalog() {
|
||||
|
||||
export function buildSitemap(skills, topCount = TOP_SKILL_COUNT, baseUrl = SITE_URL) {
|
||||
const topSkillPaths = selectTopSkillEntries(skills, topCount);
|
||||
const landingPaths = getSeoLandingPaths();
|
||||
return generateSitemapXml({
|
||||
baseUrl,
|
||||
paths: ['/', '/plugins', ...topSkillPaths],
|
||||
paths: ['/', '/plugins', ...landingPaths, ...topSkillPaths],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { buildSitemap, selectTopSkillEntries } from './generate-sitemap.js';
|
||||
import { buildSitemap, getSeoLandingPaths, selectTopSkillEntries } from './generate-sitemap.js';
|
||||
|
||||
describe('sitemap generation script helpers', () => {
|
||||
it('builds top skill entries sorted by stars/date/name without duplicates', () => {
|
||||
@@ -23,6 +23,7 @@ describe('sitemap generation script helpers', () => {
|
||||
const xml = buildSitemap(catalog, 1, 'https://example.com');
|
||||
|
||||
expect(xml).toContain('https://example.com/</loc>');
|
||||
expect(xml).toContain('https://example.com/topics/antigravity-cli-skills</loc>');
|
||||
expect(xml).toContain('https://example.com/skill/gamma</loc>');
|
||||
expect(xml).not.toContain('/skill/delta');
|
||||
});
|
||||
@@ -36,7 +37,7 @@ describe('sitemap generation script helpers', () => {
|
||||
expect(xml).toContain('/safe%26id</loc>');
|
||||
});
|
||||
|
||||
it('returns only homepage when top skill limit is zero', () => {
|
||||
it('returns homepage and topic routes when top skill limit is zero', () => {
|
||||
const catalog = [
|
||||
{ id: 'gamma', stars: 2 },
|
||||
{ id: 'delta', stars: 1 },
|
||||
@@ -45,6 +46,18 @@ describe('sitemap generation script helpers', () => {
|
||||
const xml = buildSitemap(catalog, 0, 'https://example.com');
|
||||
|
||||
expect(xml).toContain('https://example.com/</loc>');
|
||||
expect(xml).toContain('https://example.com/topics/github-ai-skills-repository</loc>');
|
||||
expect(xml).not.toContain('https://example.com/skill');
|
||||
});
|
||||
|
||||
it('loads stable SEO landing paths from shared catalog data', () => {
|
||||
expect(getSeoLandingPaths()).toEqual(
|
||||
expect.arrayContaining([
|
||||
'/topics/antigravity-cli-skills',
|
||||
'/topics/github-ai-skills-repository',
|
||||
'/topics/antigravity-plugins',
|
||||
'/topics/skills-para-antigravity',
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ const DIST_DIR = path.join(ROOT_DIR, 'dist');
|
||||
const PUBLIC_DIR = path.join(ROOT_DIR, 'public');
|
||||
const TEMPLATE_PATH = path.join(DIST_DIR, 'index.html');
|
||||
const SKILLS_PATH = path.join(PUBLIC_DIR, 'skills.json');
|
||||
const SEO_LANDING_PAGES_PATH = path.join(ROOT_DIR, 'src', 'data', 'seoLandingPages.json');
|
||||
|
||||
const HOME_CATALOG_COUNT_FALLBACK = 1689;
|
||||
const PRERENDER_SOCIAL_IMAGE = 'social-card.svg';
|
||||
@@ -18,13 +19,18 @@ const FAQ_ITEMS = [
|
||||
{
|
||||
question: 'What is Antigravity Awesome Skills?',
|
||||
answer:
|
||||
'Antigravity Awesome Skills is an installable GitHub library of 1,689+ reusable SKILL.md playbooks for AI coding assistants. It supports Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and related hosts through direct skill installs, specialized plugins, bundles, workflows, and a searchable catalog.',
|
||||
'Antigravity Awesome Skills is an installable GitHub library of 1,700+ reusable SKILL.md playbooks for AI coding assistants. It supports Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and related hosts through direct skill installs, specialized plugins, bundles, workflows, and a searchable catalog.',
|
||||
},
|
||||
{
|
||||
question: 'How do I install Antigravity Awesome Skills?',
|
||||
answer:
|
||||
'Install the library with npx antigravity-awesome-skills. Use tool-specific flags such as --codex, --cursor, --gemini, --claude, or --antigravity when you want the installer to target a specific skills directory already used by your assistant runtime.',
|
||||
},
|
||||
{
|
||||
question: 'Is Antigravity Awesome Skills a GitHub repository?',
|
||||
answer:
|
||||
'Yes. The GitHub repository at https://github.com/sickn33/antigravity-awesome-skills is the canonical source for the skill library, installer, specialized plugins, bundles, workflows, and documentation. The hosted catalog is the searchable browsing surface for that repository.',
|
||||
},
|
||||
{
|
||||
question: 'What are AAS specialized plugins?',
|
||||
answer:
|
||||
@@ -177,11 +183,161 @@ function safeText(value) {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function normalizeMatchText(value) {
|
||||
return safeText(value)
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function getLandingPageMatchTerms(page) {
|
||||
return [
|
||||
page.slug,
|
||||
page.eyebrow,
|
||||
page.h1,
|
||||
page.summary,
|
||||
page.primaryIntent,
|
||||
...(Array.isArray(page.keywords) ? page.keywords : []),
|
||||
...(Array.isArray(page.relatedTerms) ? page.relatedTerms : []),
|
||||
];
|
||||
}
|
||||
|
||||
function scoreLandingPageForSkill(page, skill) {
|
||||
const haystack = normalizeMatchText([
|
||||
skill.id,
|
||||
skill.name,
|
||||
skill.description,
|
||||
skill.category,
|
||||
skill.source,
|
||||
skill.path,
|
||||
].filter(Boolean).join(' '));
|
||||
const category = normalizeMatchText(skill.category);
|
||||
const relatedCategories = Array.isArray(page.relatedCategories)
|
||||
? page.relatedCategories.map(normalizeMatchText)
|
||||
: [];
|
||||
let score = relatedCategories.includes(category) ? 12 : 0;
|
||||
|
||||
for (const term of getLandingPageMatchTerms(page)) {
|
||||
const normalizedTerm = normalizeMatchText(term);
|
||||
|
||||
if (!normalizedTerm || normalizedTerm.length < 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (haystack.includes(normalizedTerm)) {
|
||||
score += Math.min(12, 3 + normalizedTerm.split(' ').length * 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
const matchedTokens = normalizedTerm
|
||||
.split(' ')
|
||||
.filter((token) => token.length >= 4 && haystack.includes(token));
|
||||
|
||||
score += Math.min(6, matchedTokens.length);
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
function getRelatedLandingPagesForSkill(landingPages, skill, limit = 3) {
|
||||
const maxItems = Math.max(0, limit);
|
||||
|
||||
if (maxItems === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const scoredPages = landingPages
|
||||
.map((page, index) => ({
|
||||
page,
|
||||
index,
|
||||
score: scoreLandingPageForSkill(page, skill),
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (a.score !== b.score) {
|
||||
return b.score - a.score;
|
||||
}
|
||||
|
||||
return a.index - b.index;
|
||||
});
|
||||
const selected = scoredPages.filter(({ score }) => score > 0).map(({ page }) => page);
|
||||
|
||||
for (const { page } of scoredPages) {
|
||||
if (selected.length >= maxItems) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!selected.includes(page)) {
|
||||
selected.push(page);
|
||||
}
|
||||
}
|
||||
|
||||
return selected.slice(0, maxItems);
|
||||
}
|
||||
|
||||
function buildStaticLinkList(links) {
|
||||
return links
|
||||
.map((link) => `<li><a href="${escapeHtml(link.href)}">${escapeHtml(link.label)}</a></li>`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
function buildPrerenderFallback({ heading, description, links }) {
|
||||
const items = buildStaticLinkList(links);
|
||||
|
||||
return [
|
||||
'<main data-prerender-fallback="true">',
|
||||
`<h1>${escapeHtml(heading)}</h1>`,
|
||||
`<p>${escapeHtml(description)}</p>`,
|
||||
items ? `<nav aria-label="Related topic guides"><ul>${items}</ul></nav>` : '',
|
||||
'</main>',
|
||||
].join('');
|
||||
}
|
||||
|
||||
function buildTopicFallback({ page, landingPages, siteBaseUrl }) {
|
||||
const relatedLinks = landingPages
|
||||
.filter((landing) => landing.slug && landing.slug !== page.slug)
|
||||
.slice(0, 3)
|
||||
.map((landing) => ({
|
||||
href: routeToUrl(`/topics/${encodeURIComponent(landing.slug)}`, siteBaseUrl),
|
||||
label: landing.h1,
|
||||
}));
|
||||
|
||||
return buildPrerenderFallback({
|
||||
heading: page.h1,
|
||||
description: page.summary,
|
||||
links: relatedLinks,
|
||||
});
|
||||
}
|
||||
|
||||
function buildSkillFallback({ skill, landingPages, siteBaseUrl }) {
|
||||
const relatedLinks = getRelatedLandingPagesForSkill(landingPages, skill).map((page) => ({
|
||||
href: routeToUrl(`/topics/${encodeURIComponent(page.slug)}`, siteBaseUrl),
|
||||
label: page.h1,
|
||||
}));
|
||||
|
||||
return buildPrerenderFallback({
|
||||
heading: `@${safeText(skill.name) || safeText(skill.id) || 'Skill'}`,
|
||||
description: safeText(skill.description) || 'Installable skill from Antigravity Awesome Skills.',
|
||||
links: relatedLinks,
|
||||
});
|
||||
}
|
||||
|
||||
function setRootFallback(html, fallbackHtml) {
|
||||
const rootPattern = /<div\s+id=["']root["']><\/div>/i;
|
||||
|
||||
if (!fallbackHtml || !rootPattern.test(html)) {
|
||||
return html;
|
||||
}
|
||||
|
||||
return html.replace(rootPattern, `<div id="root">${fallbackHtml}</div>`);
|
||||
}
|
||||
|
||||
function buildHomeMeta({ catalogCount, imageUrl, canonicalUrl }) {
|
||||
const visibleCount = Math.max(catalogCount, HOME_CATALOG_COUNT_FALLBACK);
|
||||
const formattedCount = visibleCount.toLocaleString('en-US');
|
||||
const title = `Antigravity Awesome Skills | ${formattedCount}+ AI coding skills and plugins`;
|
||||
const description = `Explore ${formattedCount}+ installable agentic skills, specialized plugins, bundles, and workflows for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and other AI coding assistants.`;
|
||||
const title = `Antigravity Awesome Skills GitHub | ${formattedCount}+ AI coding skills`;
|
||||
const description = `Explore the GitHub library of ${formattedCount}+ installable agentic skills, specialized plugins, bundles, and workflows for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and other AI coding assistants.`;
|
||||
const catalogBaseUrl = canonicalUrl.replace(/\/$/, '');
|
||||
const sourceCodeEntity = {
|
||||
'@context': 'https://schema.org',
|
||||
@@ -204,6 +360,9 @@ function buildHomeMeta({ catalogCount, imageUrl, canonicalUrl }) {
|
||||
'Cursor skills',
|
||||
'Gemini CLI skills',
|
||||
'Antigravity skills',
|
||||
'Antigravity CLI skills',
|
||||
'GitHub AI skills repository',
|
||||
'AI agent skills GitHub',
|
||||
'specialized plugins',
|
||||
'SKILL.md',
|
||||
],
|
||||
@@ -376,6 +535,122 @@ function buildPluginsMeta({ pluginCount, imageUrl, canonicalUrl }) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildTopicLandingMeta({ page, imageUrl, canonicalUrl }) {
|
||||
const catalogBaseUrl = canonicalUrl.replace(/\/topics\/[^/]+\/?$/, '');
|
||||
const keywords = Array.isArray(page.keywords) ? page.keywords.join(', ') : '';
|
||||
const sourceCodeEntity = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareSourceCode',
|
||||
name: SITE_NAME,
|
||||
description: 'Installable GitHub library of agentic skills, specialized plugins, bundles, and workflows for AI coding assistants.',
|
||||
url: REPOSITORY_URL,
|
||||
sameAs: [
|
||||
canonicalUrl,
|
||||
HOSTED_CATALOG_URL,
|
||||
'https://www.npmjs.com/package/antigravity-awesome-skills',
|
||||
],
|
||||
mainEntityOfPage: canonicalUrl,
|
||||
codeRepository: REPOSITORY_URL,
|
||||
applicationCategory: 'DeveloperApplication',
|
||||
keywords: [
|
||||
...(Array.isArray(page.keywords) ? page.keywords : []),
|
||||
'specialized plugins',
|
||||
'SKILL.md',
|
||||
],
|
||||
isAccessibleForFree: true,
|
||||
programmingLanguage: {
|
||||
'@type': 'ComputerLanguage',
|
||||
name: 'Markdown',
|
||||
url: 'https://en.wikipedia.org/wiki/Markdown',
|
||||
},
|
||||
license: `${REPOSITORY_URL}/blob/main/LICENSE`,
|
||||
};
|
||||
|
||||
return {
|
||||
title: page.title,
|
||||
description: page.description,
|
||||
canonicalUrl,
|
||||
ogTitle: page.title,
|
||||
ogDescription: page.description,
|
||||
ogImage: imageUrl,
|
||||
twitterCard: 'summary_large_image',
|
||||
jsonLd: [
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
name: page.h1,
|
||||
headline: page.h1,
|
||||
description: page.description,
|
||||
url: canonicalUrl,
|
||||
isPartOf: {
|
||||
'@type': 'WebSite',
|
||||
name: SITE_NAME,
|
||||
url: catalogBaseUrl,
|
||||
sameAs: REPOSITORY_URL,
|
||||
},
|
||||
about: sourceCodeEntity,
|
||||
keywords,
|
||||
mainEntity: {
|
||||
'@type': 'ItemList',
|
||||
name: `${page.eyebrow} topics`,
|
||||
itemListElement: Array.isArray(page.sections)
|
||||
? page.sections.map((section, index) => ({
|
||||
'@type': 'ListItem',
|
||||
position: index + 1,
|
||||
name: section.heading,
|
||||
description: section.body,
|
||||
}))
|
||||
: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{
|
||||
'@type': 'ListItem',
|
||||
position: 1,
|
||||
name: SITE_NAME,
|
||||
item: HOSTED_CATALOG_URL,
|
||||
},
|
||||
{
|
||||
'@type': 'ListItem',
|
||||
position: 2,
|
||||
name: page.h1,
|
||||
item: canonicalUrl,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Organization',
|
||||
'@id': `${REPOSITORY_URL}#organization`,
|
||||
name: SITE_NAME,
|
||||
url: REPOSITORY_URL,
|
||||
sameAs: [
|
||||
'https://x.com/AASkills_',
|
||||
'https://www.npmjs.com/package/antigravity-awesome-skills',
|
||||
HOSTED_CATALOG_URL,
|
||||
],
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
name: SITE_NAME,
|
||||
url: catalogBaseUrl,
|
||||
sameAs: REPOSITORY_URL,
|
||||
inLanguage: 'en',
|
||||
potentialAction: {
|
||||
'@type': 'SearchAction',
|
||||
target: `${catalogBaseUrl}/?q={search_term_string}`,
|
||||
'query-input': 'required name=search_term_string',
|
||||
},
|
||||
},
|
||||
sourceCodeEntity,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function applySeoMeta(templateHtml, meta) {
|
||||
let output = templateHtml;
|
||||
const title = safeText(meta.title);
|
||||
@@ -403,9 +678,9 @@ function applySeoMeta(templateHtml, meta) {
|
||||
return output;
|
||||
}
|
||||
|
||||
function writePrerenderedRoute(routePath, templateHtml, meta) {
|
||||
function writePrerenderedRoute(routePath, templateHtml, meta, fallbackHtml = '') {
|
||||
const filePath = routeToFilePath(routePath);
|
||||
const rendered = applySeoMeta(templateHtml, meta);
|
||||
const rendered = setRootFallback(applySeoMeta(templateHtml, meta), fallbackHtml);
|
||||
const directory = path.dirname(filePath);
|
||||
ensureDirectory(directory);
|
||||
fs.writeFileSync(filePath, rendered, 'utf-8');
|
||||
@@ -426,6 +701,21 @@ function readCatalog() {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function readSeoLandingPages() {
|
||||
if (!fs.existsSync(SEO_LANDING_PAGES_PATH)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(SEO_LANDING_PAGES_PATH, 'utf-8');
|
||||
const parsed = JSON.parse(raw);
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error('SEO landing pages data must be an array.');
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!fs.existsSync(TEMPLATE_PATH)) {
|
||||
throw new Error(`Built index file not found at ${TEMPLATE_PATH}. Run npm run build before prerender.`);
|
||||
@@ -433,6 +723,7 @@ function main() {
|
||||
|
||||
const template = fs.readFileSync(TEMPLATE_PATH, 'utf-8');
|
||||
const skills = readCatalog();
|
||||
const landingPages = readSeoLandingPages();
|
||||
const siteBaseUrl = getSiteBaseUrl();
|
||||
const topCount = parseCount(process.env.PRERENDER_TOP_SKILL_COUNT || process.env.TOP_SKILL_COUNT, 40);
|
||||
const topSkillPaths = selectTopSkillEntries(skills, topCount);
|
||||
@@ -456,6 +747,26 @@ function main() {
|
||||
});
|
||||
writePrerenderedRoute('/plugins', template, pluginsMeta);
|
||||
|
||||
for (const page of landingPages) {
|
||||
if (!page?.slug) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const routePath = `/topics/${encodeURIComponent(page.slug)}`;
|
||||
const canonicalUrl = routeToUrl(routePath, siteBaseUrl);
|
||||
const landingMeta = buildTopicLandingMeta({
|
||||
page,
|
||||
imageUrl: socialImage,
|
||||
canonicalUrl,
|
||||
});
|
||||
writePrerenderedRoute(
|
||||
routePath,
|
||||
template,
|
||||
landingMeta,
|
||||
buildTopicFallback({ page, landingPages, siteBaseUrl }),
|
||||
);
|
||||
}
|
||||
|
||||
for (const skillRoute of topSkillPaths) {
|
||||
const decodedId = decodeURIComponent(skillRoute.replace(/^\/skill\//, ''));
|
||||
const skill = skillMap.get(decodedId);
|
||||
@@ -470,7 +781,12 @@ function main() {
|
||||
imageUrl: socialImage,
|
||||
canonicalUrl,
|
||||
});
|
||||
writePrerenderedRoute(skillRoute, template, skillMeta);
|
||||
writePrerenderedRoute(
|
||||
skillRoute,
|
||||
template,
|
||||
skillMeta,
|
||||
buildSkillFallback({ skill, landingPages, siteBaseUrl }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { getSeoLandingPaths } from './generate-sitemap.js';
|
||||
|
||||
export function extractSitemapLocations(xmlText) {
|
||||
const raw = String(xmlText ?? '');
|
||||
@@ -165,12 +166,23 @@ export function analyzeSitemap(urlText, { minSkillUrls = 1 } = {}) {
|
||||
`${normalizedRoot}/plugins`,
|
||||
`${normalizedRoot}/plugins/`,
|
||||
]);
|
||||
const topicPathVariants = new Set(
|
||||
getSeoLandingPaths().flatMap((topicPath) => [
|
||||
`${normalizedRoot}${topicPath}`,
|
||||
`${normalizedRoot}${topicPath}/`,
|
||||
]),
|
||||
);
|
||||
const skillRoutes = extraRoutes.filter(({ parsed: parsedUrl }) =>
|
||||
parsedUrl.pathname.startsWith(skillPrefix),
|
||||
);
|
||||
const topicRoutes = extraRoutes.filter(({ parsed: parsedUrl }) =>
|
||||
topicPathVariants.has(parsedUrl.pathname),
|
||||
);
|
||||
const unsupportedRoutes = extraRoutes.filter(
|
||||
({ parsed: parsedUrl }) =>
|
||||
!parsedUrl.pathname.startsWith(skillPrefix) && !allowedExtraPathVariants.has(parsedUrl.pathname),
|
||||
!parsedUrl.pathname.startsWith(skillPrefix) &&
|
||||
!allowedExtraPathVariants.has(parsedUrl.pathname) &&
|
||||
!topicPathVariants.has(parsedUrl.pathname),
|
||||
);
|
||||
|
||||
assert(
|
||||
@@ -188,6 +200,7 @@ export function analyzeSitemap(urlText, { minSkillUrls = 1 } = {}) {
|
||||
rootPath: rootUrl.pathname,
|
||||
normalizedRootPath: normalizedRoot,
|
||||
skillUrls: skillRoutes.map(({ raw }) => raw),
|
||||
topicUrls: topicRoutes.map(({ raw }) => raw),
|
||||
pluginUrls: extraRoutes
|
||||
.filter(({ parsed: parsedUrl }) => allowedExtraPathVariants.has(parsedUrl.pathname))
|
||||
.map(({ raw }) => raw),
|
||||
@@ -293,6 +306,7 @@ export function assertIndexDiscoveryMeta(htmlText, { expectedSkillCountLabel = '
|
||||
combined.includes(expectedSkillCountLabel),
|
||||
`Home SEO metadata must expose the current ${expectedSkillCountLabel} skill count.`,
|
||||
);
|
||||
assert(combined.includes('GitHub library'), 'Home SEO metadata must mention the GitHub library.');
|
||||
assert(combined.includes('specialized plugins'), 'Home SEO metadata must mention specialized plugins.');
|
||||
assert(!combined.includes('prompt templates'), 'Home SEO metadata must not use stale prompt-template positioning.');
|
||||
assertJsonLdTypes(htmlText, ['CollectionPage', 'Organization', 'WebSite', 'SoftwareSourceCode', 'FAQPage']);
|
||||
@@ -310,6 +324,32 @@ export function assertPluginsDiscoveryMeta(htmlText) {
|
||||
assertJsonLdTypes(htmlText, ['CollectionPage', 'Organization']);
|
||||
}
|
||||
|
||||
export function assertTopicDiscoveryMeta(htmlText) {
|
||||
const title = extractTitle(htmlText);
|
||||
const description = extractMetaContent(htmlText, 'name', 'description') || '';
|
||||
const ogTitle = extractMetaContent(htmlText, 'property', 'og:title') || '';
|
||||
const combined = [title, description, ogTitle].join(' ');
|
||||
|
||||
assert(combined.includes('Antigravity') || combined.includes('GitHub'), 'Topic page SEO metadata must expose a relevant discovery title.');
|
||||
assert(
|
||||
combined.includes('skills') || combined.includes('Skills') || combined.includes('plugins') || combined.includes('Plugins'),
|
||||
'Topic page SEO metadata must mention skills or plugins.',
|
||||
);
|
||||
assertJsonLdTypes(htmlText, ['WebPage', 'BreadcrumbList', 'Organization', 'WebSite', 'SoftwareSourceCode']);
|
||||
}
|
||||
|
||||
function assertStaticRelatedTopicLinks(htmlText, routeType) {
|
||||
const html = String(htmlText ?? '');
|
||||
assert(
|
||||
html.includes('data-prerender-fallback="true"'),
|
||||
`${routeType} prerendered page must expose a static fallback body.`,
|
||||
);
|
||||
assert(
|
||||
/<a\s+href=["'][^"']*\/topics\/[^"']+["'][^>]*>[^<]+<\/a>/i.test(html),
|
||||
`${routeType} prerendered page must include static related topic links.`,
|
||||
);
|
||||
}
|
||||
|
||||
function routePathToDistFile(routePath, normalizedRootPath) {
|
||||
const normalizedPath = (routePath || '/').replace(/\/+$/, '') || '/';
|
||||
const normalizedRoot = normalizedRootPath === '/' ? '' : String(normalizedRootPath || '').replace(/\/+$/, '');
|
||||
@@ -328,6 +368,7 @@ export function assertPrerenderedSkillRoutes(skillUrls, distDir = 'dist', normal
|
||||
fs.existsSync(filePath),
|
||||
`Missing prerendered page for skill route: ${parsed.pathname}. Expected ${filePath}.`,
|
||||
);
|
||||
assertStaticRelatedTopicLinks(readFile(filePath), 'Skill');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,6 +384,20 @@ export function assertPrerenderedPluginRoutes(pluginUrls, distDir = 'dist', norm
|
||||
}
|
||||
}
|
||||
|
||||
export function assertPrerenderedTopicRoutes(topicUrls, distDir = 'dist', normalizedRootPath = '') {
|
||||
for (const topicUrl of topicUrls) {
|
||||
const parsed = new URL(topicUrl);
|
||||
const filePath = path.join(distDir, routePathToDistFile(parsed.pathname, normalizedRootPath));
|
||||
assert(
|
||||
fs.existsSync(filePath),
|
||||
`Missing prerendered page for topic route: ${parsed.pathname}. Expected ${filePath}.`,
|
||||
);
|
||||
const html = readFile(filePath);
|
||||
assertTopicDiscoveryMeta(html);
|
||||
assertStaticRelatedTopicLinks(html, 'Topic');
|
||||
}
|
||||
}
|
||||
|
||||
export function assertRobots(robotsText) {
|
||||
const lines = String(robotsText ?? '').split(/\r?\n/).map((line) => line.trim());
|
||||
const allowsRoot = lines.some((line) => line.startsWith('Allow: /'));
|
||||
@@ -403,6 +458,7 @@ export function runVerification({
|
||||
const expectedSkillCountLabel = readSkillCountLabel(distDir);
|
||||
assertPrerenderedSkillRoutes(sitemapReport.skillUrls, distDir, sitemapReport.normalizedRootPath);
|
||||
assertPrerenderedPluginRoutes(sitemapReport.pluginUrls, distDir, sitemapReport.normalizedRootPath);
|
||||
assertPrerenderedTopicRoutes(sitemapReport.topicUrls, distDir, sitemapReport.normalizedRootPath);
|
||||
assertIndexSocialMeta(indexHtml);
|
||||
assertIndexDiscoveryMeta(indexHtml, { expectedSkillCountLabel });
|
||||
assertRobots(readFile(robotsPath));
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
analyzeSitemap,
|
||||
assertPrerenderedPluginRoutes,
|
||||
assertPrerenderedSkillRoutes,
|
||||
assertPrerenderedTopicRoutes,
|
||||
assertIndexSocialMeta,
|
||||
assertLlms,
|
||||
assertRobots,
|
||||
@@ -38,6 +39,7 @@ describe('seo assets verification helpers', () => {
|
||||
<urlset>
|
||||
<url><loc>https://owner.github.io/repo/</loc></url>
|
||||
<url><loc>https://owner.github.io/repo/plugins</loc></url>
|
||||
<url><loc>https://owner.github.io/repo/topics/antigravity-cli-skills</loc></url>
|
||||
<url><loc>https://owner.github.io/repo/skill/agent-a</loc></url>
|
||||
<url><loc>https://owner.github.io/repo/skill/agent-b</loc></url>
|
||||
</urlset>
|
||||
@@ -104,12 +106,12 @@ describe('seo assets verification helpers', () => {
|
||||
const html = `
|
||||
<html>
|
||||
<head>
|
||||
<title>Antigravity Awesome Skills | 1,678+ AI coding skills and plugins</title>
|
||||
<meta name="description" content="Explore 1,678+ installable agentic skills, specialized plugins, bundles, and workflows." />
|
||||
<meta property="og:title" content="Antigravity Awesome Skills | 1,678+ AI coding skills and plugins" />
|
||||
<meta property="og:description" content="Explore 1,678+ installable agentic skills, specialized plugins, bundles, and workflows." />
|
||||
<meta name="twitter:title" content="Antigravity Awesome Skills | 1,678+ AI coding skills and plugins" />
|
||||
<meta name="twitter:description" content="Explore 1,678+ installable agentic skills, specialized plugins, bundles, and workflows." />
|
||||
<title>Antigravity Awesome Skills GitHub | 1,678+ AI coding skills</title>
|
||||
<meta name="description" content="Explore the GitHub library of 1,678+ installable agentic skills, specialized plugins, bundles, and workflows." />
|
||||
<meta property="og:title" content="Antigravity Awesome Skills GitHub | 1,678+ AI coding skills" />
|
||||
<meta property="og:description" content="Explore the GitHub library of 1,678+ installable agentic skills, specialized plugins, bundles, and workflows." />
|
||||
<meta name="twitter:title" content="Antigravity Awesome Skills GitHub | 1,678+ AI coding skills" />
|
||||
<meta name="twitter:description" content="Explore the GitHub library of 1,678+ installable agentic skills, specialized plugins, bundles, and workflows." />
|
||||
<script type="application/ld+json">
|
||||
[
|
||||
{"@context":"https://schema.org","@type":"CollectionPage","sameAs":"https://github.com/sickn33/antigravity-awesome-skills"},
|
||||
@@ -146,12 +148,36 @@ describe('seo assets verification helpers', () => {
|
||||
expect(() => assertPluginsDiscoveryMeta(html)).not.toThrow();
|
||||
});
|
||||
|
||||
it('validates prerendered topic route files when present', () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'seo-assets-'));
|
||||
const distDir = path.join(tmpDir, 'dist');
|
||||
const routeFile = path.join(distDir, 'topics', 'antigravity-cli-skills', 'index.html');
|
||||
fs.mkdirSync(path.dirname(routeFile), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
routeFile,
|
||||
'<html><head><title>Antigravity CLI Skills | Installable AI agent playbooks</title><meta name="description" content="Install Antigravity CLI skills from the GitHub repository." /><meta property="og:title" content="Antigravity CLI Skills" /><script type="application/ld+json">[{"@context":"https://schema.org","@type":"WebPage"},{"@context":"https://schema.org","@type":"BreadcrumbList"},{"@context":"https://schema.org","@type":"Organization"},{"@context":"https://schema.org","@type":"WebSite"},{"@context":"https://schema.org","@type":"SoftwareSourceCode"}]</script></head><body><div id="root"><main data-prerender-fallback="true"><a href="https://owner.github.io/repo/topics/github-ai-skills-repository">A GitHub repository for installable AI agent skills</a></main></div></body></html>',
|
||||
);
|
||||
|
||||
const xml = `
|
||||
<urlset>
|
||||
<url><loc>https://owner.github.io/repo/</loc></url>
|
||||
<url><loc>https://owner.github.io/repo/topics/antigravity-cli-skills</loc></url>
|
||||
</urlset>
|
||||
`;
|
||||
|
||||
const report = analyzeSitemap(xml, { minSkillUrls: 0 });
|
||||
expect(() => assertPrerenderedTopicRoutes(report.topicUrls, distDir, report.normalizedRootPath)).not.toThrow();
|
||||
});
|
||||
|
||||
it('validates prerendered skill route files when present', () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'seo-assets-'));
|
||||
const distDir = path.join(tmpDir, 'dist');
|
||||
const routeFile = path.join(distDir, 'skill', 'agent-a', 'index.html');
|
||||
fs.mkdirSync(path.dirname(routeFile), { recursive: true });
|
||||
fs.writeFileSync(routeFile, '<html></html>');
|
||||
fs.writeFileSync(
|
||||
routeFile,
|
||||
'<html><body><div id="root"><main data-prerender-fallback="true"><a href="https://owner.github.io/repo/topics/antigravity-cli-skills">Antigravity CLI skills for agentic coding workflows</a></main></div></body></html>',
|
||||
);
|
||||
|
||||
const xml = `
|
||||
<urlset>
|
||||
|
||||
Reference in New Issue
Block a user