Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: process.env.SITE_URL || 'https://skills.internetcomputer.org',
base: '/',
integrations: [preact(), sitemap()],
integrations: [preact(), sitemap({ lastmod: new Date() })],
build: {
format: 'directory',
},
Expand Down
151 changes: 84 additions & 67 deletions src/components/BrowseTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@ export default function BrowseTab({ skills }: Props) {
() => ["All", ...Array.from(new Set(skills.map((s) => s.category))).sort()],
[skills]
);

// Pre-select category from URL hash (e.g. /skills/#DeFi)
useEffect(() => {
const hash = decodeURIComponent(window.location.hash.slice(1));
if (hash && categories.includes(hash)) setActiveCategory(hash);
}, [categories]);

// Keep URL hash in sync when category changes
useEffect(() => {
if (activeCategory === "All") {
history.replaceState(null, "", window.location.pathname + window.location.search);
} else {
history.replaceState(null, "", `#${encodeURIComponent(activeCategory)}`);
}
}, [activeCategory]);
const filtered = useMemo(() => {
return skills.filter((s) => {
return activeCategory === "All" || s.category === activeCategory;
Expand Down Expand Up @@ -105,89 +120,91 @@ export default function BrowseTab({ skills }: Props) {
return (
<div
key={skill.name}
role="link"
tabIndex={0}
onClick={() => { window.location.href = `/skills/${skill.name}/`; }}
onKeyDown={(e) => { if (e.key === "Enter") window.location.href = `/skills/${skill.name}/`; }}
className="skill-card"
style={{
position: "relative",
padding: "24px",
background: "var(--bg-card)",
border: "1px solid var(--border-default)",
borderRadius: "5px",
cursor: "pointer",
color: "inherit",
display: "block",
overflow: "hidden",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "10px", marginBottom: "12px" }}>
<span style={{
fontSize: "18px", width: "36px", height: "36px",
display: "flex", alignItems: "center", justifyContent: "center",
background: "var(--bg-input)",
borderRadius: "8px", color: "var(--text-secondary)",
flexShrink: 0,
}}><CategoryIcon category={skill.category} /></span>
<div style={{ minWidth: 0, flex: 1 }}>
<div style={{
fontSize: "16px", fontWeight: 700, color: "var(--text-primary)", letterSpacing: "-0.3px",
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
}}>
{skill.title}
</div>
<div style={{ fontSize: "13px", color: "var(--text-faint)", marginTop: "2px" }}>
{skill.category}
{/* Cover link — crawlable anchor with no nesting */}
<a
href={`/skills/${skill.name}/`}
aria-label={skill.title}
style={{ position: "absolute", inset: 0, zIndex: 0, borderRadius: "5px" }}
/>
{/* Content sits above the cover link */}
<div style={{ position: "relative", zIndex: 1 }}>
<div style={{ display: "flex", alignItems: "center", gap: "10px", marginBottom: "12px" }}>
<span style={{
fontSize: "18px", width: "36px", height: "36px",
display: "flex", alignItems: "center", justifyContent: "center",
background: "var(--bg-input)",
borderRadius: "8px", color: "var(--text-secondary)",
flexShrink: 0,
}}><CategoryIcon category={skill.category} /></span>
<div style={{ minWidth: 0, flex: 1 }}>
<div style={{
fontSize: "16px", fontWeight: 700, color: "var(--text-primary)", letterSpacing: "-0.3px",
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
}}>
{skill.title}
</div>
<div style={{ fontSize: "13px", color: "var(--text-faint)", marginTop: "2px" }}>
{skill.category}
</div>
</div>
<a href={skill.fileCount > 1
? `/.well-known/skills/${skill.name}/SKILL.zip`
: `/.well-known/skills/${skill.name}/SKILL.md`}
download
title={skill.fileCount > 1 ? "Download skill (.zip)" : "Download skill (.md)"}
aria-label={`Download ${skill.title}`}
className="github-link"
style={{
display: "flex", alignItems: "center", justifyContent: "center",
width: "36px", height: "36px", borderRadius: "6px",
color: "var(--text-faint)", flexShrink: 0,
background: "var(--bg-card-subtle)",
border: "1px solid var(--border-subtle)",
}}>
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 2v8M4.5 7.5 8 11l3.5-3.5M2.5 13.5h11" />
</svg>
</a>
<a href={`https://github.com/dfinity/icskills/blob/main/skills/${skill.name}/SKILL.md`}
target="_blank" rel="noopener noreferrer"
title="View on GitHub" aria-label={`View ${skill.title} on GitHub`}
className="github-link"
style={{
display: "flex", alignItems: "center", justifyContent: "center",
width: "36px", height: "36px", borderRadius: "6px",
color: "var(--text-faint)", flexShrink: 0,
background: "var(--bg-card-subtle)",
border: "1px solid var(--border-subtle)",
}}>
<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>
</a>
</div>
<a href={skill.fileCount > 1
? `/.well-known/skills/${skill.name}/SKILL.zip`
: `/.well-known/skills/${skill.name}/SKILL.md`}
download
onClick={(e) => e.stopPropagation()}
title={skill.fileCount > 1 ? "Download skill (.zip)" : "Download skill (.md)"}
aria-label={`Download ${skill.title}`}
className="github-link"
style={{
display: "flex", alignItems: "center", justifyContent: "center",
width: "36px", height: "36px", borderRadius: "6px",
color: "var(--text-faint)", flexShrink: 0,
background: "var(--bg-card-subtle)",
border: "1px solid var(--border-subtle)",
}}>
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 2v8M4.5 7.5 8 11l3.5-3.5M2.5 13.5h11" />
</svg>
</a>
<a href={`https://github.com/dfinity/icskills/blob/main/skills/${skill.name}/SKILL.md`}
target="_blank" rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
title="View on GitHub" aria-label={`View ${skill.title} on GitHub`}
className="github-link"
style={{
display: "flex", alignItems: "center", justifyContent: "center",
width: "36px", height: "36px", borderRadius: "6px",
color: "var(--text-faint)", flexShrink: 0,
background: "var(--bg-card-subtle)",
border: "1px solid var(--border-subtle)",
}}>
<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>
</a>
</div>

<p style={{
fontSize: "14px", color: "var(--text-dim)", lineHeight: 1.6,
margin: "0 0 16px 0", fontFamily: SANS_FONT,
overflow: "hidden",
display: "-webkit-box",
WebkitLineClamp: 3,
WebkitBoxOrient: "vertical",
}}>{skill.description}</p>
<p style={{
fontSize: "14px", color: "var(--text-dim)", lineHeight: 1.6,
margin: "0 0 16px 0", fontFamily: SANS_FONT,
overflow: "hidden",
display: "-webkit-box",
WebkitLineClamp: 3,
WebkitBoxOrient: "vertical",
}}>{skill.description}</p>

<div style={{ fontSize: "12px", color: "var(--text-muted)" }}>
updated {skill.lastUpdated}
<div style={{ fontSize: "12px", color: "var(--text-muted)" }}>
updated {skill.lastUpdated}
</div>
</div>

</div>
);
})}
Expand Down
9 changes: 9 additions & 0 deletions src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ interface Props {
ogTitle?: string;
ogDescription?: string;
jsonLd?: string;
extraJsonLd?: string;
prevUrl?: string;
nextUrl?: string;
}

import { SITE_URL } from "../data/site";
Expand All @@ -17,6 +20,9 @@ const {
ogTitle = "Internet Computer (ICP) Skills — Skills for agents, not docs for humans",
ogDescription = "Agent-readable skill files for every Internet Computer capability. Zero hallucinations. Paste the raw URL into Claude, Cursor, or any agent.",
jsonLd,
extraJsonLd,
prevUrl,
nextUrl,
} = Astro.props;

const site = SITE_URL;
Expand Down Expand Up @@ -59,6 +65,8 @@ const defaultJsonLd = JSON.stringify({
<meta name="robots" content="index, follow">
<meta name="author" content="Internet Computer (ICP) Skills">
<link rel="canonical" href={canonicalUrl}>
{prevUrl && <link rel="prev" href={prevUrl} />}
{nextUrl && <link rel="next" href={nextUrl} />}

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

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

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
Expand Down
12 changes: 9 additions & 3 deletions src/layouts/SiteLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ interface Props {
ogTitle?: string;
ogDescription?: string;
jsonLd?: string;
activeTab: "browse" | "how-it-works" | "get-started";
extraJsonLd?: string;
prevUrl?: string;
nextUrl?: string;
activeTab?: "skills" | "how-it-works" | "get-started";
}

const {
Expand All @@ -20,17 +23,20 @@ const {
ogTitle,
ogDescription,
jsonLd,
extraJsonLd,
prevUrl,
nextUrl,
activeTab,
} = Astro.props;

const tabs = [
{ id: "browse", label: "browse", href: "/" },
{ id: "skills", label: "browse", href: "/skills/" },
{ id: "how-it-works", label: "how it works", href: "/how-it-works/" },
{ id: "get-started", label: "get started", href: "/get-started/" },
] as const;
---

<BaseLayout title={title} description={description} canonicalUrl={canonicalUrl} ogTitle={ogTitle} ogDescription={ogDescription} jsonLd={jsonLd}>
<BaseLayout title={title} description={description} canonicalUrl={canonicalUrl} ogTitle={ogTitle} ogDescription={ogDescription} jsonLd={jsonLd} extraJsonLd={extraJsonLd} prevUrl={prevUrl} nextUrl={nextUrl}>
<div style="min-height:100vh;background:var(--bg-page);color:var(--text-body);font-family:'Inter',system-ui,sans-serif;position:relative;overflow:hidden;">
<!-- Grid background -->
<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>
Expand Down
105 changes: 102 additions & 3 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -1,14 +1,113 @@
---
import SiteLayout from "../layouts/SiteLayout.astro";
import BrowseTab from "../components/BrowseTab";
import { CategoryIcon } from "../components/Icons";
import { loadAllSkills } from "../data/skills";
import { SITE_URL } from "../data/site";
import { SANS_FONT } from "../data/constants";

const skills = loadAllSkills();

const categoryMap = skills.reduce((acc, s) => {
if (!acc[s.category]) acc[s.category] = [];
acc[s.category].push(s);
return acc;
}, {} as Record<string, typeof skills>);
const categories = Object.entries(categoryMap).sort(([a], [b]) => a.localeCompare(b));

const promptText = `Fetch ${SITE_URL}/llms.txt and follow its instructions when building on ICP`;

const itemListJsonLd = JSON.stringify({
"@context": "https://schema.org",
"@type": "ItemList",
"itemListElement": skills.map((s, i) => ({
"@type": "ListItem",
"position": i + 1,
"url": `${SITE_URL}/skills/${s.name}/`,
"name": s.title,
})),
});
---

<SiteLayout
title="Internet Computer (ICP) Skills — Agent-Readable Documentation for Internet Computer"
activeTab="browse"
extraJsonLd={itemListJsonLd}
>
<BrowseTab client:load skills={skills} />
{/* Hero */}
<div style="margin-bottom: 64px;">
<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);`}>
ICP skills for agents that write code.
</h1>
<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};`}>
Build using sovereign software on an onchain open cloud that's tamperproof,
unstoppable, and can process digital assets and payments
</p>
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 16px;">
<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;`}>
<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>
<span id="copy-label">Give your agent ICP skills</span>
</button>
<a href="/skills/" style={`font-size: 14px; color: var(--text-muted); text-decoration: none; font-family: ${SANS_FONT};`}>
Browse all {skills.length} skills →
</a>
</div>
</div>

{/* Category cards */}
<div>
<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};`}>
Categories
</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(min(220px, 100%), 1fr)); gap: 10px;">
{categories.map(([cat, catSkills]) => (
<a
href={`/skills/#${encodeURIComponent(cat)}`}
style="display: block; padding: 16px 18px; background: var(--bg-card); border: 1px solid var(--border-default); border-radius: 6px; text-decoration: none; color: inherit;"
>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
<span style="display: flex; align-items: center; color: var(--text-secondary); flex-shrink: 0;">
<CategoryIcon category={cat} size={18} />
</span>
<span style={`font-size: 14px; font-weight: 600; color: var(--text-primary); font-family: ${SANS_FONT};`}>{cat}</span>
<span style={`margin-left: auto; font-size: 11px; color: var(--text-faint); font-family: ${SANS_FONT}; flex-shrink: 0;`}>
{catSkills.length} {catSkills.length === 1 ? "skill" : "skills"}
</span>
</div>
<ul style="list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px;">
{catSkills.slice(0, 3).map((s) => (
<li style={`font-size: 12px; color: var(--text-dim); font-family: ${SANS_FONT}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;`}>
· {s.title}
</li>
))}
{catSkills.length > 3 && (
<li style={`font-size: 12px; color: var(--text-faint); font-family: ${SANS_FONT};`}>
+{catSkills.length - 3} more
</li>
)}
</ul>
</a>
))}
</div>
</div>
</SiteLayout>

<script is:inline define:vars={{ promptText }}>
(function () {
var btn = document.getElementById('copy-prompt');
var label = document.getElementById('copy-label');
if (!btn || !label) return;
var original = label.textContent;
btn.addEventListener('click', function () {
navigator.clipboard.writeText(promptText).catch(function () {});
label.textContent = 'Now paste into your agent';
btn.style.background = 'rgba(var(--green-rgb),0.1)';
btn.style.borderColor = 'rgba(var(--green-rgb),0.2)';
btn.style.color = 'var(--green)';
setTimeout(function () {
label.textContent = original;
btn.style.background = '';
btn.style.borderColor = '';
btn.style.color = '';
}, 3000);
});
})();
</script>
Loading
Loading