From db83e8635b586eb8839ab7e4e20e30ea284cc42c Mon Sep 17 00:00:00 2001 From: oratis Date: Fri, 26 Jun 2026 13:47:19 +0800 Subject: [PATCH] feat(gtm): self-serve "Listed on TakoAPI" badge loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Productizes the badge backlink loop (the top GTM-code gap; the outreach campaign already pointed repos at a badge): - GET /api/badge/[slug] — owned dynamic SVG badge (brand purple), shows a project's live star count / "listed" for hosted; slug-validated; CDN-cached. Updating + on our own domain, so repos have a reason to embed ours. - BadgeSnippet on every /agents/[slug] — copy-paste Markdown/HTML, the self-serve half (scales beyond the 30 manual outreach PRs). - /badge how-to page (fixes the 404 the GTM plan flags) + live example. - sitemap += /badge; i18n Badge namespace across all 15 locales. Co-Authored-By: Claude Opus 4.8 --- messages/ar.json | 16 +++++ messages/de.json | 16 +++++ messages/en.json | 16 +++++ messages/es.json | 16 +++++ messages/fr.json | 16 +++++ messages/hi.json | 16 +++++ messages/id.json | 16 +++++ messages/it.json | 16 +++++ messages/ja.json | 16 +++++ messages/ko.json | 16 +++++ messages/pt.json | 16 +++++ messages/ru.json | 16 +++++ messages/tr.json | 16 +++++ messages/vi.json | 16 +++++ messages/zh.json | 16 +++++ src/app/[locale]/agents/[slug]/page.tsx | 2 + src/app/[locale]/badge/page.tsx | 90 +++++++++++++++++++++++++ src/app/api/badge/[slug]/route.ts | 73 ++++++++++++++++++++ src/app/sitemap.ts | 1 + src/components/BadgeSnippet.tsx | 54 +++++++++++++++ 20 files changed, 460 insertions(+) create mode 100644 src/app/[locale]/badge/page.tsx create mode 100644 src/app/api/badge/[slug]/route.ts create mode 100644 src/components/BadgeSnippet.tsx diff --git a/messages/ar.json b/messages/ar.json index 10c699fd..4ded80cb 100644 --- a/messages/ar.json +++ b/messages/ar.json @@ -640,5 +640,21 @@ "indexDescription": "اعثر على وكلاء الذكاء الاصطناعي حسب الاستخدام — الواجهات الأمامية والاستثمار والتسويق والدعم وغيرها — جميعها قابلة للاستدعاء عبر واجهة برمجية واحدة.", "indexHeroTitle": "تصفّح حسب السيناريو", "indexHeroSubtitle": "اعثر على وكلاء الذكاء الاصطناعي حسب المهمة التي تريد إنجازها." + }, + "Badge": { + "snippetTitle": "إضافة شارة", + "copy": "نسخ", + "copied": "تم النسخ", + "snippetHint": "الصقها في ملف README لربطها بصفحتك على TakoAPI.", + "pageTitle": "شارة README الخاصة بـ TakoAPI", + "pageIntro": "أظهر أن مشروعك مُدرَج على TakoAPI — شارة صغيرة تربط بصفحتك وتعرض عدد النجوم المحدّث.", + "example": "مثال", + "step1Title": "اعثر على صفحتك", + "step1": "ابحث عن مشروعك في دليل الوكلاء.", + "step2Title": "انسخ الشارة", + "step2": "في صفحتك، انسخ Markdown من مربع الشارة.", + "step3Title": "الصقها في README", + "step3": "أضِفها قرب الأعلى — يحصل الزوار ومحركات البحث على رابط لصفحتك على TakoAPI.", + "findListing": "اعثر على صفحتك" } } diff --git a/messages/de.json b/messages/de.json index dcaf35fd..70f71adf 100644 --- a/messages/de.json +++ b/messages/de.json @@ -640,5 +640,21 @@ "indexDescription": "Finde KI-Agenten nach Einsatzzweck – Frontend, Investing, Marketing, Support und mehr – alle über eine API aufrufbar.", "indexHeroTitle": "Nach Szenario durchsuchen", "indexHeroSubtitle": "Finde KI-Agenten nach der Aufgabe, die du erledigen willst." + }, + "Badge": { + "snippetTitle": "Badge hinzufügen", + "copy": "Kopieren", + "copied": "Kopiert", + "snippetHint": "Füge es in dein README ein, um auf deinen TakoAPI-Eintrag zu verlinken.", + "pageTitle": "TakoAPI-README-Badge", + "pageIntro": "Zeige, dass dein Projekt auf TakoAPI gelistet ist – ein kleines Badge, das auf deinen Eintrag verlinkt und deine aktuelle Sternezahl anzeigt.", + "example": "Beispiel", + "step1Title": "Eintrag finden", + "step1": "Suche im Agent-Verzeichnis nach deinem Projekt.", + "step2Title": "Badge kopieren", + "step2": "Kopiere das Markdown aus der Badge-Box auf deiner Eintragsseite.", + "step3Title": "In dein README einfügen", + "step3": "Füge es oben ein – Besucher und Suchmaschinen erhalten einen Link zu deiner TakoAPI-Seite.", + "findListing": "Eintrag finden" } } diff --git a/messages/en.json b/messages/en.json index e71d0c7d..d212bf76 100644 --- a/messages/en.json +++ b/messages/en.json @@ -640,5 +640,21 @@ "indexDescription": "Find AI agents by what you'd use them for — frontend, investing, marketing, support, and more — all callable through one API.", "indexHeroTitle": "Browse by scenario", "indexHeroSubtitle": "Find AI agents by the job you need done." + }, + "Badge": { + "snippetTitle": "Add a badge", + "copy": "Copy", + "copied": "Copied", + "snippetHint": "Paste into your README to link back to your TakoAPI listing.", + "pageTitle": "TakoAPI README badge", + "pageIntro": "Show that your project is listed on TakoAPI — a small badge that links back to your listing and shows your live star count.", + "example": "Example", + "step1Title": "Find your listing", + "step1": "Search the agent directory for your project.", + "step2Title": "Copy the badge", + "step2": "On your listing page, copy the Markdown from the badge box.", + "step3Title": "Paste into your README", + "step3": "Add it near the top — visitors and search engines get a link to your TakoAPI page.", + "findListing": "Find your listing" } } diff --git a/messages/es.json b/messages/es.json index 46bf89c6..3925346c 100644 --- a/messages/es.json +++ b/messages/es.json @@ -640,5 +640,21 @@ "indexDescription": "Encuentra agentes de IA según para qué los usarías: frontend, inversión, marketing, soporte y más, todos invocables a través de una API.", "indexHeroTitle": "Explorar por escenario", "indexHeroSubtitle": "Encuentra agentes de IA según la tarea que necesitas resolver." + }, + "Badge": { + "snippetTitle": "Añadir insignia", + "copy": "Copiar", + "copied": "Copiado", + "snippetHint": "Pégalo en tu README para enlazar a tu ficha de TakoAPI.", + "pageTitle": "Insignia README de TakoAPI", + "pageIntro": "Muestra que tu proyecto está listado en TakoAPI: una pequeña insignia que enlaza a tu ficha y muestra tu número de estrellas en vivo.", + "example": "Ejemplo", + "step1Title": "Encuentra tu ficha", + "step1": "Busca tu proyecto en el directorio de agentes.", + "step2Title": "Copia la insignia", + "step2": "En la página de tu ficha, copia el Markdown del cuadro de la insignia.", + "step3Title": "Pégala en tu README", + "step3": "Añádela cerca del inicio: visitantes y buscadores obtienen un enlace a tu página de TakoAPI.", + "findListing": "Encuentra tu ficha" } } diff --git a/messages/fr.json b/messages/fr.json index 25a2e5cd..6fa614a2 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -640,5 +640,21 @@ "indexDescription": "Trouvez des agents IA selon l'usage : frontend, investissement, marketing, support et plus, tous appelables via une API.", "indexHeroTitle": "Parcourir par scénario", "indexHeroSubtitle": "Trouvez des agents IA selon la tâche à accomplir." + }, + "Badge": { + "snippetTitle": "Ajouter un badge", + "copy": "Copier", + "copied": "Copié", + "snippetHint": "Collez-le dans votre README pour créer un lien vers votre fiche TakoAPI.", + "pageTitle": "Badge README TakoAPI", + "pageIntro": "Montrez que votre projet est référencé sur TakoAPI — un petit badge qui renvoie vers votre fiche et affiche votre nombre d'étoiles en direct.", + "example": "Exemple", + "step1Title": "Trouvez votre fiche", + "step1": "Cherchez votre projet dans l'annuaire d'agents.", + "step2Title": "Copiez le badge", + "step2": "Sur la page de votre fiche, copiez le Markdown depuis l'encadré du badge.", + "step3Title": "Collez-le dans votre README", + "step3": "Ajoutez-le près du haut — visiteurs et moteurs de recherche obtiennent un lien vers votre page TakoAPI.", + "findListing": "Trouver votre fiche" } } diff --git a/messages/hi.json b/messages/hi.json index df397a44..30cf19ab 100644 --- a/messages/hi.json +++ b/messages/hi.json @@ -640,5 +640,21 @@ "indexDescription": "उपयोग के अनुसार AI एजेंट खोजें — फ्रंटएंड, निवेश, मार्केटिंग, सपोर्ट और बहुत कुछ — सभी एक API से कॉल किए जा सकते हैं।", "indexHeroTitle": "परिदृश्य के अनुसार ब्राउज़ करें", "indexHeroSubtitle": "जो काम करना है उसके अनुसार AI एजेंट खोजें।" + }, + "Badge": { + "snippetTitle": "बैज जोड़ें", + "copy": "कॉपी करें", + "copied": "कॉपी हो गया", + "snippetHint": "अपने TakoAPI लिस्टिंग से लिंक करने के लिए इसे अपने README में पेस्ट करें।", + "pageTitle": "TakoAPI README बैज", + "pageIntro": "दिखाएँ कि आपका प्रोजेक्ट TakoAPI पर सूचीबद्ध है — एक छोटा बैज जो आपकी लिस्टिंग से लिंक करता है और आपकी लाइव स्टार संख्या दिखाता है।", + "example": "उदाहरण", + "step1Title": "अपनी लिस्टिंग खोजें", + "step1": "एजेंट डायरेक्टरी में अपना प्रोजेक्ट खोजें।", + "step2Title": "बैज कॉपी करें", + "step2": "अपनी लिस्टिंग पेज पर बैज बॉक्स से Markdown कॉपी करें।", + "step3Title": "README में पेस्ट करें", + "step3": "इसे ऊपर की ओर जोड़ें — विज़िटर और सर्च इंजन को आपके TakoAPI पेज का लिंक मिलता है।", + "findListing": "अपनी लिस्टिंग खोजें" } } diff --git a/messages/id.json b/messages/id.json index 4527ae9d..ee5f2006 100644 --- a/messages/id.json +++ b/messages/id.json @@ -640,5 +640,21 @@ "indexDescription": "Temukan agen AI berdasarkan kegunaannya — frontend, investasi, marketing, dukungan, dan lainnya — semua dapat dipanggil melalui satu API.", "indexHeroTitle": "Jelajahi berdasarkan skenario", "indexHeroSubtitle": "Temukan agen AI berdasarkan pekerjaan yang perlu diselesaikan." + }, + "Badge": { + "snippetTitle": "Tambahkan lencana", + "copy": "Salin", + "copied": "Tersalin", + "snippetHint": "Tempel ke README Anda untuk menautkan ke halaman TakoAPI Anda.", + "pageTitle": "Lencana README TakoAPI", + "pageIntro": "Tunjukkan bahwa proyek Anda terdaftar di TakoAPI — lencana kecil yang menautkan ke halaman Anda dan menampilkan jumlah bintang terkini.", + "example": "Contoh", + "step1Title": "Temukan halaman Anda", + "step1": "Cari proyek Anda di direktori agen.", + "step2Title": "Salin lencana", + "step2": "Di halaman Anda, salin Markdown dari kotak lencana.", + "step3Title": "Tempel ke README", + "step3": "Tambahkan di dekat atas — pengunjung dan mesin pencari mendapat tautan ke halaman TakoAPI Anda.", + "findListing": "Temukan halaman Anda" } } diff --git a/messages/it.json b/messages/it.json index 9c878c5a..f7944338 100644 --- a/messages/it.json +++ b/messages/it.json @@ -640,5 +640,21 @@ "indexDescription": "Trova agenti IA in base all'uso: frontend, investimenti, marketing, assistenza e altro, tutti richiamabili tramite un'API.", "indexHeroTitle": "Esplora per scenario", "indexHeroSubtitle": "Trova agenti IA in base al compito da svolgere." + }, + "Badge": { + "snippetTitle": "Aggiungi un badge", + "copy": "Copia", + "copied": "Copiato", + "snippetHint": "Incollalo nel tuo README per collegarti alla tua scheda TakoAPI.", + "pageTitle": "Badge README di TakoAPI", + "pageIntro": "Mostra che il tuo progetto è elencato su TakoAPI: un piccolo badge che rimanda alla tua scheda e mostra il numero di stelle in tempo reale.", + "example": "Esempio", + "step1Title": "Trova la tua scheda", + "step1": "Cerca il tuo progetto nella directory degli agenti.", + "step2Title": "Copia il badge", + "step2": "Nella pagina della tua scheda, copia il Markdown dal riquadro del badge.", + "step3Title": "Incollalo nel tuo README", + "step3": "Aggiungilo in alto: visitatori e motori di ricerca ottengono un link alla tua pagina TakoAPI.", + "findListing": "Trova la tua scheda" } } diff --git a/messages/ja.json b/messages/ja.json index ba095a50..7d5ea11b 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -640,5 +640,21 @@ "indexDescription": "用途で AI エージェントを探す——フロントエンド、投資、マーケティング、サポートなど、すべて 1 つの API で呼び出し可能。", "indexHeroTitle": "シナリオ別に探す", "indexHeroSubtitle": "やりたいことから AI エージェントを探しましょう。" + }, + "Badge": { + "snippetTitle": "バッジを追加", + "copy": "コピー", + "copied": "コピーしました", + "snippetHint": "README に貼り付けると、TakoAPI の掲載ページにリンクできます。", + "pageTitle": "TakoAPI README バッジ", + "pageIntro": "あなたのプロジェクトが TakoAPI に掲載されていることを示す小さなバッジ。掲載ページへのリンクと最新のスター数を表示します。", + "example": "例", + "step1Title": "掲載ページを探す", + "step1": "エージェントディレクトリでプロジェクトを検索します。", + "step2Title": "バッジをコピー", + "step2": "掲載ページのバッジ欄から Markdown をコピーします。", + "step3Title": "README に貼り付け", + "step3": "上部付近に追加すると、訪問者や検索エンジンが TakoAPI ページへのリンクを得られます。", + "findListing": "掲載ページを探す" } } diff --git a/messages/ko.json b/messages/ko.json index 1a0c9aa4..2e01ef9b 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -640,5 +640,21 @@ "indexDescription": "용도별로 AI 에이전트를 찾으세요 — 프런트엔드, 투자, 마케팅, 지원 등 모두 하나의 API로 호출 가능합니다.", "indexHeroTitle": "시나리오별 보기", "indexHeroSubtitle": "해야 할 일에 맞는 AI 에이전트를 찾으세요." + }, + "Badge": { + "snippetTitle": "배지 추가", + "copy": "복사", + "copied": "복사됨", + "snippetHint": "README에 붙여넣어 TakoAPI 등록 페이지로 연결하세요.", + "pageTitle": "TakoAPI README 배지", + "pageIntro": "프로젝트가 TakoAPI에 등록되었음을 보여주세요 — 등록 페이지로 연결되고 실시간 스타 수를 표시하는 작은 배지입니다.", + "example": "예시", + "step1Title": "등록 페이지 찾기", + "step1": "에이전트 디렉터리에서 프로젝트를 검색하세요.", + "step2Title": "배지 복사", + "step2": "등록 페이지의 배지 상자에서 Markdown을 복사하세요.", + "step3Title": "README에 붙여넣기", + "step3": "상단 근처에 추가하면 방문자와 검색엔진이 TakoAPI 페이지로 연결됩니다.", + "findListing": "등록 페이지 찾기" } } diff --git a/messages/pt.json b/messages/pt.json index b9f3b569..a9b900ac 100644 --- a/messages/pt.json +++ b/messages/pt.json @@ -640,5 +640,21 @@ "indexDescription": "Encontre agentes de IA pelo uso: frontend, investimento, marketing, suporte e mais, todos invocáveis através de uma API.", "indexHeroTitle": "Explorar por cenário", "indexHeroSubtitle": "Encontre agentes de IA pela tarefa que precisa resolver." + }, + "Badge": { + "snippetTitle": "Adicionar selo", + "copy": "Copiar", + "copied": "Copiado", + "snippetHint": "Cole no seu README para criar um link para sua página no TakoAPI.", + "pageTitle": "Selo README do TakoAPI", + "pageIntro": "Mostre que seu projeto está listado no TakoAPI — um pequeno selo que liga à sua página e exibe sua contagem de estrelas em tempo real.", + "example": "Exemplo", + "step1Title": "Encontre sua página", + "step1": "Procure seu projeto no diretório de agentes.", + "step2Title": "Copie o selo", + "step2": "Na página da sua listagem, copie o Markdown da caixa do selo.", + "step3Title": "Cole no seu README", + "step3": "Adicione perto do topo — visitantes e buscadores recebem um link para sua página no TakoAPI.", + "findListing": "Encontre sua página" } } diff --git a/messages/ru.json b/messages/ru.json index 36ecf06e..dac6c9d8 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -640,5 +640,21 @@ "indexDescription": "Найдите ИИ-агентов по задаче: фронтенд, инвестиции, маркетинг, поддержка и другое — все вызываются через один API.", "indexHeroTitle": "По сценариям", "indexHeroSubtitle": "Найдите ИИ-агентов по задаче, которую нужно решить." + }, + "Badge": { + "snippetTitle": "Добавить бейдж", + "copy": "Копировать", + "copied": "Скопировано", + "snippetHint": "Вставьте в свой README, чтобы дать ссылку на вашу страницу в TakoAPI.", + "pageTitle": "README-бейдж TakoAPI", + "pageIntro": "Покажите, что ваш проект есть в каталоге TakoAPI — небольшой бейдж со ссылкой на вашу страницу и актуальным числом звёзд.", + "example": "Пример", + "step1Title": "Найдите свою страницу", + "step1": "Найдите свой проект в каталоге агентов.", + "step2Title": "Скопируйте бейдж", + "step2": "На странице вашего проекта скопируйте Markdown из блока бейджа.", + "step3Title": "Вставьте в README", + "step3": "Добавьте ближе к началу — посетители и поисковики получат ссылку на вашу страницу TakoAPI.", + "findListing": "Найти свою страницу" } } diff --git a/messages/tr.json b/messages/tr.json index 3698cc1d..abf7a8a3 100644 --- a/messages/tr.json +++ b/messages/tr.json @@ -640,5 +640,21 @@ "indexDescription": "Yapay zekâ aracılarını kullanım amacına göre bulun — frontend, yatırım, pazarlama, destek ve daha fazlası — hepsi tek bir API ile çağrılabilir.", "indexHeroTitle": "Senaryoya göre keşfet", "indexHeroSubtitle": "Yapmanız gereken işe göre yapay zekâ aracıları bulun." + }, + "Badge": { + "snippetTitle": "Rozet ekle", + "copy": "Kopyala", + "copied": "Kopyalandı", + "snippetHint": "TakoAPI sayfanıza bağlanmak için README'nize yapıştırın.", + "pageTitle": "TakoAPI README rozeti", + "pageIntro": "Projenizin TakoAPI'de listelendiğini gösterin — sayfanıza bağlanan ve güncel yıldız sayınızı gösteren küçük bir rozet.", + "example": "Örnek", + "step1Title": "Sayfanızı bulun", + "step1": "Projenizi aracı dizininde arayın.", + "step2Title": "Rozeti kopyalayın", + "step2": "Sayfanızda rozet kutusundan Markdown'ı kopyalayın.", + "step3Title": "README'nize yapıştırın", + "step3": "Üst kısma ekleyin — ziyaretçiler ve arama motorları TakoAPI sayfanıza bağlantı alır.", + "findListing": "Sayfanızı bulun" } } diff --git a/messages/vi.json b/messages/vi.json index 7b86ee8b..57f7f437 100644 --- a/messages/vi.json +++ b/messages/vi.json @@ -640,5 +640,21 @@ "indexDescription": "Tìm agent AI theo nhu cầu — frontend, đầu tư, marketing, hỗ trợ và hơn thế — tất cả gọi được qua một API.", "indexHeroTitle": "Duyệt theo kịch bản", "indexHeroSubtitle": "Tìm agent AI theo công việc bạn cần làm." + }, + "Badge": { + "snippetTitle": "Thêm huy hiệu", + "copy": "Sao chép", + "copied": "Đã sao chép", + "snippetHint": "Dán vào README để liên kết đến trang TakoAPI của bạn.", + "pageTitle": "Huy hiệu README TakoAPI", + "pageIntro": "Thể hiện dự án của bạn có trong TakoAPI — một huy hiệu nhỏ liên kết đến trang của bạn và hiển thị số sao theo thời gian thực.", + "example": "Ví dụ", + "step1Title": "Tìm trang của bạn", + "step1": "Tìm dự án của bạn trong thư mục agent.", + "step2Title": "Sao chép huy hiệu", + "step2": "Trên trang của bạn, sao chép Markdown từ ô huy hiệu.", + "step3Title": "Dán vào README", + "step3": "Thêm gần đầu trang — khách truy cập và công cụ tìm kiếm có liên kết đến trang TakoAPI của bạn.", + "findListing": "Tìm trang của bạn" } } diff --git a/messages/zh.json b/messages/zh.json index 796603c6..20498976 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -640,5 +640,21 @@ "indexDescription": "按用途查找 AI 智能体——前端、投资、营销、客服等——都可通过一个 API 调用。", "indexHeroTitle": "按场景浏览", "indexHeroSubtitle": "按你要完成的任务查找 AI 智能体。" + }, + "Badge": { + "snippetTitle": "添加徽章", + "copy": "复制", + "copied": "已复制", + "snippetHint": "粘贴到你的 README,即可回链到你的 TakoAPI 收录页。", + "pageTitle": "TakoAPI README 徽章", + "pageIntro": "展示你的项目已收录于 TakoAPI——一个回链到收录页并显示实时星标数的小徽章。", + "example": "示例", + "step1Title": "找到你的收录页", + "step1": "在智能体目录里搜索你的项目。", + "step2Title": "复制徽章", + "step2": "在收录页的徽章框里复制 Markdown。", + "step3Title": "粘贴到 README", + "step3": "放在顶部附近——访客和搜索引擎都能链接到你的 TakoAPI 页面。", + "findListing": "找到你的收录页" } } diff --git a/src/app/[locale]/agents/[slug]/page.tsx b/src/app/[locale]/agents/[slug]/page.tsx index 94372aeb..2fd91efb 100644 --- a/src/app/[locale]/agents/[slug]/page.tsx +++ b/src/app/[locale]/agents/[slug]/page.tsx @@ -9,6 +9,7 @@ import { localeOg } from "@/lib/locales"; import { findScenario } from "@/lib/scenarios"; import { JsonLd } from "@/components/JsonLd"; import { AgentEngagement } from "@/components/AgentEngagement"; +import { BadgeSnippet } from "@/components/BadgeSnippet"; export const dynamic = "force-dynamic"; @@ -297,6 +298,7 @@ export default async function AgentDetailPage({ params }: { params: Promise<{ lo )} + diff --git a/src/app/[locale]/badge/page.tsx b/src/app/[locale]/badge/page.tsx new file mode 100644 index 00000000..025fb167 --- /dev/null +++ b/src/app/[locale]/badge/page.tsx @@ -0,0 +1,90 @@ +import { getTranslations, setRequestLocale } from "next-intl/server"; +import { Link } from "@/i18n/navigation"; +import { prisma } from "@/lib/prisma"; +import { absoluteUrl, SITE_NAME, SITE_URL, localizedAlternates } from "@/lib/seo"; +import { localeOg } from "@/lib/locales"; + +export const dynamic = "force-dynamic"; + +export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params; + const t = await getTranslations({ locale, namespace: "Badge" }); + const title = t("pageTitle"); + const description = t("pageIntro"); + return { + metadataBase: new URL(absoluteUrl("")), + title, + description, + alternates: localizedAlternates(locale, "/badge"), + openGraph: { + title: `${title} — ${SITE_NAME}`, + description, + url: absoluteUrl("/badge"), + type: "website", + locale: localeOg(locale), + images: [absoluteUrl("/opengraph-image")], + }, + twitter: { card: "summary_large_image", title: `${title} — ${SITE_NAME}`, description }, + }; +} + +export default async function BadgePage({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params; + setRequestLocale(locale); + const t = await getTranslations("Badge"); + + const example = await prisma.agent.findFirst({ + where: { status: "APPROVED", kind: "PROJECT", stars: { gt: 500 } }, + orderBy: { stars: "desc" }, + select: { slug: true }, + }); + const exSlug = example?.slug ?? ""; + const md = exSlug ? `[![TakoAPI](${SITE_URL}/api/badge/${exSlug})](${SITE_URL}/agents/${exSlug})` : ""; + + const steps = [ + { n: 1, title: t("step1Title"), body: t("step1") }, + { n: 2, title: t("step2Title"), body: t("step2") }, + { n: 3, title: t("step3Title"), body: t("step3") }, + ]; + + return ( +
+

