SEO: JSON-LD NewsArticle + sitemap.xml + news sitemap + robots.txt + RSS feed
This commit is contained in:
parent
87cfbf743c
commit
a20c72e65e
@ -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, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
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());
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user