Skip to content
Open
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
38 changes: 38 additions & 0 deletions .github/workflows/sync-skills-submodule.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Sync Skills Submodule
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
repository_dispatch:
types: [SKILLS_UPDATED]

permissions:
contents: read

jobs:
sync-skills-submodule:
name: Sync Skills Submodule
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Harden Runner
uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df # v2.18.0
with:
egress-policy: audit
- name: Create GitHub App Token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859
id: app-token
with:
app-id: ${{ vars.GH_APP_KONG_DOCS_ID }}
private-key: ${{ secrets.GH_APP_KONG_DOCS_SECRET }}
owner: Kong

- name: Submodule Sync
uses: mheap/submodule-sync-action@a06903a4e38f042f6f52cc88d184ec1c930ee12d # v1
with:
token: ${{ steps.app-token.outputs.token }}
path: app/.repos/skills
ref: main
pr_branch: automated-skills-update
base_branch: main
target_branch: main
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "app/.repos/kuma"]
path = app/.repos/kuma
url = https://github.com/kumahq/kuma-website.git
[submodule "app/.repos/skills"]
path = app/.repos/skills
url = git@github.com:Kong/skills.git
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The skills submodule uses an SSH URL (git@github.com:...), which will fail in environments without an SSH key (CI/Netlify/submodule sync); switch this to an HTTPS URL like the existing kuma submodule.

Suggested change
url = git@github.com:Kong/skills.git
url = https://github.com/Kong/skills.git

Copilot uses AI. Check for mistakes.
1 change: 1 addition & 0 deletions app/.repos/skills
Submodule skills added at 5b93f3
69 changes: 69 additions & 0 deletions app/_assets/entrypoints/skills.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
class SkillsIndex {
constructor() {
this.searchInput = document.getElementById("skills-search");
this.skillsGrid = document.getElementById("skills-grid");
this.emptyState = document.getElementById("skills-empty");
this.cards = this.skillsGrid.querySelectorAll('[data-card="skill"]');

this.searchQuery = "";

this.addEventListeners();
this.preventCopyNavigation();
this.readURL();
this.filterCards();
}

preventCopyNavigation() {
this.skillsGrid.addEventListener("click", (e) => {
if (e.target.closest("clipboard-copy")) {
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e.target isn't guaranteed to be an Element (it can be a text node), so calling e.target.closest(...) can throw and break click handling; check e.target instanceof Element (or use e.composedPath()/closest on a casted element) before calling closest.

Suggested change
if (e.target.closest("clipboard-copy")) {
if (e.target instanceof Element && e.target.closest("clipboard-copy")) {

Copilot uses AI. Check for mistakes.
e.preventDefault();
}
});
}

addEventListeners() {
this.searchInput.addEventListener("input", () => {
this.searchQuery = this.searchInput.value;
this.filterCards();
this.updateURL();
});
}

filterCards() {
const query = this.searchQuery.toLowerCase().trim();
let count = 0;

this.cards.forEach((card) => {
const matchesSearch =
!query ||
card.dataset.title.includes(query) ||
card.dataset.description.includes(query);
card.classList.toggle("hidden", !matchesSearch);
if (matchesSearch) count++;
});

this.emptyState.classList.toggle("hidden", count > 0);
}

updateURL() {
const params = new URLSearchParams();
if (this.searchQuery) {
params.set("q", this.searchQuery);
}
const newUrl =
window.location.pathname +
(params.toString() ? "?" + params.toString() : "");
window.history.replaceState({}, "", newUrl);
}

readURL() {
const params = new URLSearchParams(window.location.search);
const q = params.get("q");
if (q) {
this.searchQuery = q;
this.searchInput.value = q;
}
}
}

document.addEventListener("DOMContentLoaded", () => new SkillsIndex());
25 changes: 23 additions & 2 deletions app/_assets/stylesheets/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@
}

.tab-button__horizontal {
@apply tab-button py-3 text-terciary hover:no-underline items-center max-h-11 flex-shrink-0;
@apply tab-button py-3 text-terciary hover:no-underline items-center max-h-11 flex-shrink-0 gap-1;

&.tab-button__horizontal--active {
@apply tab-button--active border-b-2 pb-[10px] !important;
Expand Down Expand Up @@ -844,6 +844,28 @@
}
}

#skills-grid {
.custom-code-block, .highlight {
pre {
@apply overflow-hidden text-ellipsis;
}
}
}

.skill #description ~ .content {
@apply bg-code-block rounded-md p-4;
}


.skill .heading-section h2 {
@apply border-b border-primary/5 pb-2;
}

.skill table {
@apply block overflow-x-auto;
}