{t("pageTitle")}

+

{t("pageIntro")}

+ + {exSlug && ( +
+

{t("example")}

+ {/* eslint-disable-next-line @next/next/no-img-element */} + TakoAPI badge + + {md} + +
+ )} + +
    + {steps.map((s) => ( +
  1. + + {s.n} + +
    +

    {s.title}

    +

    {s.body}

    +
    +
  2. + ))} +
+ +
+ + {t("findListing")} + +
+
+ ); +} diff --git a/src/app/api/badge/[slug]/route.ts b/src/app/api/badge/[slug]/route.ts new file mode 100644 index 00000000..1907512c --- /dev/null +++ b/src/app/api/badge/[slug]/route.ts @@ -0,0 +1,73 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +export const dynamic = "force-dynamic"; + +// Owned "TakoAPI" README badge (shields-style flat SVG). Dynamic value: a +// project's live star count, or "listed" for hosted agents — giving repos a +// reason to embed ours over a static shields.io badge (it updates, and every +// render is an impression on our own domain). Embedded via the snippet on each +// /agents/[slug] page and the /badge how-to. + +function fmtStars(n: number): string { + if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "k"; + return String(n); +} + +function esc(s: string): string { + return s.replace(/&/g, "&").replace(//g, ">"); +} + +// Rough monospace-ish width estimate for Verdana 11px; fine for a small badge. +function textWidth(s: string): number { + return Math.ceil(s.length * 6.5); +} + +function badgeSvg(label: string, value: string): string { + const PAD = 12; + const lw = textWidth(label) + PAD; + const vw = textWidth(value) + PAD; + const w = lw + vw; + const lx = lw / 2; + const vx = lw + vw / 2; + return ` +${esc(label)}: ${esc(value)} + + + + + + + + +${esc(label)} +${esc(label)} +${esc(value)} +${esc(value)} + +`; +} + +function svgResponse(svg: string, status: number, maxAge: number): NextResponse { + return new NextResponse(svg, { + status, + headers: { + "Content-Type": "image/svg+xml; charset=utf-8", + "Cache-Control": `public, max-age=${maxAge}, s-maxage=3600, stale-while-revalidate=86400`, + }, + }); +} + +export async function GET(_req: NextRequest, { params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + const agent = await prisma.agent.findFirst({ + where: { OR: [{ slug }, { id: slug }], status: "APPROVED" }, + select: { kind: true, stars: true }, + }); + if (!agent) { + return svgResponse(badgeSvg("TakoAPI", "not listed"), 404, 300); + } + const value = + agent.kind === "PROJECT" && typeof agent.stars === "number" ? `★ ${fmtStars(agent.stars)}` : "listed"; + return svgResponse(badgeSvg("TakoAPI", value), 200, 600); +} diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index e42dcffc..da82f756 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -53,6 +53,7 @@ export default async function sitemap(): Promise { entry("/scenarios", { lastModified: now, changeFrequency: "weekly", priority: 0.8 }), entry("/skills", { lastModified: now, changeFrequency: "daily", priority: 0.8 }), entry("/install", { lastModified: now, changeFrequency: "monthly", priority: 0.6 }), + entry("/badge", { lastModified: now, changeFrequency: "monthly", priority: 0.5 }), entry("/trending", { lastModified: now, changeFrequency: "weekly", priority: 0.6 }), ]; diff --git a/src/components/BadgeSnippet.tsx b/src/components/BadgeSnippet.tsx new file mode 100644 index 00000000..8a360a55 --- /dev/null +++ b/src/components/BadgeSnippet.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { Copy, Check } from "lucide-react"; +import { SITE_URL } from "@/lib/seo"; + +// Copy-paste "Listed on TakoAPI" badge for a listing's README — the self-serve +// half of the badge loop (the SVG itself is served by /api/badge/[slug]). +export function BadgeSnippet({ slug }: { slug: string }) { + const t = useTranslations("Badge"); + const [copied, setCopied] = useState(null); + + const img = `${SITE_URL}/api/badge/${slug}`; + const link = `${SITE_URL}/agents/${slug}`; + const snippets: { key: string; label: string; text: string }[] = [ + { key: "md", label: "Markdown", text: `[![TakoAPI](${img})](${link})` }, + { key: "html", label: "HTML", text: `TakoAPI` }, + ]; + + const copy = (text: string, key: string) => { + navigator.clipboard?.writeText(text); + setCopied(key); + setTimeout(() => setCopied(null), 1500); + }; + + return ( +
+

{t("snippetTitle")}

+ {/* eslint-disable-next-line @next/next/no-img-element */} + TakoAPI badge +
+ {snippets.map((s) => ( +
+
+ {s.label} + +
+ + {s.text} + +
+ ))} +
+

{t("snippetHint")}

+
+ ); +}