From 39802948db71a8d927f5477261b481b2d7856d7f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:08:55 +0000 Subject: [PATCH] feat: add HTML landing page for SVG cards with project footer Implement content negotiation in all API endpoints via a new `sendResponse` utility. When a card URL is accessed via a browser (Accept: text/html), it now serves a styled HTML page that: - Centers the SVG card. - Adds a footer in the bottom-left with the text "This is a gitlyy a readme stats generator." - Links the word "gitlyy" to the project's GitHub repository. Non-browser requests continue to receive the raw SVG. Includes `Vary: Accept` header for correct CDN caching. Co-authored-by: Chintanpatel24 <216989679+Chintanpatel24@users.noreply.github.com> --- api/commits.js | 8 ++--- api/contribution.js | 8 ++--- api/issues.js | 8 ++--- api/languages.js | 8 ++--- api/master.js | 8 ++--- api/music.js | 6 ++-- api/overview.js | 8 ++--- api/pr-stats.js | 8 ++--- api/profile.js | 8 ++--- api/streak.js | 8 ++--- src/response.js | 75 +++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 114 insertions(+), 39 deletions(-) create mode 100644 src/response.js diff --git a/api/commits.js b/api/commits.js index c7be225..9da405d 100644 --- a/api/commits.js +++ b/api/commits.js @@ -14,19 +14,19 @@ const { } = require("../src/svg-commits"); const { parseCardWidth } = require("../src/width"); const { getCache, setCache, clearCache } = require("../src/cache"); +const { sendResponse } = require("../src/response"); const CACHE_TTL = 30 * 60 * 1000; // 30 minutes module.exports = async (req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET"); - res.setHeader("Content-Type", "image/svg+xml"); res.setHeader("Cache-Control", "public, max-age=1800, s-maxage=1800, stale-while-revalidate=600"); const { username, theme, hide_border, layout, bg_color, title_color, text_color, border_color, title, width, refresh } = req.query; if (!username) { - res.status(400).send(errorSVG("Missing username")); + sendResponse(req, res, errorSVG("Missing username"), 400); return; } @@ -66,10 +66,10 @@ module.exports = async (req, res) => { cardWidth: parseCardWidth(width, 460, 400, 1200), }); - res.status(200).send(svg); + sendResponse(req, res, svg); } catch (error) { console.error("Commits Error:", error.message); - res.status(200).send(errorSVG("Failed to load commits data")); + sendResponse(req, res, errorSVG("Failed to load commits data"), 200); } }; diff --git a/api/contribution.js b/api/contribution.js index cbaef4a..ce60628 100644 --- a/api/contribution.js +++ b/api/contribution.js @@ -13,19 +13,19 @@ const { generateContributionPulseSVG, } = require("../src/svg-contribution"); const { getCache, setCache, clearCache } = require("../src/cache"); +const { sendResponse } = require("../src/response"); const CACHE_TTL = 30 * 60 * 1000; // 30 minutes module.exports = async (req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET"); - res.setHeader("Content-Type", "image/svg+xml"); res.setHeader("Cache-Control", "public, max-age=1800, s-maxage=1800, stale-while-revalidate=600"); const { username, theme, hide_border, layout, bg_color, title_color, text_color, border_color, title, width, refresh } = req.query; if (!username) { - res.status(400).send(errorSVG("Missing username")); + sendResponse(req, res, errorSVG("Missing username"), 400); return; } @@ -85,10 +85,10 @@ module.exports = async (req, res) => { hideBorder: hide_border === "true", title, }); - res.status(200).send(svg); + sendResponse(req, res, svg); } catch (error) { console.error("Contribution Error:", error.message); - res.status(200).send(errorSVG("Failed to load contribution data")); + sendResponse(req, res, errorSVG("Failed to load contribution data"), 200); } }; diff --git a/api/issues.js b/api/issues.js index 71ea0cf..f11c87b 100644 --- a/api/issues.js +++ b/api/issues.js @@ -10,19 +10,19 @@ const { getTheme, applyColorOverrides } = require("../src/themes"); const { generateIssuesSVG } = require("../src/svg-issues"); const { parseCardWidth } = require("../src/width"); const { getCache, setCache, clearCache } = require("../src/cache"); +const { sendResponse } = require("../src/response"); const CACHE_TTL = 30 * 60 * 1000; // 30 minutes module.exports = async (req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET"); - res.setHeader("Content-Type", "image/svg+xml"); res.setHeader("Cache-Control", "public, max-age=1800, s-maxage=1800, stale-while-revalidate=600"); const { username, theme, hide_border, bg_color, title_color, text_color, border_color, width, refresh } = req.query; if (!username) { - res.status(400).send(errorSVG("Missing username")); + sendResponse(req, res, errorSVG("Missing username"), 400); return; } @@ -76,10 +76,10 @@ module.exports = async (req, res) => { cardWidth: parseCardWidth(width, 460, 400, 1200), }); - res.status(200).send(svg); + sendResponse(req, res, svg); } catch (error) { console.error("Issues Stats Error:", error.message); - res.status(200).send(errorSVG("Failed to load issues data")); + sendResponse(req, res, errorSVG("Failed to load issues data"), 200); } }; diff --git a/api/languages.js b/api/languages.js index 3274b9a..e8d6e9e 100644 --- a/api/languages.js +++ b/api/languages.js @@ -19,13 +19,13 @@ const { generateLanguageDonutByCommitsSVG, } = require("../src/svg-language"); const { getCache, setCache, clearCache } = require("../src/cache"); +const { sendResponse } = require("../src/response"); const CACHE_TTL = 30 * 60 * 1000; // 30 minutes module.exports = async (req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET"); - res.setHeader("Content-Type", "image/svg+xml"); res.setHeader("Cache-Control", "public, max-age=1800, s-maxage=1800, stale-while-revalidate=600"); const { @@ -43,7 +43,7 @@ module.exports = async (req, res) => { } = req.query; if (!username) { - res.status(400).send(errorSVG("Missing username")); + sendResponse(req, res, errorSVG("Missing username"), 400); return; } @@ -108,10 +108,10 @@ module.exports = async (req, res) => { }); } - res.status(200).send(svg); + sendResponse(req, res, svg); } catch (error) { console.error("Language Error:", error.message); - res.status(200).send(errorSVG("Failed to load language data")); + sendResponse(req, res, errorSVG("Failed to load language data"), 200); } }; diff --git a/api/master.js b/api/master.js index 0638ac1..45af7b7 100644 --- a/api/master.js +++ b/api/master.js @@ -22,6 +22,7 @@ const { const { getTheme, applyColorOverrides } = require("../src/themes"); const { generateMasterCardSVG } = require("../src/svg-master"); const { getCache, setCache, clearCache } = require("../src/cache"); +const { sendResponse } = require("../src/response"); const CACHE_TTL = 30 * 60 * 1000; @@ -38,13 +39,12 @@ const safe = async (promise, fallback) => { module.exports = async (req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET"); - res.setHeader("Content-Type", "image/svg+xml"); res.setHeader("Cache-Control", "public, max-age=1800, s-maxage=1800, stale-while-revalidate=600"); const { username, theme, hide_border, bg_color, title_color, text_color, border_color, refresh } = req.query; if (!username) { - res.status(400).send(errorSVG("Missing username")); + sendResponse(req, res, errorSVG("Missing username"), 400); return; } @@ -166,10 +166,10 @@ module.exports = async (req, res) => { hideBorder: hide_border === "true", }); - res.status(200).send(svg); + sendResponse(req, res, svg); } catch (error) { console.error("Mastercard API Error:", error.message); - res.status(200).send(errorSVG("Failed to load dashboard data")); + sendResponse(req, res, errorSVG("Failed to load dashboard data"), 200); } }; diff --git a/api/music.js b/api/music.js index f60080c..29b1d0e 100644 --- a/api/music.js +++ b/api/music.js @@ -5,11 +5,11 @@ const { getTheme, applyColorOverrides } = require("../src/themes"); const { generateMusicSVG } = require("../src/svg-music"); +const { sendResponse } = require("../src/response"); module.exports = async (req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET"); - res.setHeader("Content-Type", "image/svg+xml"); res.setHeader("Cache-Control", "public, max-age=3600, s-maxage=3600, stale-while-revalidate=1800"); const { @@ -40,10 +40,10 @@ module.exports = async (req, res) => { hideBorder: hide_border === "true", }); - res.status(200).send(svg); + sendResponse(req, res, svg); } catch (error) { console.error("Music Card Error:", error.message); - res.status(200).send(errorSVG("Failed to load music card")); + sendResponse(req, res, errorSVG("Failed to load music card"), 200); } }; diff --git a/api/overview.js b/api/overview.js index 3c1e9e1..97b4b46 100644 --- a/api/overview.js +++ b/api/overview.js @@ -17,6 +17,7 @@ const { const { getTheme, applyColorOverrides } = require("../src/themes"); const { generateOverviewSVG } = require("../src/svg-overview"); const { getCache, setCache, clearCache } = require("../src/cache"); +const { sendResponse } = require("../src/response"); const CACHE_TTL = 30 * 60 * 1000; @@ -35,13 +36,12 @@ function parseMaxPRs(value, defaultValue = 20, hardLimit = 50) { module.exports = async (req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET"); - res.setHeader("Content-Type", "image/svg+xml"); res.setHeader("Cache-Control", "public, max-age=1800, s-maxage=1800, stale-while-revalidate=600"); const { username, theme, hide_border, bg_color, title_color, text_color, border_color, refresh, lines_scope, max_prs } = req.query; if (!username) { - res.status(400).send(errorSVG("Missing username")); + sendResponse(req, res, errorSVG("Missing username"), 400); return; } @@ -114,10 +114,10 @@ module.exports = async (req, res) => { hideBorder: hide_border === "true", }); - res.status(200).send(svg); + sendResponse(req, res, svg); } catch (error) { console.error("Overview Error:", error.message); - res.status(200).send(errorSVG("Failed to load overview data")); + sendResponse(req, res, errorSVG("Failed to load overview data"), 200); } }; diff --git a/api/pr-stats.js b/api/pr-stats.js index 76ec731..a0e8d5e 100644 --- a/api/pr-stats.js +++ b/api/pr-stats.js @@ -10,19 +10,19 @@ const { getTheme, applyColorOverrides } = require("../src/themes"); const { generatePRCardSVG, generatePRSummarySVG } = require("../src/svg-pr"); const { parseCardWidth } = require("../src/width"); const { getCache, setCache, clearCache } = require("../src/cache"); +const { sendResponse } = require("../src/response"); const CACHE_TTL = 30 * 60 * 1000; // 30 minutes module.exports = async (req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET"); - res.setHeader("Content-Type", "image/svg+xml"); res.setHeader("Cache-Control", "public, max-age=1800, s-maxage=1800, stale-while-revalidate=600"); const { username, theme, hide_border, layout, bg_color, title_color, text_color, border_color, title, width, refresh } = req.query; if (!username) { - res.status(400).send(errorSVG("Missing username")); + sendResponse(req, res, errorSVG("Missing username"), 400); return; } @@ -100,10 +100,10 @@ module.exports = async (req, res) => { cardWidth: parseCardWidth(width, 460, 400, 1200), }); - res.status(200).send(svg); + sendResponse(req, res, svg); } catch (error) { console.error("PR Stats Error:", error.message); - res.status(200).send(errorSVG("Failed to load PR data")); + sendResponse(req, res, errorSVG("Failed to load PR data"), 200); } }; diff --git a/api/profile.js b/api/profile.js index ef64196..d11dae7 100644 --- a/api/profile.js +++ b/api/profile.js @@ -11,19 +11,19 @@ const { getTheme, applyColorOverrides } = require("../src/themes"); const { generateProfileSVG } = require("../src/svg-profile"); const { parseCardWidth } = require("../src/width"); const { getCache, setCache, clearCache } = require("../src/cache"); +const { sendResponse } = require("../src/response"); const CACHE_TTL = 2 * 60 * 60 * 1000; // 2 hours module.exports = async (req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET"); - res.setHeader("Content-Type", "image/svg+xml"); res.setHeader("Cache-Control", "public, max-age=7200, s-maxage=7200, stale-while-revalidate=3600"); const { username, theme, hide_border, bg_color, title_color, text_color, border_color, width, refresh } = req.query; if (!username) { - res.status(400).send(errorSVG("Missing username")); + sendResponse(req, res, errorSVG("Missing username"), 400); return; } @@ -83,10 +83,10 @@ module.exports = async (req, res) => { cardWidth: parseCardWidth(width, 460, 420, 1200), }); - res.status(200).send(svg); + sendResponse(req, res, svg); } catch (error) { console.error("Profile Error:", error.message); - res.status(200).send(errorSVG("Failed to load profile data")); + sendResponse(req, res, errorSVG("Failed to load profile data"), 200); } }; diff --git a/api/streak.js b/api/streak.js index 8d40196..cd1cef0 100644 --- a/api/streak.js +++ b/api/streak.js @@ -10,19 +10,19 @@ const { fetchContributionData, fetchTotalCommitCount } = require("../src/github" const { getTheme, applyColorOverrides } = require("../src/themes"); const { generateStreakSVG } = require("../src/svg-streak"); const { getCache, setCache, clearCache } = require("../src/cache"); +const { sendResponse } = require("../src/response"); const CACHE_TTL = 30 * 60 * 1000; module.exports = async (req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET"); - res.setHeader("Content-Type", "image/svg+xml"); res.setHeader("Cache-Control", "public, max-age=1800, s-maxage=1800, stale-while-revalidate=600"); const { username, theme, hide_border, bg_color, title_color, text_color, border_color, refresh } = req.query; if (!username) { - res.status(400).send(errorSVG("Missing username")); + sendResponse(req, res, errorSVG("Missing username"), 400); return; } @@ -79,10 +79,10 @@ module.exports = async (req, res) => { hideBorder: hide_border === "true", }); - res.status(200).send(svg); + sendResponse(req, res, svg); } catch (error) { console.error("Streak Error:", error.message); - res.status(200).send(errorSVG("Failed to load streak data")); + sendResponse(req, res, errorSVG("Failed to load streak data"), 200); } }; diff --git a/src/response.js b/src/response.js new file mode 100644 index 0000000..c97c9c9 --- /dev/null +++ b/src/response.js @@ -0,0 +1,75 @@ +/** + * Utility to send SVG response or an HTML wrapper based on the Accept header. + */ + +function sendResponse(req, res, svg, status = 200) { + const accept = req.headers.accept || ""; + + if (accept.includes("text/html")) { + res.setHeader("Content-Type", "text/html"); + res.setHeader("Vary", "Accept"); + const html = ` + + +
+ + +