Skip to content

Commit db4edf7

Browse files
authored
seo: fix crawlability, restructure site, and improve internal linking (#166)
Root cause: skill cards used div+onClick instead of <a href>, leaving Google no crawl path to any skill subpage despite 19 pages existing. Changes: - BrowseTab: replace div+onClick cards with stretched cover-link pattern (absolute <a> beneath content div) so download/GitHub buttons remain valid anchors without nesting; add hash-based category URL sync via history.replaceState so /skills/#DeFi links land pre-filtered - Homepage (index.astro): restructure as static marketing page with category cards grid linking to /skills/#<category>; ItemList JSON-LD added via extraJsonLd so default WebSite graph is preserved - skills/index.astro: new hub page that mounts BrowseTab island; gives Google a canonical /skills/ entry point with crawlable card links - skills/[slug]/index.astro: add BreadcrumbList JSON-LD, visible breadcrumb nav, related-skills grid, and prev/next skill navigation; pass prevUrl/nextUrl for <link rel="prev/next"> in <head> - BaseLayout: add extraJsonLd slot (second JSON-LD block), prevUrl/nextUrl props rendered as <link rel="prev/next"> - SiteLayout: make activeTab optional; simplify nav to 3 tabs (browse, how it works, get started); thread extraJsonLd/prevUrl/nextUrl through - astro.config.mjs: add lastmod: new Date() to sitemap integration
1 parent 1d125a9 commit db4edf7

7 files changed

Lines changed: 312 additions & 78 deletions

File tree

astro.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import sitemap from '@astrojs/sitemap';
55
export default defineConfig({
66
site: process.env.SITE_URL || 'https://skills.internetcomputer.org',
77
base: '/',
8-
integrations: [preact(), sitemap()],
8+
integrations: [preact(), sitemap({ lastmod: new Date() })],
99
build: {
1010
format: 'directory',
1111
},

src/components/BrowseTab.tsx

Lines changed: 84 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,21 @@ export default function BrowseTab({ skills }: Props) {
1818
() => ["All", ...Array.from(new Set(skills.map((s) => s.category))).sort()],
1919
[skills]
2020
);
21+
22+
// Pre-select category from URL hash (e.g. /skills/#DeFi)
23+
useEffect(() => {
24+
const hash = decodeURIComponent(window.location.hash.slice(1));
25+
if (hash && categories.includes(hash)) setActiveCategory(hash);
26+
}, [categories]);
27+
28+
// Keep URL hash in sync when category changes
29+
useEffect(() => {
30+
if (activeCategory === "All") {
31+
history.replaceState(null, "", window.location.pathname + window.location.search);
32+
} else {
33+
history.replaceState(null, "", `#${encodeURIComponent(activeCategory)}`);
34+
}
35+
}, [activeCategory]);
2136
const filtered = useMemo(() => {
2237
return skills.filter((s) => {
2338
return activeCategory === "All" || s.category === activeCategory;
@@ -105,89 +120,91 @@ export default function BrowseTab({ skills }: Props) {
105120
return (
106121
<div
107122
key={skill.name}
108-
role="link"
109-
tabIndex={0}
110-
onClick={() => { window.location.href = `/skills/${skill.name}/`; }}
111-
onKeyDown={(e) => { if (e.key === "Enter") window.location.href = `/skills/${skill.name}/`; }}
112123
className="skill-card"
113124
style={{
125+
position: "relative",
114126
padding: "24px",
115127
background: "var(--bg-card)",
116128
border: "1px solid var(--border-default)",
117129
borderRadius: "5px",
118-
cursor: "pointer",
119130
color: "inherit",
120131
display: "block",
121132
overflow: "hidden",
122133
}}
123134
>
124-
<div style={{ display: "flex", alignItems: "center", gap: "10px", marginBottom: "12px" }}>
125-
<span style={{
126-
fontSize: "18px", width: "36px", height: "36px",
127-
display: "flex", alignItems: "center", justifyContent: "center",
128-
background: "var(--bg-input)",
129-
borderRadius: "8px", color: "var(--text-secondary)",
130-
flexShrink: 0,
131-
}}><CategoryIcon category={skill.category} /></span>
132-
<div style={{ minWidth: 0, flex: 1 }}>
133-
<div style={{
134-
fontSize: "16px", fontWeight: 700, color: "var(--text-primary)", letterSpacing: "-0.3px",
135-
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
136-
}}>
137-
{skill.title}
138-
</div>
139-
<div style={{ fontSize: "13px", color: "var(--text-faint)", marginTop: "2px" }}>
140-
{skill.category}
135+
{/* Cover link — crawlable anchor with no nesting */}
136+
<a
137+
href={`/skills/${skill.name}/`}
138+
aria-label={skill.title}
139+
style={{ position: "absolute", inset: 0, zIndex: 0, borderRadius: "5px" }}
140+
/>
141+
{/* Content sits above the cover link */}
142+
<div style={{ position: "relative", zIndex: 1 }}>
143+
<div style={{ display: "flex", alignItems: "center", gap: "10px", marginBottom: "12px" }}>
144+
<span style={{
145+
fontSize: "18px", width: "36px", height: "36px",
146+
display: "flex", alignItems: "center", justifyContent: "center",
147+
background: "var(--bg-input)",
148+
borderRadius: "8px", color: "var(--text-secondary)",
149+
flexShrink: 0,
150+
}}><CategoryIcon category={skill.category} /></span>
151+
<div style={{ minWidth: 0, flex: 1 }}>
152+
<div style={{
153+
fontSize: "16px", fontWeight: 700, color: "var(--text-primary)", letterSpacing: "-0.3px",
154+
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
155+
}}>
156+
{skill.title}
157+
</div>
158+
<div style={{ fontSize: "13px", color: "var(--text-faint)", marginTop: "2px" }}>
159+
{skill.category}
160+
</div>
141161
</div>
162+
<a href={skill.fileCount > 1
163+
? `/.well-known/skills/${skill.name}/SKILL.zip`
164+
: `/.well-known/skills/${skill.name}/SKILL.md`}
165+
download
166+
title={skill.fileCount > 1 ? "Download skill (.zip)" : "Download skill (.md)"}
167+
aria-label={`Download ${skill.title}`}
168+
className="github-link"
169+
style={{
170+
display: "flex", alignItems: "center", justifyContent: "center",
171+
width: "36px", height: "36px", borderRadius: "6px",
172+
color: "var(--text-faint)", flexShrink: 0,
173+
background: "var(--bg-card-subtle)",
174+
border: "1px solid var(--border-subtle)",
175+
}}>
176+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
177+
<path d="M8 2v8M4.5 7.5 8 11l3.5-3.5M2.5 13.5h11" />
178+
</svg>
179+
</a>
180+
<a href={`https://github.com/dfinity/icskills/blob/main/skills/${skill.name}/SKILL.md`}
181+
target="_blank" rel="noopener noreferrer"
182+
title="View on GitHub" aria-label={`View ${skill.title} on GitHub`}
183+
className="github-link"
184+
style={{
185+
display: "flex", alignItems: "center", justifyContent: "center",
186+
width: "36px", height: "36px", borderRadius: "6px",
187+
color: "var(--text-faint)", flexShrink: 0,
188+
background: "var(--bg-card-subtle)",
189+
border: "1px solid var(--border-subtle)",
190+
}}>
191+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/></svg>
192+
</a>
142193
</div>
143-
<a href={skill.fileCount > 1
144-
? `/.well-known/skills/${skill.name}/SKILL.zip`
145-
: `/.well-known/skills/${skill.name}/SKILL.md`}
146-
download
147-
onClick={(e) => e.stopPropagation()}
148-
title={skill.fileCount > 1 ? "Download skill (.zip)" : "Download skill (.md)"}
149-
aria-label={`Download ${skill.title}`}
150-
className="github-link"
151-
style={{
152-
display: "flex", alignItems: "center", justifyContent: "center",
153-
width: "36px", height: "36px", borderRadius: "6px",
154-
color: "var(--text-faint)", flexShrink: 0,
155-
background: "var(--bg-card-subtle)",
156-
border: "1px solid var(--border-subtle)",
157-
}}>
158-
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
159-
<path d="M8 2v8M4.5 7.5 8 11l3.5-3.5M2.5 13.5h11" />
160-
</svg>
161-
</a>
162-
<a href={`https://github.com/dfinity/icskills/blob/main/skills/${skill.name}/SKILL.md`}
163-
target="_blank" rel="noopener noreferrer"
164-
onClick={(e) => e.stopPropagation()}
165-
title="View on GitHub" aria-label={`View ${skill.title} on GitHub`}
166-
className="github-link"
167-
style={{
168-
display: "flex", alignItems: "center", justifyContent: "center",
169-
width: "36px", height: "36px", borderRadius: "6px",
170-
color: "var(--text-faint)", flexShrink: 0,
171-
background: "var(--bg-card-subtle)",
172-
border: "1px solid var(--border-subtle)",
173-
}}>
174-
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/></svg>
175-
</a>
176-
</div>
177194

178-
<p style={{
179-
fontSize: "14px", color: "var(--text-dim)", lineHeight: 1.6,
180-
margin: "0 0 16px 0", fontFamily: SANS_FONT,
181-
overflow: "hidden",
182-
display: "-webkit-box",
183-
WebkitLineClamp: 3,
184-
WebkitBoxOrient: "vertical",
185-
}}>{skill.description}</p>
195+
<p style={{
196+
fontSize: "14px", color: "var(--text-dim)", lineHeight: 1.6,
197+
margin: "0 0 16px 0", fontFamily: SANS_FONT,
198+
overflow: "hidden",
199+
display: "-webkit-box",
200+
WebkitLineClamp: 3,
201+
WebkitBoxOrient: "vertical",
202+
}}>{skill.description}</p>
186203

187-
<div style={{ fontSize: "12px", color: "var(--text-muted)" }}>
188-
updated {skill.lastUpdated}
204+
<div style={{ fontSize: "12px", color: "var(--text-muted)" }}>
205+
updated {skill.lastUpdated}
206+
</div>
189207
</div>
190-
191208
</div>
192209
);
193210
})}

src/layouts/BaseLayout.astro

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ interface Props {
66
ogTitle?: string;
77
ogDescription?: string;
88
jsonLd?: string;
9+
extraJsonLd?: string;
10+
prevUrl?: string;
11+
nextUrl?: string;
912
}
1013
1114
import { SITE_URL } from "../data/site";
@@ -17,6 +20,9 @@ const {
1720
ogTitle = "Internet Computer (ICP) Skills — Skills for agents, not docs for humans",
1821
ogDescription = "Agent-readable skill files for every Internet Computer capability. Zero hallucinations. Paste the raw URL into Claude, Cursor, or any agent.",
1922
jsonLd,
23+
extraJsonLd,
24+
prevUrl,
25+
nextUrl,
2026
} = Astro.props;
2127
2228
const site = SITE_URL;
@@ -59,6 +65,8 @@ const defaultJsonLd = JSON.stringify({
5965
<meta name="robots" content="index, follow">
6066
<meta name="author" content="Internet Computer (ICP) Skills">
6167
<link rel="canonical" href={canonicalUrl}>
68+
{prevUrl && <link rel="prev" href={prevUrl} />}
69+
{nextUrl && <link rel="next" href={nextUrl} />}
6270

6371
<meta property="og:type" content="website">
6472
<meta property="og:url" content={canonicalUrl}>
@@ -80,6 +88,7 @@ const defaultJsonLd = JSON.stringify({
8088
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
8189

8290
<script type="application/ld+json" set:html={jsonLd || defaultJsonLd} />
91+
{extraJsonLd && <script is:inline type="application/ld+json" set:html={extraJsonLd} />}
8392

8493
<link rel="preconnect" href="https://fonts.googleapis.com">
8594
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

src/layouts/SiteLayout.astro

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ interface Props {
1010
ogTitle?: string;
1111
ogDescription?: string;
1212
jsonLd?: string;
13-
activeTab: "browse" | "how-it-works" | "get-started";
13+
extraJsonLd?: string;
14+
prevUrl?: string;
15+
nextUrl?: string;
16+
activeTab?: "skills" | "how-it-works" | "get-started";
1417
}
1518
1619
const {
@@ -20,17 +23,20 @@ const {
2023
ogTitle,
2124
ogDescription,
2225
jsonLd,
26+
extraJsonLd,
27+
prevUrl,
28+
nextUrl,
2329
activeTab,
2430
} = Astro.props;
2531
2632
const tabs = [
27-
{ id: "browse", label: "browse", href: "/" },
33+
{ id: "skills", label: "browse", href: "/skills/" },
2834
{ id: "how-it-works", label: "how it works", href: "/how-it-works/" },
2935
{ id: "get-started", label: "get started", href: "/get-started/" },
3036
] as const;
3137
---
3238

33-
<BaseLayout title={title} description={description} canonicalUrl={canonicalUrl} ogTitle={ogTitle} ogDescription={ogDescription} jsonLd={jsonLd}>
39+
<BaseLayout title={title} description={description} canonicalUrl={canonicalUrl} ogTitle={ogTitle} ogDescription={ogDescription} jsonLd={jsonLd} extraJsonLd={extraJsonLd} prevUrl={prevUrl} nextUrl={nextUrl}>
3440
<div style="min-height:100vh;background:var(--bg-page);color:var(--text-body);font-family:'Inter',system-ui,sans-serif;position:relative;overflow:hidden;">
3541
<!-- Grid background -->
3642
<div style="position:fixed;inset:0;opacity:var(--grid-opacity);background-image:linear-gradient(var(--border-subtle) 1px,transparent 1px),linear-gradient(90deg,var(--border-subtle) 1px,transparent 1px);background-size:60px 60px;pointer-events:none;"></div>

src/pages/index.astro

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,113 @@
11
---
22
import SiteLayout from "../layouts/SiteLayout.astro";
3-
import BrowseTab from "../components/BrowseTab";
3+
import { CategoryIcon } from "../components/Icons";
44
import { loadAllSkills } from "../data/skills";
5+
import { SITE_URL } from "../data/site";
6+
import { SANS_FONT } from "../data/constants";
57
68
const skills = loadAllSkills();
9+
10+
const categoryMap = skills.reduce((acc, s) => {
11+
if (!acc[s.category]) acc[s.category] = [];
12+
acc[s.category].push(s);
13+
return acc;
14+
}, {} as Record<string, typeof skills>);
15+
const categories = Object.entries(categoryMap).sort(([a], [b]) => a.localeCompare(b));
16+
17+
const promptText = `Fetch ${SITE_URL}/llms.txt and follow its instructions when building on ICP`;
18+
19+
const itemListJsonLd = JSON.stringify({
20+
"@context": "https://schema.org",
21+
"@type": "ItemList",
22+
"itemListElement": skills.map((s, i) => ({
23+
"@type": "ListItem",
24+
"position": i + 1,
25+
"url": `${SITE_URL}/skills/${s.name}/`,
26+
"name": s.title,
27+
})),
28+
});
729
---
830

931
<SiteLayout
1032
title="Internet Computer (ICP) Skills — Agent-Readable Documentation for Internet Computer"
11-
activeTab="browse"
33+
extraJsonLd={itemListJsonLd}
1234
>
13-
<BrowseTab client:load skills={skills} />
35+
{/* Hero */}
36+
<div style="margin-bottom: 64px;">
37+
<h1 style={`font-size: clamp(28px, 5vw, 48px); font-weight: 800; line-height: 1.1; margin: 0 0 16px 0; letter-spacing: -2px; color: var(--text-primary);`}>
38+
ICP skills for agents that write code.
39+
</h1>
40+
<p style={`font-size: 15px; color: var(--text-tertiary); max-width: 560px; line-height: 1.6; margin: 0 0 28px 0; font-family: ${SANS_FONT};`}>
41+
Build using sovereign software on an onchain open cloud that's tamperproof,
42+
unstoppable, and can process digital assets and payments
43+
</p>
44+
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 16px;">
45+
<button id="copy-prompt" class="copy-prompt-btn" style={`display: flex; align-items: center; gap: 8px; padding: 10px 20px; border-radius: 8px; background: var(--bg-input); border: 1px solid var(--border-strong); color: var(--text-primary); cursor: pointer; font-size: 14px; font-weight: 600; font-family: ${SANS_FONT}; transition: all 0.15s ease;`}>
46+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></svg>
47+
<span id="copy-label">Give your agent ICP skills</span>
48+
</button>
49+
<a href="/skills/" style={`font-size: 14px; color: var(--text-muted); text-decoration: none; font-family: ${SANS_FONT};`}>
50+
Browse all {skills.length} skills →
51+
</a>
52+
</div>
53+
</div>
54+
55+
{/* Category cards */}
56+
<div>
57+
<h2 style={`font-size: 12px; font-weight: 600; color: var(--text-muted); letter-spacing: 0.08em; text-transform: uppercase; margin: 0 0 16px 0; font-family: ${SANS_FONT};`}>
58+
Categories
59+
</h2>
60+
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(min(220px, 100%), 1fr)); gap: 10px;">
61+
{categories.map(([cat, catSkills]) => (
62+
<a
63+
href={`/skills/#${encodeURIComponent(cat)}`}
64+
style="display: block; padding: 16px 18px; background: var(--bg-card); border: 1px solid var(--border-default); border-radius: 6px; text-decoration: none; color: inherit;"
65+
>
66+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
67+
<span style="display: flex; align-items: center; color: var(--text-secondary); flex-shrink: 0;">
68+
<CategoryIcon category={cat} size={18} />
69+
</span>
70+
<span style={`font-size: 14px; font-weight: 600; color: var(--text-primary); font-family: ${SANS_FONT};`}>{cat}</span>
71+
<span style={`margin-left: auto; font-size: 11px; color: var(--text-faint); font-family: ${SANS_FONT}; flex-shrink: 0;`}>
72+
{catSkills.length} {catSkills.length === 1 ? "skill" : "skills"}
73+
</span>
74+
</div>
75+
<ul style="list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px;">
76+
{catSkills.slice(0, 3).map((s) => (
77+
<li style={`font-size: 12px; color: var(--text-dim); font-family: ${SANS_FONT}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;`}>
78+
· {s.title}
79+
</li>
80+
))}
81+
{catSkills.length > 3 && (
82+
<li style={`font-size: 12px; color: var(--text-faint); font-family: ${SANS_FONT};`}>
83+
+{catSkills.length - 3} more
84+
</li>
85+
)}
86+
</ul>
87+
</a>
88+
))}
89+
</div>
90+
</div>
1491
</SiteLayout>
92+
93+
<script is:inline define:vars={{ promptText }}>
94+
(function () {
95+
var btn = document.getElementById('copy-prompt');
96+
var label = document.getElementById('copy-label');
97+
if (!btn || !label) return;
98+
var original = label.textContent;
99+
btn.addEventListener('click', function () {
100+
navigator.clipboard.writeText(promptText).catch(function () {});
101+
label.textContent = 'Now paste into your agent';
102+
btn.style.background = 'rgba(var(--green-rgb),0.1)';
103+
btn.style.borderColor = 'rgba(var(--green-rgb),0.2)';
104+
btn.style.color = 'var(--green)';
105+
setTimeout(function () {
106+
label.textContent = original;
107+
btn.style.background = '';
108+
btn.style.borderColor = '';
109+
btn.style.color = '';
110+
}, 3000);
111+
});
112+
})();
113+
</script>

0 commit comments

Comments
 (0)