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 %} +
+ +
+

/{{ skill.title }}

+ {% if skill.version %}{{ skill.version }}{% endif %} +
+ +
+

{{ skill.description }}

+
+ + {% if skill.scripts or skill.references or skill.assets %} +
+ {% if skill.scripts %} + + {% include_svg '/assets/icons/skills/scripts.svg' width="16" height="16" %} + + {% endif %} + + {% if skill.references %} + + {% include_svg '/assets/icons/skills/references.svg' width="16" height="16" %} + + {% endif %} + + {% if skill.assets %} + + {% include_svg '/assets/icons/skills/assets.svg' width="16" height="16" %} + + {% endif %} +
+ {% endif %} + +
+ {{ quick_install | liquify | markdownify }} +
+
+
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/' %} +
+ {% include_svg '/assets/icons/github.svg' width="16" height="16" %} + {{ repo_parts[1] }} +
+
+ +{% 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 %} +
+
Contains
+
+ {% if page.scripts %} + +
{% include_svg '/assets/icons/skills/scripts.svg' width="16" height="16" %}
+ Scripts +
+ {% endif %} + {% if page.references %} + +
{% include_svg '/assets/icons/skills/references.svg' width="16" height="16" %}
+ References +
+ {% endif %} + {% if page.assets %} + +
{% include_svg '/assets/icons/skills/assets.svg' width="16" height="16" %}
+ Assets +
+ {% endif %} +
+
+{% 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 -%} + +
+ +
+
+

Kong Skills Hub

+

+ 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 %} +
+ + +
+ +
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: [