SEO: JSON-LD NewsArticle + sitemap.xml + news sitemap + robots.txt + RSS feed

This commit is contained in:
Folx Ops 2026-06-14 06:43:33 +00:00
parent 87cfbf743c
commit a20c72e65e
2 changed files with 112 additions and 0 deletions

View File

@ -71,6 +71,85 @@ export async function registerRoutes(
);
});
// ---- SEO: robots.txt, sitemap.xml, news sitemap, RSS ----
const SITE = "https://folx.tv";
const xmlEscape = (s: string) =>
String(s || "")
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
app.get("/robots.txt", (_req, res) => {
res.type("text/plain").send(
[
"User-agent: *",
"Allow: /",
"",
`Sitemap: ${SITE}/sitemap.xml`,
`Sitemap: ${SITE}/news-sitemap.xml`,
"",
].join("\n")
);
});
app.get("/sitemap.xml", async (_req, res) => {
try {
const articles = await storage.getArticles();
const staticPaths = ["/", "/news", "/video", "/galerie", "/horoskop", "/rezepte", "/kontakt", "/impressum", "/datenschutz"];
const urls: string[] = [];
for (const p of staticPaths) {
urls.push(` <url>\n <loc>${SITE}${p}</loc>\n <changefreq>daily</changefreq>\n <priority>${p === "/" ? "1.0" : "0.7"}</priority>\n </url>`);
}
for (const a of articles) {
const lastmod = a.publishedAt ? new Date(a.publishedAt as any).toISOString() : new Date().toISOString();
urls.push(` <url>\n <loc>${SITE}/article/${xmlEscape(a.slug)}</loc>\n <lastmod>${lastmod}</lastmod>\n <changefreq>weekly</changefreq>\n <priority>0.8</priority>\n </url>`);
}
const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls.join("\n")}\n</urlset>`;
res.type("application/xml").send(xml);
} catch (e) {
res.status(500).type("application/xml").send('<?xml version="1.0"?><urlset/>');
}
});
app.get("/news-sitemap.xml", async (_req, res) => {
try {
const articles = await storage.getArticles();
const now = Date.now();
const twoDays = 48 * 60 * 60 * 1000;
const recent = articles.filter((a) => {
const t = a.publishedAt ? new Date(a.publishedAt as any).getTime() : now;
return now - t <= twoDays;
});
const urls = recent.map((a) => {
const pub = a.publishedAt ? new Date(a.publishedAt as any).toISOString() : new Date().toISOString();
return ` <url>\n <loc>${SITE}/article/${xmlEscape(a.slug)}</loc>\n <news:news>\n <news:publication>\n <news:name>Folx Music Television</news:name>\n <news:language>de</news:language>\n </news:publication>\n <news:publication_date>${pub}</news:publication_date>\n <news:title>${xmlEscape(a.title)}</news:title>\n </news:news>\n </url>`;
});
const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">\n${urls.join("\n")}\n</urlset>`;
res.type("application/xml").send(xml);
} catch (e) {
res.status(500).type("application/xml").send('<?xml version="1.0"?><urlset/>');
}
});
app.get(["/rss.xml", "/feed", "/feed.xml"], async (_req, res) => {
try {
const articles = (await storage.getArticles()).slice(0, 30);
const items = articles.map((a) => {
const pub = a.publishedAt ? new Date(a.publishedAt as any).toUTCString() : new Date().toUTCString();
const link = `${SITE}/article/${xmlEscape(a.slug)}`;
const img = a.coverImage ? (a.coverImage.startsWith("http") ? a.coverImage : SITE + a.coverImage) : "";
const enclosure = img ? `\n <enclosure url="${xmlEscape(img.replace(/\.webp$/, ".jpg"))}" type="image/jpeg" />` : "";
return ` <item>\n <title>${xmlEscape(a.title)}</title>\n <link>${link}</link>\n <guid isPermaLink="true">${link}</guid>\n <pubDate>${pub}</pubDate>\n <category>${xmlEscape(a.category || "News")}</category>\n <description>${xmlEscape(a.excerpt || "")}</description>${enclosure}\n </item>`;
});
const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">\n <channel>\n <title>Folx Music Television News</title>\n <link>${SITE}</link>\n <atom:link href="${SITE}/rss.xml" rel="self" type="application/rss+xml" />\n <description>Aktuelle Nachrichten aus der Welt der Volksmusik und des Schlagers.</description>\n <language>de-DE</language>\n <lastBuildDate>${new Date().toUTCString()}</lastBuildDate>\n${items.join("\n")}\n </channel>\n</rss>`;
res.type("application/rss+xml").send(xml);
} catch (e) {
res.status(500).type("application/rss+xml").send('<?xml version="1.0"?><rss/>');
}
});
app.get("/api/focal-points", (_req, res) => {
res.json(getCachedFocalPoints());
});

View File

@ -63,6 +63,38 @@ export function serveStatic(app: Express) {
let template = await fs.promises.readFile(indexPath, "utf-8");
template = stripExistingMeta(template);
const pubDate = article.publishedAt
? new Date(article.publishedAt as any).toISOString()
: new Date().toISOString();
const jsonLd = {
"@context": "https://schema.org",
"@type": "NewsArticle",
"headline": article.title,
"description": article.excerpt,
"image": [finalImage],
"datePublished": pubDate,
"dateModified": pubDate,
"articleSection": article.category || "News",
"author": {
"@type": "Organization",
"name": article.author || "Folx Music Television",
"url": canonicalBase,
},
"publisher": {
"@type": "Organization",
"name": "Folx Music Television",
"logo": {
"@type": "ImageObject",
"url": `${canonicalBase}/og-image.jpg`,
},
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": articleUrl,
},
};
const jsonLdTag = `<script type="application/ld+json">${JSON.stringify(jsonLd).replace(/</g, "\\u003c")}</script>`;
const ogTags = [
`<meta property="og:title" content="${escapeHtml(article.title)}" />`,
`<meta property="og:description" content="${escapeHtml(article.excerpt)}" />`,
@ -84,6 +116,7 @@ export function serveStatic(app: Express) {
`<meta name="keywords" content="Volksmusik, Schlager, ${escapeHtml(article.title)}, ${escapeHtml(article.category || 'News')}, FOLX TV" />`,
`<link rel="canonical" href="${escapeHtml(articleUrl)}" />`,
`<title>${escapeHtml(article.title)} - Volksmusik & Schlager | Folx Music Television</title>`,
jsonLdTag,
].join("\n ");
template = template.replace(/<title>[^<]*<\/title>/, ogTags);