diff --git a/.github/workflows/sync-skills-submodule.yml b/.github/workflows/sync-skills-submodule.yml
new file mode 100644
index 0000000000..bfb2a594ef
--- /dev/null
+++ b/.github/workflows/sync-skills-submodule.yml
@@ -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
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
index 9b8457e7be..ca0309d8fa 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -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
diff --git a/app/.repos/skills b/app/.repos/skills
new file mode 160000
index 0000000000..5b93f3ac27
--- /dev/null
+++ b/app/.repos/skills
@@ -0,0 +1 @@
+Subproject commit 5b93f3ac270eeecc11f579234d08619d2bff817e
diff --git a/app/_assets/entrypoints/skills.js b/app/_assets/entrypoints/skills.js
new file mode 100644
index 0000000000..f7743b5995
--- /dev/null
+++ b/app/_assets/entrypoints/skills.js
@@ -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")) {
+ 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());
diff --git a/app/_assets/stylesheets/index.css b/app/_assets/stylesheets/index.css
index 6533c757ae..0a2c6783c8 100644
--- a/app/_assets/stylesheets/index.css
+++ b/app/_assets/stylesheets/index.css
@@ -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;
@@ -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;
@@ -1007,7 +1029,6 @@
@apply border-brand-saturated border-2;
}
-
/* Spec Renderer */
.call-button {
@apply button button--secondary hover:!bg-transparent !border-brand;
diff --git a/app/_data/schemas/frontmatter/base.json b/app/_data/schemas/frontmatter/base.json
index 7d8f9b21c0..70d86998e3 100644
--- a/app/_data/schemas/frontmatter/base.json
+++ b/app/_data/schemas/frontmatter/base.json
@@ -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"
diff --git a/app/_includes/cards/skill.html b/app/_includes/cards/skill.html
new file mode 100644
index 0000000000..4106e6e30b
--- /dev/null
+++ b/app/_includes/cards/skill.html
@@ -0,0 +1,45 @@
+{% assign skill = include.skill %}
+{% capture quick_install %}{% include skills/quick_install.md slug=skill.slug %}{% endcapture %}
+
diff --git a/app/_includes/components/tabs.html b/app/_includes/components/tabs.html
index 09adde8079..41eebfa39e 100644
--- a/app/_includes/components/tabs.html
+++ b/app/_includes/components/tabs.html
@@ -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 }}
{% endfor %}
diff --git a/app/_includes/info_box/skill.html b/app/_includes/info_box/skill.html
new file mode 100644
index 0000000000..2aba2b58d0
--- /dev/null
+++ b/app/_includes/info_box/skill.html
@@ -0,0 +1,77 @@
+
+
Source
+ {% assign repo_parts = site.repos.skills | split: 'github.com/' %}
+
+
+
+{% if page.version %}
+
+
Version
+
+ {% include badge.html text=page.version %}
+
+
+{% endif %}
+
+{% if page.author %}
+
+
Author
+
+ {{ page.author }}
+
+
+{% endif %}
+
+{% if page.license %}
+
+
License
+
+ {% if page.license_is_file %}
+
{{ page.license }}
+ {% else %}
+
{{ page.license }}
+ {% endif %}
+
+
+{% endif %}
+
+{% if page.allowed_tools %}
+
+
Allowed tools
+
+ {% assign tools = page.allowed_tools | split: ' ' %}
+ {% for tool in tools %}
+ {{ tool }}
+ {% endfor %}
+
+
+{% endif %}
+
+{% if page.scripts or page.references or page.assets %}
+
+{% endif %}
diff --git a/app/_includes/skills/install.md b/app/_includes/skills/install.md
new file mode 100644
index 0000000000..f2b17416b3
--- /dev/null
+++ b/app/_includes/skills/install.md
@@ -0,0 +1,9 @@
+{% if site.data.skill_install_tabs.size > 0 %}
+{% 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 %}
diff --git a/app/_includes/skills/overview.md b/app/_includes/skills/overview.md
new file mode 100644
index 0000000000..1205123303
--- /dev/null
+++ b/app/_includes/skills/overview.md
@@ -0,0 +1,11 @@
+## Installation
+
+{% include skills/quick_install.md slug=page.slug %}
+
+## Description
+
+{{ page.description }}
+
+## SKILL.md
+
+{{ page.skill_content }}
diff --git a/app/_includes/skills/quick_install.md b/app/_includes/skills/quick_install.md
new file mode 100644
index 0000000000..b0ec347450
--- /dev/null
+++ b/app/_includes/skills/quick_install.md
@@ -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 %}
+```
\ No newline at end of file
diff --git a/app/_layouts/default.html b/app/_layouts/default.html
index 78d296ac8e..8f80731de6 100644
--- a/app/_layouts/default.html
+++ b/app/_layouts/default.html
@@ -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 %}
diff --git a/app/_layouts/skill.html b/app/_layouts/skill.html
new file mode 100644
index 0000000000..d31cb656e1
--- /dev/null
+++ b/app/_layouts/skill.html
@@ -0,0 +1,10 @@
+---
+layout: with_aside
+uses: false
+---
+
+{{ content }}
+
+{% contentfor info_box %}
+ {% include info_box/skill.html %}
+{% endcontentfor %}
diff --git a/app/_plugins/converters/shiki.rb b/app/_plugins/converters/shiki.rb
index 26e1374c48..348d49511c 100644
--- a/app/_plugins/converters/shiki.rb
+++ b/app/_plugins/converters/shiki.rb
@@ -28,7 +28,8 @@ class CodeHighlighter < Nodo::Core # rubocop:disable Style/Documentation
"nginx",
"html",
"ruby",
- "ansi"
+ "ansi",
+ "markdown",
],
});
JS
diff --git a/app/_plugins/generators/data/search_tags/base.rb b/app/_plugins/generators/data/search_tags/base.rb
index 96d1c628fc..fef133b9d1 100644
--- a/app/_plugins/generators/data/search_tags/base.rb
+++ b/app/_plugins/generators/data/search_tags/base.rb
@@ -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:)
diff --git a/app/_plugins/generators/skills.rb b/app/_plugins/generators/skills.rb
new file mode 100644
index 0000000000..8d700165ea
--- /dev/null
+++ b/app/_plugins/generators/skills.rb
@@ -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
diff --git a/app/_plugins/generators/skills/discovery.rb b/app/_plugins/generators/skills/discovery.rb
new file mode 100644
index 0000000000..ef47bd967a
--- /dev/null
+++ b/app/_plugins/generators/skills/discovery.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'json'
+
+module Jekyll
+ module SkillPages
+ class Discovery
+ def self.generate(site, skills)
+ new(site, skills).run
+ end
+
+ def initialize(site, skills)
+ @site = site
+ @skills = skills
+ end
+
+ def run
+ return if @skills.empty?
+
+ index_entries = @skills.map do |skill|
+ generate_skill_pages(@site, skill)
+ {
+ 'name' => skill.slug,
+ 'description' => skill.description,
+ 'files' => skill.all_files
+ }
+ end
+
+ generate_index(@site, index_entries)
+ end
+
+ def generate_skill_pages(site, skill)
+ skill.all_files.each do |relative_path|
+ dir = File.join(well_known_dir, skill.slug, File.dirname(relative_path))
+ dir = File.join(well_known_dir, skill.slug) if File.dirname(relative_path) == '.'
+ filename = File.basename(relative_path)
+
+ site.static_files << Pages::StaticSkillFile.new(
+ site, skill.folder, dir, filename, relative_path
+ )
+ end
+ end
+
+ def generate_index(site, entries)
+ page = PageWithoutAFile.new(site, site.source, well_known_dir, 'index.json')
+ page.data['llm'] = false
+ page.data['layout'] = nil
+ page.content = JSON.pretty_generate({ 'skills' => entries })
+ site.pages << page
+ end
+
+ def well_known_dir
+ @well_known_dir ||= @site.config.dig('well-known', 'skills')
+ end
+ end
+ end
+end
diff --git a/app/_plugins/generators/skills/generator.rb b/app/_plugins/generators/skills/generator.rb
new file mode 100644
index 0000000000..50e1998274
--- /dev/null
+++ b/app/_plugins/generators/skills/generator.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+module Jekyll
+ module SkillPages
+ def self.skills_repo_path(site)
+ ENV['SKILLS_REPO'] || site.config['skills_repo_path'] || 'kong-skills'
+ end
+
+ def self.demote_headings(text)
+ text.lines.filter_map do |line|
+ next nil if line.match?(/^# [^#]/)
+
+ line.match?(/^##/) ? "##{line}" : line
+ end.join
+ end
+
+ class Generator
+ def self.run(site)
+ new(site).run
+ end
+
+ attr_reader :site
+
+ def initialize(site)
+ @site = site
+ @skills = []
+ @repo_path = Jekyll::SkillPages.skills_repo_path(site)
+ end
+
+ def run
+ return if site.config.dig('skip', 'skills')
+
+ load_install_tabs(base_dir)
+ return unless Dir.exist?(skills_path)
+
+ Dir.glob(File.join(skills_path, '*/')).each do |folder|
+ skill = Jekyll::SkillPages::Skill.new(folder:, slug: File.basename(folder))
+ skill.metadata # force read to fail fast
+ @skills << skill
+ generate_overview_page(skill)
+ rescue Errno::ENOENT
+ next
+ end
+
+ Jekyll::SkillPages::Discovery.generate(site, @skills)
+ end
+
+ private
+
+ INSTALL_EXCLUDES = %w[README.md].freeze
+
+ def base_dir
+ @base_dir ||= File.expand_path('..', site.source)
+ end
+
+ def skills_path
+ @skills_path ||= File.join(base_dir, @repo_path, 'skills')
+ end
+
+ def load_install_tabs(base_dir)
+ install_path = File.join(base_dir, @repo_path, 'docs', 'install')
+ return unless Dir.exist?(install_path)
+
+ site.data['skill_install_tabs'] = Dir.glob(File.join(install_path, '*.md'))
+ .reject { |f| INSTALL_EXCLUDES.include?(File.basename(f)) }
+ .map { |f| parse_install_file(f) }
+ .sort_by { |tab| tab['title'] }
+ end
+
+ def parse_install_file(file)
+ raw = File.read(file)
+ slug = File.basename(file, '.md')
+
+ title = raw.lines.first&.match(/^#\s+(.+)/)&.captures&.first || slug
+
+ processed = Jekyll::SkillPages.demote_headings(raw)
+
+ repo_url = site.config.dig('repos', 'skills')
+ processed = processed.gsub(%r{\]\(\.\./\.\./(.*?)\)}, "](#{repo_url}/blob/main/\\1)") if repo_url
+
+ { 'title' => title.strip, 'slug' => slug, 'icon' => "/assets/icons/ai-tools/#{slug}.svg",
+ 'content' => processed }
+ end
+
+ def generate_overview_page(skill)
+ overview = Jekyll::SkillPages::Pages::Overview
+ .new(skill:)
+ .to_jekyll_page
+
+ site.data['skills'][skill.slug] = overview
+ site.pages << overview
+ end
+ end
+ end
+end
diff --git a/app/_plugins/generators/skills/pages/overview.rb b/app/_plugins/generators/skills/pages/overview.rb
new file mode 100644
index 0000000000..5a59d69c6c
--- /dev/null
+++ b/app/_plugins/generators/skills/pages/overview.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require_relative '../../../lib/site_accessor'
+
+module Jekyll
+ module SkillPages
+ module Pages
+ class Overview
+ include Jekyll::SiteAccessor
+
+ def self.url(skill)
+ "/skills/#{skill.slug}/"
+ end
+
+ def initialize(skill:)
+ @skill = skill
+ end
+
+ def to_jekyll_page
+ CustomJekyllPage.new(site:, page: self)
+ end
+
+ TEMPLATE = File.read('app/_includes/skills/overview.md')
+
+ def content
+ TEMPLATE
+ end
+
+ def dir
+ url
+ end
+
+ def url
+ @url ||= self.class.url(@skill)
+ end
+
+ def data
+ {
+ 'title' => @skill.name,
+ 'description' => @skill.description,
+ 'version' => @skill.version,
+ 'author' => @skill.author,
+ 'slug' => @skill.slug,
+ 'layout' => 'skill',
+ 'breadcrumbs' => ['/skills/'],
+ 'no_edit_link' => true,
+ 'content_type' => 'skill',
+ 'license' => @skill.license,
+ 'license_is_file' => @skill.license_file?,
+ 'allowed_tools' => @skill.allowed_tools,
+ 'scripts' => @skill.scripts?,
+ 'references' => @skill.references?,
+ 'assets' => @skill.assets?,
+ 'skill_content' => @skill.processed_content,
+ 'products' => @skill.products,
+ 'llm' => false,
+ 'toc' => false
+ }
+ end
+
+ def relative_path
+ @relative_path ||= File.join(
+ Jekyll::SkillPages.skills_repo_path(site), 'skills', @skill.slug, 'SKILL.md'
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/app/_plugins/generators/skills/pages/static_skill_file.rb b/app/_plugins/generators/skills/pages/static_skill_file.rb
new file mode 100644
index 0000000000..233a288737
--- /dev/null
+++ b/app/_plugins/generators/skills/pages/static_skill_file.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Jekyll
+ module SkillPages
+ module Pages
+ class StaticSkillFile < Jekyll::StaticFile
+ # A StaticFile subclass that reads from the skill folder
+ # but writes to the .well-known/skills/ output directory.
+ def initialize(site, skill_folder, dest_dir, name, relative_path)
+ super(site, skill_folder, dest_dir, name)
+ @source_path = File.join(skill_folder, relative_path)
+ end
+
+ def path
+ @source_path
+ end
+ end
+ end
+ end
+end
diff --git a/app/_plugins/generators/skills/skill.rb b/app/_plugins/generators/skills/skill.rb
new file mode 100644
index 0000000000..6c8a09c8a7
--- /dev/null
+++ b/app/_plugins/generators/skills/skill.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'digest'
+
+module Jekyll
+ module SkillPages
+ class Skill
+ attr_reader :folder, :slug
+
+ def initialize(folder:, slug:)
+ @folder = folder
+ @slug = slug
+ end
+
+ def name
+ @name ||= metadata.fetch('name', slug)
+ end
+
+ def description
+ @description ||= metadata.fetch('description', '')
+ end
+
+ def version
+ @version ||= metadata.dig('metadata', 'version')
+ end
+
+ def author
+ @author ||= metadata.dig('metadata', 'author')
+ end
+
+ def license
+ @license ||= metadata.fetch('license', nil)
+ end
+
+ def products
+ @products ||= metadata.dig('metadata', 'products') || []
+ end
+
+ def license_file?
+ return false unless license
+
+ File.exist?(File.join(@folder, license))
+ end
+
+ def allowed_tools
+ @allowed_tools ||= metadata.fetch('allowed-tools', nil)
+ end
+
+ def scripts?
+ Dir.exist?(File.join(@folder, 'scripts'))
+ end
+
+ def references?
+ Dir.exist?(File.join(@folder, 'references'))
+ end
+
+ def assets?
+ Dir.exist?(File.join(@folder, 'assets'))
+ end
+
+ def all_files
+ @all_files ||= Dir.glob(File.join(@folder, '**', '*'))
+ .select { |f| File.file?(f) }
+ .map { |f| f.sub(@folder, '') }
+ .sort
+ end
+
+ def raw_content
+ @raw_content ||= File.read(File.join(@folder, 'SKILL.md'))
+ end
+
+ def content
+ @content ||= parser.content
+ end
+
+ def processed_content
+ @processed_content ||= Jekyll::SkillPages.demote_headings(content)
+ end
+
+ def metadata
+ @metadata ||= parser.frontmatter
+ end
+
+ private
+
+ def parser
+ @parser ||= Jekyll::Utils::MarkdownParser.new(raw_content)
+ end
+ end
+ end
+end
diff --git a/app/_plugins/hooks/split_into_sections.rb b/app/_plugins/hooks/split_into_sections.rb
index 9eee1c186e..968698ac10 100644
--- a/app/_plugins/hooks/split_into_sections.rb
+++ b/app/_plugins/hooks/split_into_sections.rb
@@ -7,7 +7,7 @@
end
Jekyll::Hooks.register :pages, :post_convert do |page, _payload|
- next unless %w[concept reference plugin policy plugin_example].include?(page.data['content_type'])
+ next unless %w[concept reference plugin policy plugin_example skill].include?(page.data['content_type'])
SectionWrapper::Base.make_for(page).process
end
diff --git a/app/assets/icons/ai-tools/claude-code.svg b/app/assets/icons/ai-tools/claude-code.svg
new file mode 100644
index 0000000000..1e37042806
--- /dev/null
+++ b/app/assets/icons/ai-tools/claude-code.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icons/ai-tools/codex.svg b/app/assets/icons/ai-tools/codex.svg
new file mode 100644
index 0000000000..216aeb0011
--- /dev/null
+++ b/app/assets/icons/ai-tools/codex.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icons/ai-tools/cursor.svg b/app/assets/icons/ai-tools/cursor.svg
new file mode 100644
index 0000000000..4178ff0fdc
--- /dev/null
+++ b/app/assets/icons/ai-tools/cursor.svg
@@ -0,0 +1,5 @@
+
diff --git a/app/assets/icons/ai-tools/gemini-cli.svg b/app/assets/icons/ai-tools/gemini-cli.svg
new file mode 100644
index 0000000000..06f577c0d1
--- /dev/null
+++ b/app/assets/icons/ai-tools/gemini-cli.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icons/ai-tools/github-copilot.svg b/app/assets/icons/ai-tools/github-copilot.svg
new file mode 100644
index 0000000000..4d9875d432
--- /dev/null
+++ b/app/assets/icons/ai-tools/github-copilot.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/assets/icons/ai-tools/goose.svg b/app/assets/icons/ai-tools/goose.svg
new file mode 100644
index 0000000000..7adcb6c7a0
--- /dev/null
+++ b/app/assets/icons/ai-tools/goose.svg
@@ -0,0 +1,5 @@
+
diff --git a/app/assets/icons/ai-tools/other-tools.svg b/app/assets/icons/ai-tools/other-tools.svg
new file mode 100644
index 0000000000..0b1ab894c7
--- /dev/null
+++ b/app/assets/icons/ai-tools/other-tools.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icons/goose.svg b/app/assets/icons/goose.svg
new file mode 100644
index 0000000000..6388c314b4
--- /dev/null
+++ b/app/assets/icons/goose.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icons/other-tools.svg b/app/assets/icons/other-tools.svg
new file mode 100644
index 0000000000..57625a7c98
--- /dev/null
+++ b/app/assets/icons/other-tools.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icons/search.svg b/app/assets/icons/search.svg
new file mode 100644
index 0000000000..a460c53f94
--- /dev/null
+++ b/app/assets/icons/search.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icons/skills/assets.svg b/app/assets/icons/skills/assets.svg
new file mode 100644
index 0000000000..ce94c697ab
--- /dev/null
+++ b/app/assets/icons/skills/assets.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icons/skills/references.svg b/app/assets/icons/skills/references.svg
new file mode 100644
index 0000000000..3b4383a5a1
--- /dev/null
+++ b/app/assets/icons/skills/references.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/icons/skills/scripts.svg b/app/assets/icons/skills/scripts.svg
new file mode 100644
index 0000000000..8ef5e4fdc1
--- /dev/null
+++ b/app/assets/icons/skills/scripts.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/skills/index.html b/app/skills/index.html
new file mode 100644
index 0000000000..c9bbb7846f
--- /dev/null
+++ b/app/skills/index.html
@@ -0,0 +1,53 @@
+---
+title: Kong Skills Hub
+skills_index: true
+layout: default
+edit_and_issue_links: false
+llm: false
+description: Browse and search Kong skills for AI-powered development workflows.
+---
+{%- assign all_skills = site.data.skills | sort -%}
+
+
+
+
+
+
+
+ Browse and search Kong skills for AI-powered development workflows.
+
+
+
+ {% capture quick_install %}{% include skills/quick_install.md %}{% endcapture %}{{ quick_install | markdownify }}
+
+
Installation guide
+
+
+
+
+
+ {% include_svg '/assets/icons/search.svg' width="20" height="20" class="filter-results-field__image" %}
+
+
+
+
+
+
+
+ {% for skill_entry in all_skills %}
+ {% assign skill = skill_entry[1] %}
+ {% include cards/skill.html skill=skill %}
+ {% endfor %}
+
+
+
+ No skills found.
+
+
+
+
diff --git a/app/skills/install.html b/app/skills/install.html
new file mode 100644
index 0000000000..c8ebd86695
--- /dev/null
+++ b/app/skills/install.html
@@ -0,0 +1,12 @@
+---
+title: Install Kong Skills
+description: Step-by-step installation instructions for Kong Skills across supported AI coding tools including Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, and Goose.
+subtitle: Get up and running with Kong Skills in your AI coding environment
+layout: without_aside
+no_edit_link: true
+toc: false
+breadcrumbs:
+ - /skills/
+---
+
+{% include skills/install.md %}
diff --git a/jekyll-dev.yml b/jekyll-dev.yml
index 173923f61c..ddf6831a37 100644
--- a/jekyll-dev.yml
+++ b/jekyll-dev.yml
@@ -12,6 +12,7 @@ skip:
auto_generated: true # skip auto_generated references, i.e. app/_referneces
mesh: true # skip kuma to mesh generation
llm_pages: true # skip markdown pages generation
+ skills: true # skip skills generation
# exclude app/_references
# even though we set skip.auto_generated: true and the collection has output:false
diff --git a/jekyll.yml b/jekyll.yml
index eb8bc7c287..6fe5e75e15 100644
--- a/jekyll.yml
+++ b/jekyll.yml
@@ -1,5 +1,6 @@
source: app
destination: dist
+skills_repo_path: app/.repos/skills
permalink: pretty
timezone: America/San_Francisco
markdown: kramdown
@@ -135,6 +136,7 @@ repos:
developer: https://github.com/Kong/developer.konghq.com
developer_raw: https://raw.githubusercontent.com/Kong/developer.konghq.com
docs: https://github.com/Kong/docs.konghq.com
+ skills: https://github.com/Kong/skills
plugin_schemas_path: app/_schemas/gateway/plugins
@@ -192,3 +194,7 @@ render_banner: false
llm_copy:
on_prem_snippet: 'On-prem deployments'
konnect_snippet: 'Konnect deployments'
+
+
+well-known:
+ skills: .well-known/skills
\ No newline at end of file
diff --git a/tailwind.config.js b/tailwind.config.js
index 9605b7c10d..fbb76e54a3 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -10,6 +10,7 @@ module.exports = {
"app/_plugins/**/*.rb",
"app/_assets/javascripts/**",
"app/gateway/**",
+ "app/skills/**/*.html",
],
darkMode: "selector",
safelist: [