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) => {
|
app.get("/api/focal-points", (_req, res) => {
|
||||||
res.json(getCachedFocalPoints());
|
res.json(getCachedFocalPoints());
|
||||||
});
|
});
|
||||||
|
|||||||
@ -63,6 +63,38 @@ export function serveStatic(app: Express) {
|
|||||||
let template = await fs.promises.readFile(indexPath, "utf-8");
|
let template = await fs.promises.readFile(indexPath, "utf-8");
|
||||||
template = stripExistingMeta(template);
|
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 = [
|
const ogTags = [
|
||||||
`<meta property="og:title" content="${escapeHtml(article.title)}" />`,
|
`<meta property="og:title" content="${escapeHtml(article.title)}" />`,
|
||||||
`<meta property="og:description" content="${escapeHtml(article.excerpt)}" />`,
|
`<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" />`,
|
`<meta name="keywords" content="Volksmusik, Schlager, ${escapeHtml(article.title)}, ${escapeHtml(article.category || 'News')}, FOLX TV" />`,
|
||||||
`<link rel="canonical" href="${escapeHtml(articleUrl)}" />`,
|
`<link rel="canonical" href="${escapeHtml(articleUrl)}" />`,
|
||||||
`<title>${escapeHtml(article.title)} - Volksmusik & Schlager | Folx Music Television</title>`,
|
`<title>${escapeHtml(article.title)} - Volksmusik & Schlager | Folx Music Television</title>`,
|
||||||
|
jsonLdTag,
|
||||||
].join("\n ");
|
].join("\n ");
|
||||||
|
|
||||||
template = template.replace(/<title>[^<]*<\/title>/, ogTags);
|
template = template.replace(/<title>[^<]*<\/title>/, ogTags);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user