feat: add SEO support and favicon handling for Status Page

This commit is contained in:
Simon Larsen
2025-04-22 11:24:40 +01:00
parent 5b91fa5f4f
commit 86065c3d46
6 changed files with 129 additions and 27 deletions

View File

@@ -87,6 +87,97 @@ export default class StatusPageAPI extends BaseAPI<
public constructor() {
super(StatusPage, StatusPageService);
// get title, description of the page. This is used for SEO.
this.router.get(
`${new this.entityType().getCrudApiPath()?.toString()}/:statusPageId`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse) => {
const statusPageId: ObjectID = new ObjectID(
req.params["statusPageId"] as string,
);
const statusPage: StatusPage | null =
await StatusPageService.findOneBy({
query: {
_id: statusPageId,
},
select: {
pageTitle: true,
pageDescription: true,
},
props: {
isRoot: true,
},
});
if (!statusPage) {
return Response.sendErrorResponse(
req,
res,
new NotFoundException("Status Page not found"),
);
}
return Response.sendJsonObjectResponse(req, res, {
title: statusPage.pageTitle,
description: statusPage.pageDescription,
});
},
);
// favicon api.
this.router.get(
`${new this.entityType().getCrudApiPath()?.toString()}/favicon/:statusPageId`,
async (req: ExpressRequest, res: ExpressResponse) => {
const statusPageId: ObjectID = new ObjectID(
req.params["statusPageId"] as string,
);
const statusPage: StatusPage | null =
await StatusPageService.findOneBy({
query: {
_id: statusPageId,
},
select: {
faviconFile: {
file: true,
_id: true,
type: true,
name: true,
},
},
props: {
isRoot: true,
},
});
if (!statusPage) {
return Response.sendErrorResponse(
req,
res,
new NotFoundException("Status Page not found"),
);
}
if(!statusPage.faviconFile) {
// return default favicon.
return Response.sendFileByPath(
req,
res,
`/usr/src/Common/UI/Images/favicon/status-green.png`,
);
}
return Response.sendFileResponse(
req,
res,
statusPage.faviconFile!,
);
},
);
// confirm subscription api
this.router.get(
`${new this.entityType()

View File

@@ -142,6 +142,10 @@ export interface InitFuctionOptions {
port?: Port | undefined;
isFrontendApp?: boolean;
statusOptions: StatusAPIOptions;
getVariablesToRenderIndexPage?: (
req: ExpressRequest,
res: ExpressResponse,
) => Promise<JSONObject>;
}
type InitFunction = (
@@ -200,9 +204,30 @@ const init: InitFunction = async (
},
);
app.get("/*", (_req: ExpressRequest, res: ExpressResponse) => {
app.get("/*", async (_req: ExpressRequest, res: ExpressResponse) => {
let variables: JSONObject = {};
if(data.getVariablesToRenderIndexPage) {
try {
const variablesToRenderIndexPage: JSONObject =
await data.getVariablesToRenderIndexPage(
_req,
res,
);
variables = {
...variables,
...variablesToRenderIndexPage,
}
} catch (error) {
logger.error(error);
}
}
return res.render("/usr/src/app/views/index.ejs", {
enableGoogleTagManager: IsBillingEnabled || false,
...variables,
});
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -19,6 +19,13 @@ const init: PromiseVoidFunction = async (): Promise<void> => {
liveCheck: async () => {},
readyCheck: async () => {},
},
getVariablesToRenderIndexPage: async (_req, _res) => {
return {
title: "Status Page",
description: "Status Page",
faviconUrl: "/favicon.ico",
};
}
});
// add default routes

View File

@@ -20,13 +20,11 @@ import JSONWebTokenData from "Common/Types/JsonWebTokenData";
import Link from "Common/Types/Link";
import ObjectID from "Common/Types/ObjectID";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import { ImageFunctions } from "Common/UI/Components/Image/Image";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
import MasterPage from "Common/UI/Components/MasterPage/MasterPage";
import JSONWebToken from "Common/UI/Utils/JsonWebToken";
import LocalStorage from "Common/UI/Utils/LocalStorage";
import Navigation from "Common/UI/Utils/Navigation";
import File from "Common/Models/DatabaseModels/File";
import React, {
FunctionComponent,
ReactElement,
@@ -181,15 +179,6 @@ const DashboardMasterPage: FunctionComponent<ComponentProps> = (
setStatusPage(statusPage);
// setfavicon.
const favIcon: File | undefined = statusPage.faviconFile;
if (favIcon && favIcon.file) {
const link: any = document.createElement("link");
link.rel = "icon";
(document as any).getElementsByTagName("head")[0].appendChild(link);
link.href = ImageFunctions.getImageURL(favIcon);
}
// setcss.
const css: string | null = statusPage.customCSS || null;
if (css) {

View File

@@ -9,10 +9,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<meta name="slack-app-id" content="ACVBMTPJQ">
<meta name="description" content="OneUptime — the complete open-source observability platform.">
<meta name="description" content="<%= typeof description !== 'undefined' ? description : 'Status Page shows real-time status of all of our services.' %>">
<script src="/status-page/env.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="icon" type="image/png" href="<%= typeof faviconUrl !== 'undefined' ? faviconUrl : '/status-page/assets/images/favicon.png' %>">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
* {
@@ -47,7 +48,9 @@
}
</style>
<script src="/status-page/assets/js/tailwind-3.4.5.js"></script>
<title>Status Page</title>
<title><%= typeof title !== 'undefined' ? title : 'Status Page' %></title>
<% if(typeof enableGoogleTagManager !== 'undefined' ? enableGoogleTagManager : false){ %>
<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
@@ -59,19 +62,6 @@
<% } %>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
</head>
<body class="h-full bg-gray-50">
<% if(typeof enableGoogleTagManager !== 'undefined' ? enableGoogleTagManager : false){ %>