- Updated the router to ensure canonical URLs are correctly formatted, particularly for the home path. - Added middleware in the server to remove tracking/session query parameters, preventing duplicate URLs from being indexed by Google. - Set canonical link headers in SEO routes to assist search engines in URL attribution. These changes improve SEO performance and ensure cleaner URL structures across the application.
216 lines
6.8 KiB
JavaScript
216 lines
6.8 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', '/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');
|
|
}
|
|
});
|
|
|