.custom-code-block.collapsible {
[data-code-snippet] {
@apply overflow-hidden relative max-h-80;
Expand Down Expand Up @@ -1007,7 +1029,6 @@
@apply border-brand-saturated border-2;
}


/* Spec Renderer */
.call-button {
@apply button button--secondary hover:!bg-transparent !border-brand;
Expand Down
2 changes: 1 addition & 1 deletion app/_data/schemas/frontmatter/base.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
"content_type": {
"type": "string",
"enum": ["landing_page", "how_to", "reference", "concept", "plugin", "plugin_example", "api", "policy", "support"]
"enum": ["landing_page", "how_to", "reference", "concept", "plugin", "plugin_example", "api", "policy", "support", "skill"]
},
"description": {
"type": "string"
Expand Down
45 changes: 45 additions & 0 deletions app/_includes/cards/skill.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{% assign skill = include.skill %}
{% capture quick_install %}{% include skills/quick_install.md slug=skill.slug %}{% endcapture %}
<div
class="card card__bordered min-h-[220px]"
data-card="skill"
data-title="{{ skill.title | downcase }}"
data-description="{{ skill.description | downcase | truncate: 200 }}"
>
<a href="{{ skill.url }}" class="flex flex-col gap-5 hover:no-underline text-secondary w-full p-6">
<div class="flex items-center justify-between">
<h3 class="font-mono">/{{ skill.title }}</h3>
{% if skill.version %}<span class="badge w-fit text-xs">{{ skill.version }}</span>{% endif %}
</div>

<div class="flex flex-col gap-3 flex-grow">
<p class="text-sm line-clamp-3">{{ skill.description }}</p>
Comment on lines +6 to +16
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skill title/description are injected into HTML attributes and text without escaping; if the upstream skills metadata contains quotes/HTML this can break markup or enable injection—apply HTML escaping (especially for data-* attributes and the heading/paragraph).

Suggested change
data-title="{{ skill.title | downcase }}"
data-description="{{ skill.description | downcase | truncate: 200 }}"
>
<a href="{{ skill.url }}" class="flex flex-col gap-5 hover:no-underline text-secondary w-full p-6">
<div class="flex items-center justify-between">
<h3 class="font-mono">/{{ skill.title }}</h3>
{% if skill.version %}<span class="badge w-fit text-xs">{{ skill.version }}</span>{% endif %}
</div>
<div class="flex flex-col gap-3 flex-grow">
<p class="text-sm line-clamp-3">{{ skill.description }}</p>
data-title="{{ skill.title | downcase | escape }}"
data-description="{{ skill.description | downcase | truncate: 200 | escape }}"
>
<a href="{{ skill.url }}" class="flex flex-col gap-5 hover:no-underline text-secondary w-full p-6">
<div class="flex items-center justify-between">
<h3 class="font-mono">/{{ skill.title | escape }}</h3>
{% if skill.version %}<span class="badge w-fit text-xs">{{ skill.version }}</span>{% endif %}
</div>
<div class="flex flex-col gap-3 flex-grow">
<p class="text-sm line-clamp-3">{{ skill.description | escape }}</p>

Copilot uses AI. Check for mistakes.
</div>

{% if skill.scripts or skill.references or skill.assets %}
<div class="flex gap-3 flex-grow text-brand items-center">
{% if skill.scripts %}
<span class="inline-flex" data-tooltip="Contains scripts">
{% include_svg '/assets/icons/skills/scripts.svg' width="16" height="16" %}
</span>
{% endif %}

{% if skill.references %}
<span class="inline-flex" data-tooltip="Contains references">
{% include_svg '/assets/icons/skills/references.svg' width="16" height="16" %}
</span>
{% endif %}

{% if skill.assets %}
<span class="inline-flex" data-tooltip="Contains assets">
{% include_svg '/assets/icons/skills/assets.svg' width="16" height="16" %}
</span>
{% endif %}
</div>
{% endif %}

<div class="flex flex-col border-t border-primary/5 pt-4">
{{ quick_install | liquify | markdownify }}
</div>
</a>
</div>
1 change: 1 addition & 0 deletions app/_includes/components/tabs.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
{% if forloop.first %} tabindex="0" {% else %} tabindex="-1" {% endif %}
data-slug="{{ slug }}"
>
{% if tab[1].attributes.icon %}{% include_svg tab[1].attributes.icon width="16" height="16" %}{% endif %}
{{ tab[0] | markdownify | markdown }}
</button>
{% endfor %}
Expand Down
77 changes: 77 additions & 0 deletions app/_includes/info_box/skill.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<div class="flex flex-col gap-2 w-full">
<div class="text-primary font-medium">Source</div>
{% assign repo_parts = site.repos.skills | split: 'github.com/' %}
<div class="flex gap-1.5 items-center">
{% include_svg '/assets/icons/github.svg' width="16" height="16" %}
<a href="{{ site.repos.skills }}/tree/main/skills/{{ page.slug }}">{{ repo_parts[1] }}</a>
</div>
</div>

{% if page.version %}
<div class="flex flex-col gap-2 w-full">
<div class="text-primary font-medium">Version</div>
<div class="flex gap-1.5">
{% include badge.html text=page.version %}
</div>
</div>
{% endif %}

{% if page.author %}
<div class="flex flex-col gap-2 w-full">
<div class="text-primary font-medium">Author</div>
<div class="flex gap-1.5">
<span>{{ page.author }}</span>
</div>
</div>
{% endif %}

{% if page.license %}
<div class="flex flex-col gap-2 w-full">
<div class="text-primary font-medium">License</div>
<div class="flex gap-1.5">
{% if page.license_is_file %}
<a href="{{ site.repos.skills }}/blob/main/skills/{{ page.slug }}/{{ page.license }}">{{ page.license }}</a>
{% else %}
<span>{{ page.license }}</span>
{% endif %}
</div>
</div>
{% endif %}

{% if page.allowed_tools %}
<div class="flex flex-col gap-2 w-full">
<div class="text-primary font-medium">Allowed tools</div>
<div class="flex gap-1.5 flex-wrap">
{% assign tools = page.allowed_tools | split: ' ' %}
{% for tool in tools %}
<span>{{ tool }}</span>
{% endfor %}
</div>
</div>
{% endif %}

{% if page.scripts or page.references or page.assets %}
<div class="flex flex-col gap-2 w-full">
<div class="text-primary font-medium">Contains</div>
<div class="flex flex-col gap-1.5 flex-wrap">
{% if page.scripts %}
<a href="{{ site.repos.skills }}/tree/main/skills/{{ page.slug }}/scripts" class="flex items-center gap-2 hover:no-underline">
<div class="flex text-brand shrink-0">{% include_svg '/assets/icons/skills/scripts.svg' width="16" height="16" %}</div>
<span>Scripts</span>
</a>
{% endif %}
{% if page.references %}
<a href="{{ site.repos.skills }}/tree/main/skills/{{ page.slug }}/references" class="flex items-center gap-2 hover:no-underline">
<div class="flex text-brand shrink-0">{% include_svg '/assets/icons/skills/references.svg' width="16" height="16" %}</div>
<span>References</span>
</a>
{% endif %}
{% if page.assets %}
<a href="{{ site.repos.skills }}/tree/main/skills/{{ page.slug }}/assets" class="flex items-center gap-2 hover:no-underline">
<div class="flex text-brand shrink-0">{% include_svg '/assets/icons/skills/assets.svg' width="16" height="16" %}</div>
<span>Assets</span>
</a>
{% endif %}
</div>
</div>
{% endif %}
9 changes: 9 additions & 0 deletions app/_includes/skills/install.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% if site.data.skill_install_tabs.size > 0 %}
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

site.data.skill_install_tabs can be nil (for example when skip.skills is true or the skills repo isn't present), so calling .size here can raise during site build; guard for nil or default it to an empty array before checking size.

Suggested change
{% if site.data.skill_install_tabs.size > 0 %}
{% if site.data.skill_install_tabs and site.data.skill_install_tabs.size > 0 %}

Copilot uses AI. Check for mistakes.
{% navtabs "tools" heading_level=2 %}
{% for tab in site.data.skill_install_tabs %}
{% navtab {{ tab.title }} slug={{ tab.slug }} icon={{ tab.icon }} %}
{{ tab.content }}
{% endnavtab %}
{% endfor %}
{% endnavtabs %}
{% endif %}
11 changes: 11 additions & 0 deletions app/_includes/skills/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## Installation

{% include skills/quick_install.md slug=page.slug %}

## Description

{{ page.description }}

## SKILL.md

{{ page.skill_content }}
3 changes: 3 additions & 0 deletions app/_includes/skills/quick_install.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```bash
npx skills@latest add {{site.repos.skills | remove: "https://github.com/" | remove_last: "/"}}{% if include.slug%} --skill {{ include.slug }}{% endif %}
```
4 changes: 4 additions & 0 deletions app/_layouts/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@
{% vite_javascript_tag hub %}
{% endif %}

{% if page.skills_index %}
{% vite_javascript_tag skills %}
{% endif %}

{% if page.search %}
{% vite_javascript_tag search %}
{% endif %}
Expand Down
10 changes: 10 additions & 0 deletions app/_layouts/skill.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
layout: with_aside
uses: false
---

{{ content }}

{% contentfor info_box %}
{% include info_box/skill.html %}
{% endcontentfor %}
3 changes: 2 additions & 1 deletion app/_plugins/converters/shiki.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ class CodeHighlighter < Nodo::Core # rubocop:disable Style/Documentation
"nginx",
"html",
"ruby",
"ansi"
"ansi",
"markdown",
],
});
JS
Expand Down
3 changes: 2 additions & 1 deletion app/_plugins/generators/data/search_tags/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class Base # rubocop:disable Style/Documentation
'plugin_example' => 'PluginExample',
'reference' => 'Reference',
'policy' => 'Policy',
'support' => 'Support'
'support' => 'Support',
'skill' => 'Reference'
}.freeze

def self.make_for(site:, page:)
Expand Down
12 changes: 12 additions & 0 deletions app/_plugins/generators/skills.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

module Jekyll
class SkillsGenerator < Jekyll::Generator
priority :high

def generate(site)
site.data['skills'] ||= {}
Jekyll::SkillPages::Generator.run(site)
end
end
end
Loading
Loading