From aa7ad24758bcc9f57ec5384869a5aad9e141d90f Mon Sep 17 00:00:00 2001 From: Digital39999 Date: Sun, 22 Jun 2025 10:13:39 +0200 Subject: [PATCH] feat: add json output option for API endpoints and update documentation --- api/gist.js | 48 ++++++++++++++++++++------------ api/index.js | 65 ++++++++++++++++++++++++++++---------------- api/pin.js | 64 +++++++++++++++++++++++++++---------------- api/top-langs.js | 59 +++++++++++++++++++++++++++------------- api/wakatime.js | 46 ++++++++++++++++++++----------- docs/readme_cn.md | 1 + docs/readme_de.md | 1 + docs/readme_es.md | 1 + docs/readme_fr.md | 1 + docs/readme_it.md | 1 + docs/readme_ja.md | 1 + docs/readme_kr.md | 1 + docs/readme_nl.md | 1 + docs/readme_np.md | 1 + docs/readme_pt-BR.md | 1 + docs/readme_tr.md | 1 + readme.md | 1 + 17 files changed, 194 insertions(+), 100 deletions(-) diff --git a/api/gist.js b/api/gist.js index 5b03743ce9e6e..4200b10ab5702 100644 --- a/api/gist.js +++ b/api/gist.js @@ -4,9 +4,9 @@ import { renderError, parseBoolean, } from "../src/common/utils.js"; -import { isLocaleAvailable } from "../src/translations.js"; -import { renderGistCard } from "../src/cards/gist-card.js"; import { fetchGist } from "../src/fetchers/gist-fetcher.js"; +import { renderGistCard } from "../src/cards/gist-card.js"; +import { isLocaleAvailable } from "../src/translations.js"; export default async (req, res) => { const { @@ -22,19 +22,27 @@ export default async (req, res) => { border_color, show_owner, hide_border, + json, } = req.query; - res.setHeader("Content-Type", "image/svg+xml"); + const returnJson = parseBoolean(json); + + res.setHeader( + "Content-Type", + returnJson ? "application/json" : "image/svg+xml", + ); if (locale && !isLocaleAvailable(locale)) { return res.send( - renderError("Something went wrong", "Language not found", { - title_color, - text_color, - bg_color, - border_color, - theme, - }), + returnJson + ? { error: "Language not found" } + : renderError("Something went wrong", "Language not found", { + title_color, + text_color, + bg_color, + border_color, + theme, + }), ); } @@ -55,6 +63,10 @@ export default async (req, res) => { `max-age=${cacheSeconds}, s-maxage=${cacheSeconds}`, ); + if (returnJson) { + return res.send(gistData); + } + return res.send( renderGistCard(gistData, { title_color, @@ -77,13 +89,15 @@ export default async (req, res) => { }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, ); // Use lower cache period for errors. return res.send( - renderError(err.message, err.secondaryMessage, { - title_color, - text_color, - bg_color, - border_color, - theme, - }), + returnJson + ? { error: err.message, secondaryMessage: err.secondaryMessage } + : renderError(err.message, err.secondaryMessage, { + title_color, + text_color, + bg_color, + border_color, + theme, + }), ); } }; diff --git a/api/index.js b/api/index.js index c42bc04891234..a2f470e735429 100644 --- a/api/index.js +++ b/api/index.js @@ -1,5 +1,3 @@ -import { renderStatsCard } from "../src/cards/stats-card.js"; -import { blacklist } from "../src/common/blacklist.js"; import { clampValue, CONSTANTS, @@ -8,7 +6,9 @@ import { renderError, } from "../src/common/utils.js"; import { fetchStats } from "../src/fetchers/stats-fetcher.js"; +import { renderStatsCard } from "../src/cards/stats-card.js"; import { isLocaleAvailable } from "../src/translations.js"; +import { blacklist } from "../src/common/blacklist.js"; export default async (req, res) => { const { @@ -38,30 +38,41 @@ export default async (req, res) => { border_color, rank_icon, show, + json, } = req.query; - res.setHeader("Content-Type", "image/svg+xml"); + + const returnJson = parseBoolean(json); + + res.setHeader( + "Content-Type", + returnJson ? "application/json" : "image/svg+xml", + ); if (blacklist.includes(username)) { return res.send( - renderError("Something went wrong", "This username is blacklisted", { - title_color, - text_color, - bg_color, - border_color, - theme, - }), + returnJson + ? { error: "This username is blacklisted" } + : renderError("Something went wrong", "This username is blacklisted", { + title_color, + text_color, + bg_color, + border_color, + theme, + }), ); } if (locale && !isLocaleAvailable(locale)) { return res.send( - renderError("Something went wrong", "Language not found", { - title_color, - text_color, - bg_color, - border_color, - theme, - }), + returnJson + ? { error: "Language not found" } + : renderError("Something went wrong", "Language not found", { + title_color, + text_color, + bg_color, + border_color, + theme, + }), ); } @@ -91,6 +102,10 @@ export default async (req, res) => { `max-age=${cacheSeconds}, s-maxage=${cacheSeconds}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, ); + if (returnJson) { + return res.send(stats); + } + return res.send( renderStatsCard(stats, { hide: parseArray(hide), @@ -126,13 +141,15 @@ export default async (req, res) => { }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, ); // Use lower cache period for errors. return res.send( - renderError(err.message, err.secondaryMessage, { - title_color, - text_color, - bg_color, - border_color, - theme, - }), + returnJson + ? { error: err.message, secondaryMessage: err.secondaryMessage } + : renderError(err.message, err.secondaryMessage, { + title_color, + text_color, + bg_color, + border_color, + theme, + }), ); } }; diff --git a/api/pin.js b/api/pin.js index bede7d87f5972..620e89e56889f 100644 --- a/api/pin.js +++ b/api/pin.js @@ -1,5 +1,3 @@ -import { renderRepoCard } from "../src/cards/repo-card.js"; -import { blacklist } from "../src/common/blacklist.js"; import { clampValue, CONSTANTS, @@ -7,7 +5,9 @@ import { renderError, } from "../src/common/utils.js"; import { fetchRepo } from "../src/fetchers/repo-fetcher.js"; +import { renderRepoCard } from "../src/cards/repo-card.js"; import { isLocaleAvailable } from "../src/translations.js"; +import { blacklist } from "../src/common/blacklist.js"; export default async (req, res) => { const { @@ -25,31 +25,41 @@ export default async (req, res) => { border_radius, border_color, description_lines_count, + json, } = req.query; - res.setHeader("Content-Type", "image/svg+xml"); + const returnJson = parseBoolean(json); + + res.setHeader( + "Content-Type", + returnJson ? "application/json" : "image/svg+xml", + ); if (blacklist.includes(username)) { return res.send( - renderError("Something went wrong", "This username is blacklisted", { - title_color, - text_color, - bg_color, - border_color, - theme, - }), + returnJson + ? { error: "This username is blacklisted" } + : renderError("Something went wrong", "This username is blacklisted", { + title_color, + text_color, + bg_color, + border_color, + theme, + }), ); } if (locale && !isLocaleAvailable(locale)) { return res.send( - renderError("Something went wrong", "Language not found", { - title_color, - text_color, - bg_color, - border_color, - theme, - }), + returnJson + ? { error: "Language not found" } + : renderError("Something went wrong", "Language not found", { + title_color, + text_color, + bg_color, + border_color, + theme, + }), ); } @@ -70,6 +80,10 @@ export default async (req, res) => { `max-age=${cacheSeconds}, s-maxage=${cacheSeconds}`, ); + if (returnJson) { + return res.send(repoData); + } + return res.send( renderRepoCard(repoData, { hide_border: parseBoolean(hide_border), @@ -93,13 +107,15 @@ export default async (req, res) => { }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, ); // Use lower cache period for errors. return res.send( - renderError(err.message, err.secondaryMessage, { - title_color, - text_color, - bg_color, - border_color, - theme, - }), + returnJson + ? { error: err.message, secondaryMessage: err.secondaryMessage } + : renderError(err.message, err.secondaryMessage, { + title_color, + text_color, + bg_color, + border_color, + theme, + }), ); } }; diff --git a/api/top-langs.js b/api/top-langs.js index c5bed634c1eab..73954e9a074d8 100644 --- a/api/top-langs.js +++ b/api/top-langs.js @@ -1,5 +1,3 @@ -import { renderTopLanguages } from "../src/cards/top-languages-card.js"; -import { blacklist } from "../src/common/blacklist.js"; import { CONSTANTS, parseArray, @@ -7,7 +5,9 @@ import { renderError, } from "../src/common/utils.js"; import { fetchTopLanguages } from "../src/fetchers/top-languages-fetcher.js"; +import { renderTopLanguages } from "../src/cards/top-languages-card.js"; import { isLocaleAvailable } from "../src/translations.js"; +import { blacklist } from "../src/common/blacklist.js"; export default async (req, res) => { const { @@ -32,23 +32,36 @@ export default async (req, res) => { border_color, disable_animations, hide_progress, + json, } = req.query; - res.setHeader("Content-Type", "image/svg+xml"); + + const returnJson = parseBoolean(json); + + res.setHeader( + "Content-Type", + returnJson ? "application/json" : "image/svg+xml", + ); if (blacklist.includes(username)) { return res.send( - renderError("Something went wrong", "This username is blacklisted", { - title_color, - text_color, - bg_color, - border_color, - theme, - }), + returnJson + ? { error: "This username is blacklisted" } + : renderError("Something went wrong", "This username is blacklisted", { + title_color, + text_color, + bg_color, + border_color, + theme, + }), ); } if (locale && !isLocaleAvailable(locale)) { - return res.send(renderError("Something went wrong", "Locale not found")); + return res.send( + returnJson + ? { error: "Locale not found" } + : renderError("Something went wrong", "Locale not found"), + ); } if ( @@ -57,7 +70,9 @@ export default async (req, res) => { !["compact", "normal", "donut", "donut-vertical", "pie"].includes(layout)) ) { return res.send( - renderError("Something went wrong", "Incorrect layout input"), + returnJson + ? { error: "Incorrect layout input" } + : renderError("Something went wrong", "Incorrect layout input"), ); } @@ -82,6 +97,10 @@ export default async (req, res) => { `max-age=${cacheSeconds / 2}, s-maxage=${cacheSeconds}`, ); + if (returnJson) { + return res.send(topLangs); + } + return res.send( renderTopLanguages(topLangs, { custom_title, @@ -110,13 +129,15 @@ export default async (req, res) => { }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, ); // Use lower cache period for errors. return res.send( - renderError(err.message, err.secondaryMessage, { - title_color, - text_color, - bg_color, - border_color, - theme, - }), + returnJson + ? { error: err.message, secondaryMessage: err.secondaryMessage } + : renderError(err.message, err.secondaryMessage, { + title_color, + text_color, + bg_color, + border_color, + theme, + }), ); } }; diff --git a/api/wakatime.js b/api/wakatime.js index 73ef9986feeb7..5b72938b4c434 100644 --- a/api/wakatime.js +++ b/api/wakatime.js @@ -1,4 +1,3 @@ -import { renderWakatimeCard } from "../src/cards/wakatime-card.js"; import { clampValue, CONSTANTS, @@ -7,6 +6,7 @@ import { renderError, } from "../src/common/utils.js"; import { fetchWakatimeStats } from "../src/fetchers/wakatime-fetcher.js"; +import { renderWakatimeCard } from "../src/cards/wakatime-card.js"; import { isLocaleAvailable } from "../src/translations.js"; export default async (req, res) => { @@ -32,19 +32,27 @@ export default async (req, res) => { border_color, display_format, disable_animations, + json, } = req.query; - res.setHeader("Content-Type", "image/svg+xml"); + const returnJson = parseBoolean(json); + + res.setHeader( + "Content-Type", + returnJson ? "application/json" : "image/svg+xml", + ); if (locale && !isLocaleAvailable(locale)) { return res.send( - renderError("Something went wrong", "Language not found", { - title_color, - text_color, - bg_color, - border_color, - theme, - }), + returnJson + ? { error: "Language not found" } + : renderError("Something went wrong", "Language not found", { + title_color, + text_color, + bg_color, + border_color, + theme, + }), ); } @@ -67,6 +75,10 @@ export default async (req, res) => { }, s-maxage=${cacheSeconds}, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, ); + if (returnJson) { + return res.send(stats); + } + return res.send( renderWakatimeCard(stats, { custom_title, @@ -97,13 +109,15 @@ export default async (req, res) => { }, stale-while-revalidate=${CONSTANTS.ONE_DAY}`, ); // Use lower cache period for errors. return res.send( - renderError(err.message, err.secondaryMessage, { - title_color, - text_color, - bg_color, - border_color, - theme, - }), + returnJson + ? { error: err.message, secondaryMessage: err.secondaryMessage } + : renderError(err.message, err.secondaryMessage, { + title_color, + text_color, + bg_color, + border_color, + theme, + }), ); } }; diff --git a/docs/readme_cn.md b/docs/readme_cn.md index 0773656fc7e7c..282bc39f47a9a 100644 --- a/docs/readme_cn.md +++ b/docs/readme_cn.md @@ -154,6 +154,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr - `theme` - 主题名称,从[所有可用主题](../themes/README.md)中选择 - `cache_seconds` - 手动设置缓存头 _(最小值: 14400,最大值: 86400)_ - `locale` - 在卡片中设置语言 _(例如 cn, de, es, 等等)_ +- `json` - 输出原始 JSON 数据,而不是渲染卡片。适用于调试或在其他应用中使用数据。 _(布尔值)_ ##### bg_color 渐变 diff --git a/docs/readme_de.md b/docs/readme_de.md index edc5f8b24c695..73e8020b37eda 100644 --- a/docs/readme_de.md +++ b/docs/readme_de.md @@ -144,6 +144,7 @@ Du kannst das Erscheinungsbild deiner `Stats Card` oder `Repo Card`, mithilfe vo - `theme` - Name des Erscheinungsbildes/Themes [alle verfügbaren Themes](../themes/README.md) - `cache_seconds` - manuelles festlegen der Cachezeiten _(min: 14400, max: 86400)_ - `locale` - Stellen Sie die Sprache auf der Karte ein _(z.B. cn, de, es, etc.)_ +- `json` - Gibt rohe JSON-Daten aus, anstatt eine Karte zu rendern. Nützlich zum Debuggen oder für die Verwendung der Daten in anderen Anwendungen. _(Boolean)_ ##### Farbverlauf in bg_color diff --git a/docs/readme_es.md b/docs/readme_es.md index 89f647f7e08c7..cea7185f65f1f 100644 --- a/docs/readme_es.md +++ b/docs/readme_es.md @@ -159,6 +159,7 @@ Puedes personalizar el aspecto de tu `Tarjeta de Estadísticas` o `Tarjeta de Re - `theme` - Nombre del tema, elige uno de [todos los temas disponible ](../themes/README.md) - `cache_seconds` - Cache _(min: 14400, max: 86400)_ - `locale` - configurar el idioma en la tarjeta _(p.ej. cn, de, es, etc.)_ +- `json` - Muestra datos JSON en bruto en lugar de renderizar una tarjeta. Útil para depuración o para usar datos en otras aplicaciones. _(booleano)_ ##### Gradiente en `bg_color` diff --git a/docs/readme_fr.md b/docs/readme_fr.md index 0f035f96e76d7..02eeb756b91ad 100644 --- a/docs/readme_fr.md +++ b/docs/readme_fr.md @@ -141,6 +141,7 @@ Vous pouvez personnaliser l'apparence de votre `Carte des stats` ou `Carte de d - `theme` - Nom du thème, parmis [tous les thèmes disponibles](../themes/README.md) - `cache_seconds` - Paramétrer le cache manuellement _(min: 14400, max: 86400)_ - `locale` - définir la langue de la carte _(par exemple. cn, de, es, etc.)_ +- `json` - Affiche les données JSON brutes au lieu de rendre une carte. Utile pour le débogage ou pour utiliser les données dans d'autres applications. _(booléen)_ ##### Gradient in bg_color diff --git a/docs/readme_it.md b/docs/readme_it.md index 8f16f4e6bd080..8f6ff0bac66b8 100644 --- a/docs/readme_it.md +++ b/docs/readme_it.md @@ -155,6 +155,7 @@ Puoi personalizzare l'aspetto delle tue `Stats Card` o delle `Repo Card` in qual - `theme` - Nome del tema, dai un'occhiata a [tutti i temi disponibili](../themes/README.md) - `cache_seconds` - Specifica manualmente il valore di cache, in secondi _(min: 14400, max: 86400)_ - `locale` - Impostare la lingua nella scheda _(per esempio. cn, de, es, eccetera.)_ +- `json` - Mostra i dati JSON grezzi invece di rendere una scheda. Utile per il debug o per utilizzare i dati in altre applicazioni. _(booleano)_ ##### Gradiente nello sfondo diff --git a/docs/readme_ja.md b/docs/readme_ja.md index d338785f909d8..7213fd8ce6ca4 100644 --- a/docs/readme_ja.md +++ b/docs/readme_ja.md @@ -155,6 +155,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr - `theme` - [使用可能なテーマ一覧](../themes/README.md) から選んだテーマ名 - `cache_seconds` - キャッシュ時間の秒数 _(最小値: 14400, 最大値: 86400)_ - `locale` - カードに言語を設定する _(例えば cn, de, es, 等)_ +- `json` - カードをレンダリングする代わりに生のJSONデータを出力します。デバッグや他のアプリでデータを使用するのに便利です。 _(ブール値)_ ##### bg_color の グラデーション指定 diff --git a/docs/readme_kr.md b/docs/readme_kr.md index 178d0342506f2..202c3856919b3 100644 --- a/docs/readme_kr.md +++ b/docs/readme_kr.md @@ -168,6 +168,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr - `theme` - 테마의 이름, [사용 가능한 모든 테마](../themes/README.md) 에서 선택 - `cache_seconds` - 수동으로 캐시 헤더 설정 _(min: 14400, max: 86400)_ - `locale` - 카드에 표시할 언어 _(e.g. kr, cn, de, es, etc.)_ +- `json` - 카드 대신 원시 JSON 데이터를 출력합니다. 디버깅이나 다른 앱에서 데이터 사용에 유용합니다. _(boolean)_ ##### 배경에 그라데이션 주기 diff --git a/docs/readme_nl.md b/docs/readme_nl.md index 7fbec69971517..fc6a064e0a2dd 100644 --- a/docs/readme_nl.md +++ b/docs/readme_nl.md @@ -161,6 +161,7 @@ Je kan het uiterlijk van je `Statistieken kaart` of `Repo kaart` aanpassen hoe j - `theme` - Naam van het thema, kies uit [alle beschikbare thema\'s](../themes/README.md) - `cache_seconds` - Stel de cache header handmatig in _(min: 14400, max: 86400)_ - `locale` - Stel taal van de kaart in _(e.g. cn, de, es, etc.)_ +- `json` - Toont ruwe JSON-gegevens in plaats van een kaart te renderen. Handig voor debuggen of het gebruiken van gegevens in andere apps. _(boolean)_ ##### Kleurenverloop in bg_color (achtergrond kleur): diff --git a/docs/readme_np.md b/docs/readme_np.md index f5909ae718219..156015dd539d6 100644 --- a/docs/readme_np.md +++ b/docs/readme_np.md @@ -159,6 +159,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr - `theme` - name of the theme, choose from [all available themes](./themes/README.md) - `cache_seconds` - set the cache header manually _(min: 14400, max: 86400)_ - `locale` - set the language in the card _(e.g. cn, de, es, etc.)_ +- `json` - Outputs raw JSON data instead of the card. Useful for debugging or using data in other apps. _(boolean)_ ##### Gradient in bg_color diff --git a/docs/readme_pt-BR.md b/docs/readme_pt-BR.md index 35c334131ee93..bca4e029a1565 100644 --- a/docs/readme_pt-BR.md +++ b/docs/readme_pt-BR.md @@ -156,6 +156,7 @@ Personalize a aparência do seu `Stats Card` ou `Repo Card` da maneira que desej - `theme` - Nome do tema, escolha em [todos os temas disponíveis](../themes/README.md) - `cache_seconds` - Defina o cabeçalho do cache manualmente _(min: 14400, max: 86400)_ - `locale` - defina o idioma no cartão _(por exemplo. cn, de, es, etc.)_ +- `json` - Mostra dados JSON brutos em vez de renderizar o cartão. Útil para depuração ou uso dos dados em outros apps. _(booleano)_ > Nota sobre o cache: Cartões de repositório tem um cache padrão de 30 minutos (1800 segundos), se o número a contagem de forks e contagem de estrelas é menor que 1 mil o padrão é 2 horas (7200 segundos). Note também que o cache é limitado a um mínimo de 30 minutos e um máximo de 24 horas. diff --git a/docs/readme_tr.md b/docs/readme_tr.md index f654cd95cf252..296613d4b332a 100644 --- a/docs/readme_tr.md +++ b/docs/readme_tr.md @@ -160,6 +160,7 @@ dark, radical, merko, gruvbox, tokyonight, onedark, cobalt, synthwave, highcontr - `theme` - Temanın rengi [tüm temalar](./themes/README.md) - `cache_seconds` - Manuel olarak cache'i belirleyebilirsiniz _(en az: 14400, en fazla: 86400)_ - `locale` - Karttaki dili seçebilirsiniz _(örneğin; tr, cn, de, es, vb.)_ +- `json` - Ham verileri render etmek yerine ham JSON verileri çıktılar. Hata ayıklama veya diğer uygulamalarda veri kullanımı için yararlıdır. _(boolean)_ ##### bg_color'da Gradient diff --git a/readme.md b/readme.md index 363b008b2f8c2..0aa24b7839f9e 100644 --- a/readme.md +++ b/readme.md @@ -300,6 +300,7 @@ You can customize the appearance of all your cards however you wish with URL par | `cache_seconds` | Sets the cache header manually (min: 21600, max: 86400). | integer | `21600` | | `locale` | Sets the language in the card, you can check full list of available locales [here](#available-locales). | enum | `en` | | `border_radius` | Corner rounding on the card. | number | `4.5` | +| `json` | Outputs raw JSON data instead of rendering a card. Useful for debugging or data use in other apps. | boolean | `false` | > [!WARNING]\ > We use caching to decrease the load on our servers (see ). Our cards have a default cache of 6 hours (21600 seconds). Also, note that the cache is clamped to a minimum of 6 hours and a maximum of 24 hours. If you want the data on your statistics card to be updated more often you can [deploy your own instance](#deploy-on-your-own) and set [environment variable](#disable-rate-limit-protections) `CACHE_SECONDS` to a value of your choosing.