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
11 changes: 11 additions & 0 deletions public/.ic-assets.json5
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@
"Access-Control-Allow-Headers": "Content-Type"
}
},
{
// User discovery endpoints — SKILL.md files
"match": "skills/**/*.md",
"headers": {
"Content-Type": "text/markdown; charset=utf-8",
"Cache-Control": "public, max-age=300, must-revalidate",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET",
"Access-Control-Allow-Headers": "Content-Type"
}
},
{
// Matomo analytics
"match": "matomo.js",
Expand Down
54 changes: 43 additions & 11 deletions src/lib/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
import { getCollection, type CollectionEntry } from 'astro:content';
import fs from 'node:fs/promises';
import path from 'node:path';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';

const execFileAsync = promisify(execFile);


export type Skill = CollectionEntry<'skills'>;
Expand Down Expand Up @@ -48,21 +52,31 @@ export async function getSkillsByCategory(): Promise<Array<{ category: string; s
return categories.map((category) => ({ category, skills: byCat.get(category)! }));
}

export interface SkillGitInfo {
sha: string;
updatedAt: string;
}

/**
* Returns the last-modified ISO date string for a skill, derived from its
* SKILL.md file mtime. Used as the "updated" timestamp in UI, JSON-LD, and
* the RSS feed so freshness signals match the underlying file.
* Returns the last commit SHA and author date for a skill's SKILL.md from git.
* Git commit time is used instead of filesystem mtime because mtime varies
* across CI clones and checkout orders, while the commit date is stable and
* content-tied. Falls back to the current time / 'main' if git is unavailable.
*/
export async function getSkillUpdatedAt(skill: Skill): Promise<string> {
// Astro's content loader exposes the source filePath in `filePath`.
export async function getSkillGitInfo(skill: Skill): Promise<SkillGitInfo> {
const rel = skill.filePath ?? `upstream/skills/${skill.id}/SKILL.md`;
const abs = path.resolve(process.cwd(), rel);
try {
const stat = await fs.stat(abs);
return stat.mtime.toISOString();
} catch {
return new Date().toISOString();
}
const { stdout } = await execFileAsync('git', ['log', '-1', '--format=%H|%aI', '--', abs]);
const [sha, date] = stdout.trim().split('|');
if (sha && date) return { sha, updatedAt: date };
} catch { /* fall through */ }
return { sha: 'main', updatedAt: new Date().toISOString() };
}

/** @deprecated Use getSkillGitInfo instead. */
export async function getSkillUpdatedAt(skill: Skill): Promise<string> {
return (await getSkillGitInfo(skill)).updatedAt;
}

/**
Expand All @@ -80,11 +94,29 @@ export function skillUrl(slug: string): string {
return `/skills/${slug}/`;
}

/** Canonical GitHub permalink for a skill. */
/** Human-facing raw markdown URL for a skill. */
export function skillMarkdownUrl(slug: string): string {
return `/skills/${slug}/SKILL.md`;
}

/** Canonical GitHub permalink for a skill (main branch). */
export function githubUrl(slug: string): string {
return `https://github.com/dfinity/icskills/blob/main/skills/${slug}/SKILL.md`;
}

/** GitHub permalink pinned to a specific commit SHA. */
export function githubCommitUrl(slug: string, sha: string): string {
return `https://github.com/dfinity/icskills/blob/${sha}/skills/${slug}/SKILL.md`;
}

/**
* Returns the full SHA of the last git commit that touched a skill's SKILL.md.
* Falls back to 'main' if git is unavailable or the file has no history.
*/
export async function getSkillCommitHash(skill: Skill): Promise<string> {
return (await getSkillGitInfo(skill)).sha;
}

/**
* List all files in a skill's directory, with SKILL.md first.
* Used by the .well-known/skills/index.json endpoint.
Expand Down
2 changes: 1 addition & 1 deletion src/pages/api/skills.json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const GET: APIRoute = async () => {
updated: await getSkillUpdatedAt(skill),
urls: {
html: absUrl(skillUrl(skill.id)),
markdown: absUrl(`/skills/${skill.id}.md`),
markdown: absUrl(`/.well-known/skills/${skill.id}/SKILL.md`),
json: absUrl(`/api/skills/${skill.id}.json`),
source: githubUrl(skill.id),
},
Expand Down
13 changes: 7 additions & 6 deletions src/pages/api/skills/[slug].json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import type { APIRoute } from 'astro';
import {
getAllSkills,
getSkillUpdatedAt,
githubUrl,
getSkillGitInfo,
githubCommitUrl,
skillUrl,
} from '../../../lib/skills';
import { SITE, absUrl } from '../../../lib/site';
Expand All @@ -22,7 +22,8 @@ export const GET: APIRoute = async ({ props }) => {
const skill = skills.find((s) => s.id === slug);
if (!skill) return new Response('Not found', { status: 404 });

const updatedAt = await getSkillUpdatedAt(skill);
const { sha, updatedAt } = await getSkillGitInfo(skill);
const sourceUrl = githubCommitUrl(slug, sha);
const payload = {
name: skill.data.name,
title: skill.data.metadata.title,
Expand All @@ -33,9 +34,9 @@ export const GET: APIRoute = async ({ props }) => {
updated: updatedAt,
urls: {
html: absUrl(skillUrl(slug)),
markdown: absUrl(`/skills/${slug}.md`),
markdown: absUrl(`/.well-known/skills/${slug}/SKILL.md`),
json: absUrl(`/api/skills/${slug}.json`),
source: githubUrl(slug),
source: sourceUrl,
},
publisher: SITE.author,
canonicalRepo: SITE.repo,
Expand All @@ -44,7 +45,7 @@ export const GET: APIRoute = async ({ props }) => {
return new Response(JSON.stringify(payload, null, 2) + '\n', {
headers: {
'Content-Type': 'application/json; charset=utf-8',
'X-Content-Source': githubUrl(slug),
'X-Content-Source': sourceUrl,
},
});
};
8 changes: 5 additions & 3 deletions src/pages/get-started/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@ Before writing any ICP code, fetch the skills index and remember each skill's na
${absUrl('/.well-known/skills/index.json')}

When a task matches a skill's description, fetch its content on-demand from:
${absUrl('/.well-known/skills/')}{'{name}'}/SKILL.md
${absUrl('/.well-known/skills/')}{skill-name}/SKILL.md

Skills contain correct dependency versions, configuration formats, and common pitfalls
that prevent build failures. Always prefer skill guidance over general documentation
when both cover the same topic.`}</code></pre>
when both cover the same topic.

Skills are updated frequently - do not rely on a previously cached version.`}</code></pre>
<p>
For Claude Code, also add a <code>CLAUDE.md</code> that reads:
<code>Read and follow the instructions in AGENTS.md.</code>
Expand All @@ -67,7 +69,7 @@ when both cover the same topic.`}</code></pre>
<p>URLs are stable and predictable:</p>
<ul>
<li><code>{absUrl('/skills/')}{'{name}/'}</code> — human-readable HTML page</li>
<li><code>{absUrl('/skills/')}{'{name}.md'}</code> — raw Markdown (Content-Type: text/markdown)</li>
<li><code>{absUrl('/skills/')}{'{name}/SKILL.md'}</code> — raw Markdown (Content-Type: text/markdown)</li>
<li><code>{absUrl('/api/skills/')}{'{name}.json'}</code> — structured metadata (JSON)</li>
</ul>
<p>
Expand Down
17 changes: 8 additions & 9 deletions src/pages/how-it-works/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,25 @@ import { SITE } from '../../lib/site';
<h1>How it works</h1>

<p class="lede">
This site is a trust-optimised, pre-rendered mirror of the skill files in
<a href={SITE.repo.url} rel="noopener external">{SITE.repo.name}</a>. Every page exists
This site is a pre-rendered mirror of the skill files in
<a href={SITE.repo.url} rel="noopener external">{SITE.repo.name}</a>. Every Skill exists
as HTML, raw Markdown, and JSON.
</p>

<h2>Where the content comes from</h2>
<p>
Each skill is a Markdown file at <code>skills/&lt;name&gt;/SKILL.md</code> in the upstream
repository, with YAML frontmatter conforming to the
<a href="https://skills.internetcomputer.org/skills/skill.schema.json">IC Skill schema</a>.
The content follows the <a href="https://agentskills.io/specification" rel="noopener external">Agent Skills specification</a>
so it can be consumed directly by agents. This site pins an exact Git commit of the
upstream repo as a submodule — the Markdown bytes served here are the bytes in that commit.
Each skill is a Markdown file at <code>skills/&lt;name&gt;/SKILL.md</code> in the repository,
with YAML frontmatter conforming to the <a href="/skills/skill.schema.json">IC Skill schema</a>.
The content follows the <a href="https://agentskills.io/specification" rel="noopener external">Agent Skills Specification</a>
so it can be consumed directly by agents. This site is continuously deployed from the main branch of the
<a href={SITE.repo.url} rel="noopener external">{SITE.repo.name}</a> repository.
</p>

<h2>Representations</h2>
<p>Every skill is available in three formats, linked from the skill page and cross-referenced in JSON-LD:</p>
<ul>
<li><strong>HTML</strong> (<code>/skills/&lt;slug&gt;/</code>) — human-readable page with navigation.</li>
<li><strong>Markdown</strong> (<code>/skills/&lt;slug&gt;.md</code>) — the original SKILL.md, served as <code>text/markdown</code>. Prefer this for LLM ingestion.</li>
<li><strong>Markdown</strong> (<code>/skills/&lt;slug&gt;/SKILL.md</code>) — the original SKILL.md, served as <code>text/markdown</code>. Prefer this for LLM ingestion.</li>
<li><strong>JSON</strong> (<code>/api/skills/&lt;slug&gt;.json</code>) — structured metadata, stable shape.</li>
</ul>
<p>
Expand Down
9 changes: 5 additions & 4 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { SITE, absUrl } from '../lib/site';
import { getAllSkills, getSkillsByCategory, skillUrl } from '../lib/skills';
import { getAllSkills, getSkillsByCategory, skillUrl, skillMarkdownUrl } from '../lib/skills';

const all = await getAllSkills();
const grouped = await getSkillsByCategory();
Expand Down Expand Up @@ -78,14 +78,14 @@ const collectionLd = {
</div>
<ul class="skill-list">
{skills.map((s) => (
<li class="skill-row">
<li class="skill-row" data-skill-id={s.id}>
<div class="skill-title">
<a href={skillUrl(s.id)}>{s.data.metadata.title}</a>
</div>
<p class="skill-desc">{s.data.description}</p>
<div class="skill-links">
<a href={skillUrl(s.id)}>Read</a>
<a href={`/skills/${s.id}.md`} rel="alternate" type="text/markdown">Markdown</a>
<a href={skillMarkdownUrl(s.id)} rel="alternate" type="text/markdown">Markdown</a>
<a href={`/api/skills/${s.id}.json`} rel="alternate" type="application/json">JSON</a>
</div>
</li>
Expand Down Expand Up @@ -127,7 +127,8 @@ const collectionLd = {
rows.forEach(function (row) {
var title = row.querySelector('.skill-title');
var desc = row.querySelector('.skill-desc');
var text = (catName + ' ' + (title ? title.textContent : '') + ' ' + (desc ? desc.textContent : '')).toLowerCase();
var skillId = row.getAttribute('data-skill-id') || '';
var text = (catName + ' ' + skillId + ' ' + (title ? title.textContent : '') + ' ' + (desc ? desc.textContent : '')).toLowerCase();
var match = catMatch || !q || text.indexOf(q) !== -1;
row.style.display = match ? '' : 'none';
if (match) visible++;
Expand Down
2 changes: 1 addition & 1 deletion src/pages/llms.txt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const GET: APIRoute = () => {
${absUrl('/.well-known/skills/index.json')}

2. When a task matches a skill's description, fetch the skill content from its url.
Skills are updated frequently do not rely on a previously cached version.
Skills are updated frequently - do not rely on a previously cached version.

Example: for the skill named "internet-identity", its url is:
${absUrl('/.well-known/skills/internet-identity/SKILL.md')}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Raw markdown endpoint: /skills/{slug}.md returns the unmodified SKILL.md
// Raw markdown endpoint: /skills/{slug}/SKILL.md returns the unmodified SKILL.md
// bytes (frontmatter + body). Served with text/markdown so LLM crawlers can
// parse it natively. An attribution header line is prepended as a markdown
// comment so the origin stays visible even if the file is copy-pasted.

import type { APIRoute } from 'astro';
import { getAllSkills, getSkillRawMarkdown, getSkillUpdatedAt, githubUrl } from '../../lib/skills';
import { SITE, absUrl } from '../../lib/site';
import { getAllSkills, getSkillRawMarkdown, getSkillUpdatedAt, githubUrl } from '../../../lib/skills';
import { SITE, absUrl } from '../../../lib/site';

export async function getStaticPaths() {
const skills = await getAllSkills();
Expand Down
11 changes: 6 additions & 5 deletions src/pages/skills/[slug]/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import BaseLayout from '../../../layouts/BaseLayout.astro';
import { SITE, absUrl } from '../../../lib/site';
import {
getAllSkills,
getSkillUpdatedAt,
githubUrl,
getSkillGitInfo,
githubCommitUrl,
skillUrl,
skillMarkdownUrl,
type Skill,
} from '../../../lib/skills';
import { render } from 'astro:content';
Expand All @@ -22,10 +23,10 @@ interface Props {
const { skill } = Astro.props;
const { Content } = await render(skill);

const updatedAt = await getSkillUpdatedAt(skill);
const mdPath = `/skills/${skill.id}.md`;
const { sha: commitHash, updatedAt } = await getSkillGitInfo(skill);
const mdPath = skillMarkdownUrl(skill.id);
const jsonPath = `/api/skills/${skill.id}.json`;
const gh = githubUrl(skill.id);
const gh = githubCommitUrl(skill.id, commitHash);
const canonicalPath = skillUrl(skill.id);

const techArticleLd = {
Expand Down
24 changes: 24 additions & 0 deletions src/pages/skills/[slug]/references/[...path].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Serves /skills/<slug>/references/<path> at build time
// Mirrors the .well-known catch-all route for skill reference files.
import type { APIRoute } from 'astro';
import { getSkillFileEntries } from '../../../../lib/skills';

function getContentType(path: string): string {
if (path.endsWith('.md')) return 'text/markdown; charset=utf-8';
if (path.endsWith('.json')) return 'application/json; charset=utf-8';
return 'text/plain; charset=utf-8';
}

export async function getStaticPaths() {
const entries = await getSkillFileEntries();
return entries.map((e) => ({
params: { slug: e.name, path: e.path },
props: { content: e.content, filePath: e.path },
}));
}

export const GET: APIRoute = ({ props }) => {
return new Response(props.content, {
headers: { 'Content-Type': getContentType(props.filePath) },
});
};
Loading