Implement SEO enhancements across documentation and application pages, including Open Graph and Twitter meta tags, improve robots.txt for AI crawlers, and streamline code formatting for better readability.

This commit is contained in:
Raj Nandan Sharma
2026-03-18 12:20:42 +05:30
parent 702ceca9b0
commit 3b07623346
22 changed files with 318 additions and 33 deletions
+2 -2
View File
@@ -87,11 +87,11 @@ Constants are exported as a **default export** from `src/lib/global-constants.ts
```typescript
// In Svelte/client code or SvelteKit routes:
import GC from "$lib/global-constants";
import GC from "$lib/global-constants"
// Usage: GC.UP, GC.DOWN, GC.DEGRADED, GC.MAINTENANCE, GC.NO_DATA
// In server code (use relative path):
import GC from "../../global-constants.js";
import GC from "../../global-constants.js"
// Usage: GC.UP, GC.DOWN, etc.
```
+1
View File
@@ -3,6 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta property="og:locale" content="en_US" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
@@ -399,9 +399,7 @@ async function removeTagFromGroupMonitors(tag: string): Promise<void> {
const weight = Math.round((1 / remaining.length) * 1000) / 1000;
for (let i = 0; i < remaining.length; i++) {
remaining[i].weight =
i === remaining.length - 1
? Math.round((1 - weight * (remaining.length - 1)) * 1000) / 1000
: weight;
i === remaining.length - 1 ? Math.round((1 - weight * (remaining.length - 1)) * 1000) / 1000 : weight;
}
}
@@ -573,13 +571,10 @@ export const GetBadge = async (badgeType: BadgeType, params: BadgeParams): Promi
}
}
const defaultLocale = i18nConfig?.defaultLocale || "en";
const activatedCodes = new Set(
i18nConfig?.locales?.filter((l) => l.selected).map((l) => l.code) ?? ["en"],
);
const activatedCodes = new Set(i18nConfig?.locales?.filter((l) => l.selected).map((l) => l.code) ?? ["en"]);
const requestedLocale = params.locale || defaultLocale;
const locale = activatedCodes.has(requestedLocale) && isLocaleAvailable(requestedLocale)
? requestedLocale
: defaultLocale;
const locale =
activatedCodes.has(requestedLocale) && isLocaleAvailable(requestedLocale) ? requestedLocale : defaultLocale;
const statusLocaleKey: Record<string, string> = {
[GC.UP]: "Operational",
+1
View File
@@ -20,6 +20,7 @@
<Toaster />
<svelte:head>
<meta name="robots" content="noindex, nofollow" />
{@html `<style>:root{--up:${colorUp};--degraded:${colorDegraded};--down:${colorDown};--maintenance:${colorMaintenance};}</style>`}
</svelte:head>
<main>
@@ -152,6 +152,37 @@
<meta property="og:title" content="{data.title} - Documentation" />
<meta property="og:description" content={data.description || `Documentation for ${data.title}`} />
<meta property="og:type" content="article" />
<meta property="article:author" content="https://github.com/rajnandan1" />
<link rel="canonical" href={`https://kener.ing/docs/${data.slug}`} />
<meta property="og:url" content={`https://kener.ing/docs/${data.slug}`} />
<meta name="twitter:card" content="summary_large_image" />
{@html `<script type="application/ld+json">${JSON.stringify({
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Documentation",
item: "https://kener.ing/docs"
},
...(data.group
? [
{
"@type": "ListItem",
position: 2,
name: data.group
}
]
: []),
{
"@type": "ListItem",
position: data.group ? 3 : 2,
name: data.title,
item: `https://kener.ing/docs/${data.slug}`
}
]
})}</script>`}
</svelte:head>
<div class="mx-auto flex justify-between gap-8">
+47 -4
View File
@@ -11,19 +11,62 @@
<svelte:head>
<title>Kener Documentation</title>
<!-- social preview og.jpg -->
<meta property="og:image" content="/og.jpg" />
<meta property="og:image" content="https://kener.ing/og.jpg" />
<meta property="og:title" content="Kener Documentation" />
<meta
property="og:description"
content="Comprehensive documentation for Kener, the open-source status page generator. Learn how to set up, customize, and manage your own status page with Kener."
content="Comprehensive documentation for Kener, the open-source status page system. Learn how to set up, customize, and manage your own status page with monitoring, incident management, and notifications."
/>
<meta property="og:site_name" content="Kener" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Kener Documentation" />
<meta
name="twitter:description"
content="Comprehensive documentation for Kener, the open-source status page generator. Learn how to set up, customize, and manage your own status page with Kener."
content="Comprehensive documentation for Kener, the open-source status page system. Learn how to set up, customize, and manage your own status page with monitoring, incident management, and notifications."
/>
<meta name="twitter:image" content="/og.jpg" />
<meta name="twitter:image" content="https://kener.ing/og.jpg" />
<meta name="author" content="Raj Nandan Sharma" />
<link rel="author" href="https://github.com/rajnandan1" />
{@html `<script type="application/ld+json">${JSON.stringify({
"@context": "https://schema.org",
"@graph": [
{
"@type": "Organization",
name: "Kener",
url: "https://kener.ing",
logo: "https://kener.ing/logo96.png",
sameAs: ["https://github.com/rajnandan1/kener"]
},
{
"@type": "SoftwareApplication",
name: "Kener",
applicationCategory: "DeveloperApplication",
operatingSystem: "Linux, macOS, Windows",
url: "https://kener.ing",
description:
"Open-source status page system built with SvelteKit. Features real-time monitoring (API, Ping, TCP, DNS, SSL, SQL, gRPC), incident management, maintenance scheduling, notifications (email, Slack, Discord, webhooks), embeddable widgets, and a REST API.",
offers: {
"@type": "Offer",
price: "0",
priceCurrency: "USD"
},
author: {
"@type": "Person",
name: "Raj Nandan Sharma",
url: "https://github.com/rajnandan1"
},
license: "https://opensource.org/licenses/MIT",
softwareVersion: "4.x",
downloadUrl: "https://github.com/rajnandan1/kener",
screenshot: "https://kener.ing/og.jpg"
},
{
"@type": "WebSite",
name: "Kener",
url: "https://kener.ing"
}
]
})}</script>`}
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Q3MLRXCBFT"></script>
<script>
+92 -2
View File
@@ -235,11 +235,101 @@
</script>
<svelte:head>
<title>Documentation - {data.config.name}</title>
<meta name="description" content="Documentation and guides for {data.config.name}" />
<title>{data.config.name} - Open Source Status Page System | Documentation</title>
<meta
name="description"
content="{data.config
.name} is an open-source status page system built with SvelteKit. Monitor APIs, Ping, TCP, DNS, SSL, SQL, and more. Features incident management, maintenance scheduling, notifications, embeddable widgets, and a complete REST API. Self-host with Docker or deploy to Railway and Zeabur."
/>
<link rel="icon" href={data.config.favicon} />
<link rel="canonical" href="https://kener.ing/docs" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://kener.ing/docs" />
<meta property="og:title" content="{data.config.name} - Open Source Status Page System | Documentation" />
<meta
property="og:description"
content="{data.config
.name} is an open-source status page system built with SvelteKit. Monitor APIs, Ping, TCP, DNS, SSL, SQL, and more. Features incident management, maintenance scheduling, notifications, and a complete REST API."
/>
<meta property="og:image" content="https://kener.ing/og.jpg" />
<meta property="og:site_name" content={data.config.name} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="{data.config.name} - Open Source Status Page System" />
<meta
name="twitter:description"
content="Open-source status page with monitoring, incident management, maintenance scheduling, and notifications. Self-host with Docker or deploy instantly."
/>
<meta name="twitter:image" content="https://kener.ing/og.jpg" />
{@html `<script type="application/ld+json">${JSON.stringify({
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: [
{
"@type": "Question",
name: "What is Kener?",
acceptedAnswer: {
"@type": "Answer",
text: "Kener is an open-source status page system built with SvelteKit and Node.js. It provides real-time monitoring, uptime tracking, incident management, maintenance scheduling, and customizable dashboards. You can self-host it with Docker or deploy to platforms like Railway and Zeabur."
}
},
{
"@type": "Question",
name: "What types of monitors does Kener support?",
acceptedAnswer: {
"@type": "Answer",
text: "Kener supports API (HTTP), Ping, TCP, DNS, SSL certificate, SQL database, Heartbeat, GameDig (game server), gRPC, and Group monitors. Each monitor type can be configured with custom check intervals, thresholds, and alerting rules."
}
},
{
"@type": "Question",
name: "How do I deploy Kener?",
acceptedAnswer: {
"@type": "Answer",
text: "Kener can be deployed using Docker, or one-click deployed to Railway or Zeabur. It supports SQLite (default), PostgreSQL, and MySQL databases. You need Node.js 20+ and Redis for the job queue."
}
},
{
"@type": "Question",
name: "Does Kener support incident management?",
acceptedAnswer: {
"@type": "Answer",
text: "Yes, Kener has full incident management with transparent timelines, status updates, acknowledgements, and clear communication workflows. You can create, update, and resolve incidents through the admin dashboard or the REST API."
}
},
{
"@type": "Question",
name: "Can I customize the look and feel of my Kener status page?",
acceptedAnswer: {
"@type": "Answer",
text: "Yes, Kener offers extensive customization including custom logos, colors, CSS, theme behavior (light/dark mode), localization (i18n), and multiple branded status pages from a single instance. It also supports embeddable widgets and badges."
}
},
{
"@type": "Question",
name: "Does Kener have an API?",
acceptedAnswer: {
"@type": "Answer",
text: "Yes, Kener provides a complete REST API (v4) for automating incidents, monitor operations, and reporting. API access is secured with Bearer token authentication and supports full CRUD operations."
}
},
{
"@type": "Question",
name: "What notification channels does Kener support?",
acceptedAnswer: {
"@type": "Answer",
text: "Kener supports notifications via email, webhooks, Slack, and Discord. It uses a trigger-based workflow system where you can configure smart conditions to route alerts and automate operational notifications."
}
},
{
"@type": "Question",
name: "Is Kener free and open source?",
acceptedAnswer: {
"@type": "Answer",
text: "Yes, Kener is fully free and open-source, licensed under the MIT license. The source code is available on GitHub at github.com/rajnandan1/kener."
}
}
]
})}</script>`}
</svelte:head>
<div class="docs-landing bg-background text-foreground min-h-screen">
@@ -31,7 +31,7 @@ Response: Array of todo objects with `completed` boolean field.
### Eval function:
```javascript
(async function (statusCode, responseTime, responseRaw) {
;(async function (statusCode, responseTime, responseRaw) {
if (statusCode !== 200) {
return { status: "DOWN", latency: responseTime }
}
@@ -81,7 +81,7 @@ SECRET_PARAM=your_real_secret_value
## Cheerio HTML content check {#cheerio-html-check-eval}
```javascript
(async function (statusCode, responseTime, responseRaw, modules) {
;(async function (statusCode, responseTime, responseRaw, modules) {
if (statusCode !== 200) {
return { status: "DOWN", latency: responseTime }
}
@@ -26,13 +26,13 @@ Connection errors and timeouts return **DOWN**.
## Configuration fields {#configuration-fields}
| Field | Type | Default | Notes |
| :-------- | :-------- | :------ | :--------------------------------------------- |
| `host` | `string` | — | Required |
| `port` | `number` | `50051` | Required |
| `service` | `string` | `""` | Fully qualified service name; empty = overall |
| `tls` | `boolean` | `false` | Use TLS credentials |
| `timeout` | `number` | `10000` | Request deadline in ms |
| Field | Type | Default | Notes |
| :-------- | :-------- | :------ | :-------------------------------------------- |
| `host` | `string` | — | Required |
| `port` | `number` | `50051` | Required |
| `service` | `string` | `""` | Fully qualified service name; empty = overall |
| `tls` | `boolean` | `false` | Use TLS credentials |
| `timeout` | `number` | `10000` | Request deadline in ms |
## Example {#example}
+1
View File
@@ -13,6 +13,7 @@
<Toaster />
<svelte:head>
<meta name="robots" content="noindex, nofollow" />
<title>Kener Status</title>
<link rel="icon" href={data.favicon} />
{#if data.font?.cssSrc}
+2
View File
@@ -133,6 +133,8 @@
<meta name="description" content={(data.pageDetails?.page_title || "Status Page") + " - Status Page"} />
<meta property="og:description" content={(data.pageDetails?.page_title || "Status Page") + " - Status Page"} />
{/if}
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
{#if data.socialPagePreviewImage}
<meta property="og:image" content={clientResolver(resolve, data.socialPagePreviewImage)} />
<meta name="twitter:image" content={clientResolver(resolve, data.socialPagePreviewImage)} />
@@ -133,6 +133,8 @@
<meta name="description" content={(data.pageDetails?.page_title || "Status Page") + " - Status Page"} />
<meta property="og:description" content={(data.pageDetails?.page_title || "Status Page") + " - Status Page"} />
{/if}
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
{#if data.socialPagePreviewImage}
<meta property="og:image" content={clientResolver(resolve, data.socialPagePreviewImage)} />
<meta name="twitter:image" content={clientResolver(resolve, data.socialPagePreviewImage)} />
@@ -159,6 +159,11 @@
<svelte:head>
<title>{currentMonth} - Maintenances & Incidents - {data.siteName}</title>
<meta name="description" content={`${currentMonth} maintenances and incidents for ${data.siteName}`} />
<meta property="og:title" content={`${currentMonth} - Maintenances & Incidents - ${data.siteName}`} />
<meta property="og:description" content={`${currentMonth} maintenances and incidents for ${data.siteName}`} />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
{#if data.socialPreviewImage}
<meta property="og:image" content={clientResolver(resolve, data.socialPreviewImage)} />
<meta name="twitter:image" content={clientResolver(resolve, data.socialPreviewImage)} />
@@ -159,6 +159,11 @@
<svelte:head>
<title>{currentMonth} - Maintenances & Incidents - {data.siteName}</title>
<meta name="description" content={`${currentMonth} maintenances and incidents for ${data.siteName}`} />
<meta property="og:title" content={`${currentMonth} - Maintenances & Incidents - ${data.siteName}`} />
<meta property="og:description" content={`${currentMonth} maintenances and incidents for ${data.siteName}`} />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
{#if data.socialPreviewImage}
<meta property="og:image" content={clientResolver(resolve, data.socialPreviewImage)} />
<meta name="twitter:image" content={clientResolver(resolve, data.socialPreviewImage)} />
@@ -20,9 +20,12 @@
<svelte:head>
<title>{data.incident.title + " - " + data.siteName}</title>
<!-- meta description -->
<meta property="og:title" content={data.incident.title + " - " + data.siteName} />
<meta property="og:type" content="article" />
<meta name="twitter:card" content="summary_large_image" />
{#if data.comments.length > 0}
<meta name="description" content={data.comments[0].comment} />
<meta property="og:description" content={data.comments[0].comment} />
{/if}
{#if data.socialPreviewImage}
<meta property="og:image" content={clientResolver(resolve, data.socialPreviewImage)} />
@@ -65,9 +65,12 @@
<svelte:head>
<title>{data.maintenance.title + " - " + data.siteName}</title>
<!-- meta description -->
<meta property="og:title" content={data.maintenance.title + " - " + data.siteName} />
<meta property="og:type" content="article" />
<meta name="twitter:card" content="summary_large_image" />
{#if data.maintenance.description}
<meta name="description" content={data.maintenance.description} />
<meta property="og:description" content={data.maintenance.description} />
{/if}
{#if data.socialPreviewImage}
<meta property="og:image" content={clientResolver(resolve, data.socialPreviewImage)} />
@@ -29,9 +29,12 @@
<svelte:head>
<title>{data.monitorName + " - " + data.siteName}</title>
<!-- meta description -->
<meta property="og:title" content={data.monitorName + " - " + data.siteName} />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
{#if data.monitorDescription}
<meta name="description" content={data.monitorDescription} />
<meta property="og:description" content={data.monitorDescription} />
{/if}
{#if data.socialPreviewImage}
<meta property="og:image" content={clientResolver(resolve, data.socialPreviewImage)} />
+1
View File
@@ -62,6 +62,7 @@
<Toaster />
<svelte:head>
<meta name="robots" content="noindex, nofollow" />
<title>{pageTitle} | Kener</title>
<link rel="icon" href={clientResolver(resolve, "/logo96.png")} />
{#if data.font?.cssSrc}
@@ -250,9 +250,7 @@
{/each}
</Select.Content>
</Select.Root>
<p class="text-muted-foreground text-xs">
Status text will be shown in the selected language
</p>
<p class="text-muted-foreground text-xs">Status text will be shown in the selected language</p>
</div>
{/if}
+11
View File
@@ -0,0 +1,11 @@
import { redirect } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { getDocsRootConfig } from "../(docs)/docs/docs-utils.server";
export const GET: RequestHandler = () => {
const rootConfig = getDocsRootConfig();
const latestVersion = rootConfig.versions.find((v) => v.latest) ?? rootConfig.versions[0];
const versionSlug = latestVersion?.slug ?? "v4";
throw redirect(301, `/docs/${versionSlug}/llms.txt`);
};
+53
View File
@@ -0,0 +1,53 @@
import type { RequestHandler } from "@sveltejs/kit";
import { getDocsRootConfig, getVersionDocsUrls } from "../(docs)/docs/docs-utils.server";
const BASE_DOMAIN = "https://kener.ing";
export const GET: RequestHandler = () => {
const rootConfig = getDocsRootConfig();
const latestVersion = rootConfig.versions.find((v) => v.latest) ?? rootConfig.versions[0];
const urls: { loc: string; priority: string; changefreq: string }[] = [
{ loc: `${BASE_DOMAIN}/docs`, priority: "1.0", changefreq: "weekly" },
];
if (latestVersion) {
const docsUrls = getVersionDocsUrls(latestVersion.slug, BASE_DOMAIN);
for (const url of docsUrls) {
// Skip raw markdown URLs and external URLs
if (url.includes("/docs/raw/") || !url.startsWith(BASE_DOMAIN)) continue;
urls.push({ loc: url, priority: "0.8", changefreq: "weekly" });
}
// Add llms.txt for AI discoverability
urls.push({
loc: `${BASE_DOMAIN}/docs/${latestVersion.slug}/llms.txt`,
priority: "0.5",
changefreq: "weekly",
});
}
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls
.map(
(u) => ` <url>
<loc>${escapeXml(u.loc)}</loc>
<changefreq>${u.changefreq}</changefreq>
<priority>${u.priority}</priority>
</url>`,
)
.join("\n")}
</urlset>`;
return new Response(xml, {
headers: {
"content-type": "application/xml; charset=utf-8",
"cache-control": "public, max-age=3600",
},
});
};
function escapeXml(str: string): string {
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
+38 -1
View File
@@ -1 +1,38 @@
User-agent: *
User-agent: *
Allow: /
Disallow: /manage/
Disallow: /account/
Disallow: /api/
Disallow: /embed/
# AI Search Engine Crawlers
User-agent: GPTBot
Allow: /
User-agent: ChatGPT-User
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: anthropic-ai
Allow: /
User-agent: PerplexityBot
Allow: /
User-agent: Google-Extended
Allow: /
User-agent: Googlebot
Allow: /
User-agent: Bingbot
Allow: /
Sitemap: https://kener.ing/sitemap.xml
# AI Documentation Index
# See https://llmstxt.org for specification
# /llms.txt redirects to the latest version