Implement SEO improvements and enforce HTTPS redirection in production

- Added a middleware to enforce HTTPS and canonical host for known public hosts, enhancing security and SEO.
- Introduced a function to generate the sitemap.xml dynamically, improving the delivery of SEO data to crawlers.
- Updated the sitemap route to utilize the new function, ensuring stable delivery and error handling.

These changes collectively enhance the application's SEO capabilities and ensure secure access in production environments.
This commit is contained in:
Torsten Schulz (local)
2026-03-27 11:35:50 +01:00
parent 9b079e31a0
commit bb13779c72
3 changed files with 148 additions and 20 deletions

View File

@@ -20,6 +20,8 @@ const server = createServer(app);
const NODE_ENV = process.env.NODE_ENV || 'development';
const PORT = process.env.PORT || (NODE_ENV === 'production' ? 4000 : 3300);
const IS_PRODUCTION = NODE_ENV === 'production';
const PRIMARY_HOST = 'www.ypchat.net';
const LEGACY_HOSTS = new Set(['ypchat.net']);
// CORS-Origins konfigurieren
const allowedOrigins = IS_PRODUCTION
@@ -83,6 +85,38 @@ app.use(session({
// Trust Proxy für Apache Reverse Proxy (muss vor Routes stehen)
if (IS_PRODUCTION) {
app.set('trust proxy', 1); // Vertraue dem ersten Proxy (Apache)
// SEO-Fallback: erzwinge kanonischen Host + HTTPS, falls der Proxy es nicht bereits tut.
app.use((req, res, next) => {
const forwardedHost = String(req.headers['x-forwarded-host'] || '')
.split(',')[0]
.trim()
.toLowerCase();
const rawHost = String(req.headers.host || '')
.split(':')[0]
.trim()
.toLowerCase();
const host = forwardedHost || rawHost;
const forwardedProto = String(req.headers['x-forwarded-proto'] || '')
.split(',')[0]
.trim()
.toLowerCase();
const isHttps = forwardedProto ? forwardedProto === 'https' : req.secure;
const isKnownPublicHost = host === PRIMARY_HOST || LEGACY_HOSTS.has(host);
if (!isKnownPublicHost) {
next();
return;
}
if (!isHttps || host !== PRIMARY_HOST) {
res.redirect(301, `https://${PRIMARY_HOST}${req.originalUrl || '/'}`);
return;
}
next();
});
}
// Statische Dateien aus docroot
@@ -102,6 +136,7 @@ setupBroadcast(io, __dirname);
// SPA-Fallback muss nach allen anderen Routen stehen
if (IS_PRODUCTION) {
const distPath = join(__dirname, '../docroot/dist');
const KNOWN_SPA_ROUTES = new Set(['/', '/partners', '/feedback', '/mockup-redesign']);
app.use(express.static(distPath));
// Fallback für Vue Router (SPA) - muss am Ende stehen
app.get('*', (req, res) => {
@@ -114,8 +149,14 @@ if (IS_PRODUCTION) {
res.status(404).send('Not found');
return;
}
// Nur für nicht-API und nicht-static Requests
if (!req.path.startsWith('/api') && !req.path.startsWith('/static') && req.path !== '/robots.txt' && req.path !== '/sitemap.xml') {
// Nur bekannte SPA-Routen als index.html ausliefern, unbekannte Routen mit 404 beantworten
if (
!req.path.startsWith('/api') &&
!req.path.startsWith('/static') &&
req.path !== '/robots.txt' &&
req.path !== '/sitemap.xml' &&
KNOWN_SPA_ROUTES.has(req.path)
) {
res.sendFile(join(distPath, 'index.html'));
} else {
res.status(404).send('Not found');