From ac87fa1969371f9ff389761e88eadb8a9a1b8982 Mon Sep 17 00:00:00 2001 From: leonace924 Date: Mon, 5 Jan 2026 19:58:52 -0500 Subject: [PATCH] feat: add rss title field and handle rss link from request --- db/knex_init_db.js | 1 + .../2026-01-05-0000-add-rss-title.js | 12 +++ server/model/status_page.js | 76 +++++++++++++++---- server/routers/status-page-router.js | 2 +- .../status-page-socket-handler.js | 1 + src/lang/en.json | 2 + src/pages/StatusPage.vue | 9 +++ 7 files changed, 87 insertions(+), 16 deletions(-) create mode 100644 db/knex_migrations/2026-01-05-0000-add-rss-title.js diff --git a/db/knex_init_db.js b/db/knex_init_db.js index 46bff4bfa..95a6fb55f 100644 --- a/db/knex_init_db.js +++ b/db/knex_init_db.js @@ -202,6 +202,7 @@ async function createTables() { table.text("footer_text"); table.text("custom_css"); table.boolean("show_powered_by").notNullable().defaultTo(true); + table.string("rss_title", 255); table.string("google_analytics_tag_id"); }); diff --git a/db/knex_migrations/2026-01-05-0000-add-rss-title.js b/db/knex_migrations/2026-01-05-0000-add-rss-title.js new file mode 100644 index 000000000..33f775c65 --- /dev/null +++ b/db/knex_migrations/2026-01-05-0000-add-rss-title.js @@ -0,0 +1,12 @@ +exports.up = function (knex) { + return knex.schema + .alterTable("status_page", function (table) { + table.string("rss_title", 255); + }); +}; + +exports.down = function (knex) { + return knex.schema.alterTable("status_page", function (table) { + table.dropColumn("rss_title"); + }); +}; diff --git a/server/model/status_page.js b/server/model/status_page.js index 500473587..ecb73088b 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -7,6 +7,7 @@ const analytics = require("../analytics/analytics"); const { marked } = require("marked"); const { Feed } = require("feed"); const config = require("../config"); +const { setting } = require("../util-server"); const { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE, DOWN } = require("../../src/util"); @@ -22,16 +23,17 @@ class StatusPage extends BeanModel { * Handle responses to RSS pages * @param {Response} response Response object * @param {string} slug Status page slug + * @param {Request} request Request object * @returns {Promise} */ - static async handleStatusPageRSSResponse(response, slug) { + static async handleStatusPageRSSResponse(response, slug, request) { let statusPage = await R.findOne("status_page", " slug = ? ", [ slug ]); if (statusPage) { response.type("application/rss+xml"); - response.send(await StatusPage.renderRSS(statusPage, slug)); + response.send(await StatusPage.renderRSS(statusPage, slug, request)); } else { response.status(404).send(UptimeKumaServer.getInstance().indexHTML); } @@ -64,20 +66,29 @@ class StatusPage extends BeanModel { /** * SSR for RSS feed - * @param {statusPage} statusPage object - * @param {slug} slug from router - * @returns {Promise} the rendered html + * @param {StatusPage} statusPage Status page object + * @param {string} slug Status page slug + * @param {Request} request Express request object + * @returns {Promise} The rendered RSS XML */ - static async renderRSS(statusPage, slug) { + static async renderRSS(statusPage, slug, request) { const { heartbeats, statusDescription } = await StatusPage.getRSSPageData(statusPage); - let proto = config.isSSL ? "https" : "http"; - let host = `${proto}://${config.hostname || "localhost"}:${config.port}/status/${slug}`; + // Build the feed URL, respecting proxy headers if trustProxy is enabled + const feedUrl = await StatusPage.buildRSSUrl(slug, request); + + // Use custom RSS title if set, otherwise fall back to status page title + let feedTitle = "Uptime Kuma RSS Feed"; + if (statusPage.rss_title) { + feedTitle = statusPage.rss_title; + } else if (statusPage.title) { + feedTitle = `${statusPage.title} RSS Feed`; + } const feed = new Feed({ - title: "uptime kuma rss feed", - description: `current status: ${statusDescription}`, - link: host, + title: feedTitle, + description: `Current status: ${statusDescription}`, + link: feedUrl, language: "en", // optional, used only in RSS 2.0, possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes updated: new Date(), // optional, default = today }); @@ -85,8 +96,9 @@ class StatusPage extends BeanModel { heartbeats.forEach(heartbeat => { feed.addItem({ title: `${heartbeat.name} is down`, - description: `${heartbeat.name} has been down since ${heartbeat.time}`, - id: heartbeat.monitorID, + description: `${heartbeat.name} has been down since ${heartbeat.time} UTC`, + id: `${heartbeat.monitorID}-${heartbeat.time}`, + link: feedUrl, date: new Date(heartbeat.time), }); }); @@ -94,6 +106,38 @@ class StatusPage extends BeanModel { return feed.rss2(); } + /** + * Build RSS feed URL, handling proxy headers + * @param {string} slug Status page slug + * @param {Request} request Express request object + * @returns {Promise} The full URL for the RSS feed + */ + static async buildRSSUrl(slug, request) { + if (request) { + const trustProxy = await setting("trustProxy"); + + // Determine protocol (check X-Forwarded-Proto if behind proxy) + let proto = request.protocol; + if (trustProxy && request.headers["x-forwarded-proto"]) { + proto = request.headers["x-forwarded-proto"].split(",")[0].trim(); + } + + // Determine host (check X-Forwarded-Host if behind proxy) + let host = request.get("host"); + if (trustProxy && request.headers["x-forwarded-host"]) { + host = request.headers["x-forwarded-host"]; + } + + return `${proto}://${host}/status/${slug}`; + } + + // Fallback to config values + const proto = config.isSSL ? "https" : "http"; + const host = config.hostname || "localhost"; + const port = config.port; + return `${proto}://${host}:${port}/status/${slug}`; + } + /** * SSR for status pages * @param {string} indexHTML HTML page to render @@ -415,7 +459,8 @@ class StatusPage extends BeanModel { analyticsScriptUrl: this.analytics_script_url, analyticsType: this.analytics_type, showCertificateExpiry: !!this.show_certificate_expiry, - showOnlyLastHeartbeat: !!this.show_only_last_heartbeat + showOnlyLastHeartbeat: !!this.show_only_last_heartbeat, + rssTitle: this.rss_title, }; } @@ -441,7 +486,8 @@ class StatusPage extends BeanModel { analyticsScriptUrl: this.analytics_script_url, analyticsType: this.analytics_type, showCertificateExpiry: !!this.show_certificate_expiry, - showOnlyLastHeartbeat: !!this.show_only_last_heartbeat + showOnlyLastHeartbeat: !!this.show_only_last_heartbeat, + rssTitle: this.rss_title, }; } diff --git a/server/routers/status-page-router.js b/server/routers/status-page-router.js index 6e57451f1..78a000be5 100644 --- a/server/routers/status-page-router.js +++ b/server/routers/status-page-router.js @@ -22,7 +22,7 @@ router.get("/status/:slug", cache("5 minutes"), async (request, response) => { router.get("/status/:slug/rss", cache("5 minutes"), async (request, response) => { let slug = request.params.slug; slug = slug.toLowerCase(); - await StatusPage.handleStatusPageRSSResponse(response, slug); + await StatusPage.handleStatusPageRSSResponse(response, slug, request); }); router.get("/status", cache("5 minutes"), async (request, response) => { diff --git a/server/socket-handlers/status-page-socket-handler.js b/server/socket-handlers/status-page-socket-handler.js index e42b13607..76f6775df 100644 --- a/server/socket-handlers/status-page-socket-handler.js +++ b/server/socket-handlers/status-page-socket-handler.js @@ -164,6 +164,7 @@ module.exports.statusPageSocketHandler = (socket) => { statusPage.footer_text = config.footerText; statusPage.custom_css = config.customCSS; statusPage.show_powered_by = config.showPoweredBy; + statusPage.rss_title = config.rssTitle; statusPage.show_only_last_heartbeat = config.showOnlyLastHeartbeat; statusPage.show_certificate_expiry = config.showCertificateExpiry; statusPage.modified_date = R.isoDateTime(); diff --git a/src/lang/en.json b/src/lang/en.json index 5bc546dd7..b7763bc61 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -388,6 +388,8 @@ "Proxy": "Proxy", "Date Created": "Date Created", "Footer Text": "Footer Text", + "RSS Title": "RSS Title", + "Leave blank to use status page title": "Leave blank to use status page title", "Refresh Interval": "Refresh Interval", "Refresh Interval Description": "The status page will do a full site refresh every {0} seconds", "Show Powered By": "Show Powered By", diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index eb7b7f780..14a20a931 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -34,6 +34,15 @@ + +
+ + +
+ {{ $t("Leave blank to use status page title") }} +
+
+