Files
singlechat/server/index.js
Torsten Schulz (local) 33bf162798 Enhance Ratgeber section with new routes and SEO metadata
- Expanded the KNOWN_SPA_ROUTES in index.js to include new Ratgeber paths for improved navigation.
- Added detailed SEO metadata for the new Ratgeber routes in routes-seo.js, enhancing search engine visibility and user engagement.
- Updated robots.txt to allow indexing of the new Ratgeber pages, ensuring they are discoverable by search engines.

These changes collectively improve the site's informational resources and enhance SEO performance for chat-related guidance.
2026-04-27 14:27:34 +02:00

229 lines
6.9 KiB
JavaScript

import express from 'express';
import { createServer } from 'http';
import { Server as SocketIOServer } from 'socket.io';
import cookieParser from 'cookie-parser';
import session from 'express-session';
import cors from 'cors';
import { fileURLToPath, URL as NodeURL } from 'url';
import { dirname, join } from 'path';
import { setupBroadcast } from './broadcast.js';
import { setupRoutes } from './routes.js';
import { setupSEORoutes } from './routes-seo.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const app = express();
const server = createServer(app);
// Umgebungsvariablen
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
? ['https://ypchat.net', 'https://www.ypchat.net']
: ['http://localhost:5175', 'http://127.0.0.1:5175'];
// Socket.IO auf dem gleichen HTTP-Server wie Express
const io = new SocketIOServer(server, {
cors: {
origin: allowedOrigins,
credentials: true,
methods: ['GET', 'POST']
},
transports: ['websocket', 'polling'],
allowEIO3: true,
maxHttpBufferSize: 10 * 1024 * 1024, // 10MB für große Bilder (Base64-kodiert)
pingTimeout: 60000,
pingInterval: 25000
});
console.log('Socket.IO Server initialisiert auf Express-Server');
console.log('Umgebung:', NODE_ENV);
console.log('CORS erlaubt für:', allowedOrigins);
// CORS-Konfiguration
app.use(cors({
origin: (origin, callback) => {
// Erlaube Requests ohne Origin (z.B. Postman, mobile Apps)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin) || !IS_PRODUCTION) {
callback(null, true);
} else {
callback(new Error('Nicht erlaubt durch CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
// Session-Konfiguration
const sessionSecret = process.env.SESSION_SECRET || 'singlechat-secret-key-change-in-production';
app.use(session({
secret: sessionSecret,
resave: false,
saveUninitialized: false,
cookie: {
secure: IS_PRODUCTION, // true für HTTPS in Production
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000, // 24 Stunden
sameSite: IS_PRODUCTION ? 'lax' : false // Lax für HTTPS, false für Development
}
}));
// 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();
});
// Tracking-/Session-Query entfernen (wtd, js=no), damit Google keine Duplikat-URLs indexiert.
// Suchkonsole: "Alternative Seite mit richtigem kanonischen Tag" fuer /?wtd=... verschwindet nach Neu-Crawl.
app.use((req, res, next) => {
if (req.method !== 'GET' && req.method !== 'HEAD') {
next();
return;
}
if (req.path.startsWith('/api')) {
next();
return;
}
const host = req.get('host') || 'localhost';
const proto = req.get('x-forwarded-proto') || req.protocol || 'https';
const raw = req.originalUrl.split('#')[0];
let u;
try {
u = new NodeURL(raw, `${proto}://${host}`);
} catch {
next();
return;
}
const hadWtd = u.searchParams.has('wtd');
const hadJsNo = u.searchParams.get('js') === 'no';
if (!hadWtd && !hadJsNo) {
next();
return;
}
if (hadWtd) u.searchParams.delete('wtd');
if (hadJsNo) u.searchParams.delete('js');
const search = u.searchParams.toString();
const dest = u.pathname + (search ? `?${search}` : '');
if (dest !== raw) {
res.redirect(301, dest);
return;
}
next();
});
}
// Statische Dateien aus docroot
app.use('/static', express.static(join(__dirname, '../docroot')));
// SEO-Routes (robots.txt, sitemap.xml, Pre-Rendering)
// Müssen vor anderen Routes stehen, damit sie nicht vom SPA-Fallback abgefangen werden
setupSEORoutes(app, __dirname);
// API Routes (müssen vor SPA-Fallback stehen)
setupRoutes(app, __dirname);
// Socket.IO-Handling
setupBroadcast(io, __dirname);
// In Production: Serviere auch die gebauten Client-Dateien
// 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',
'/faq',
'/regeln',
'/sicherheit',
'/ratgeber',
'/ratgeber/erste-nachricht',
'/ratgeber/profil-tipps',
'/ratgeber/sicher-chatten',
'/ratgeber/red-flags',
'/mockup-redesign'
]);
app.use(express.static(distPath));
// Fallback für Vue Router (SPA) - muss am Ende stehen
app.get('*', (req, res) => {
// Hinweis: SEO-Routen (/, /partners, …) werden bereits von setupSEORoutes registriert
// und greifen vor diesem Catch-All. Kein frühes return ohne res.* hier.
// In Production: /src/ Pfade sollten nicht existieren (404)
if (IS_PRODUCTION && req.path.startsWith('/src/')) {
res.status(404).send('Not found');
return;
}
// 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');
}
});
}
// Server starten
const HOST = '127.0.0.1'; // Nur localhost, da Apache als Reverse Proxy fungiert
server.listen(PORT, HOST, () => {
console.log(`Server läuft auf http://${HOST}:${PORT}`);
console.log(`Umgebung: ${NODE_ENV}`);
console.log(`CORS erlaubt für: ${allowedOrigins.join(', ')}`);
if (IS_PRODUCTION) {
console.log('Production-Modus: HTTPS über Apache Reverse Proxy');
}
});