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:
69
SEO-TODO.md
Normal file
69
SEO-TODO.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# SEO TODO (offene Punkte)
|
||||
|
||||
## 1) Host-/TLS-Konsistenz (Apex und www)
|
||||
|
||||
- [x] App-Fallback-Redirect in Node aktiv (`ypchat.net` + HTTP -> `https://www.ypchat.net`).
|
||||
|
||||
### Zertifikatserstellung (Let's Encrypt / Certbot, Apache)
|
||||
|
||||
1. DNS pruefen
|
||||
- `A/AAAA` fuer `ypchat.net` und `www.ypchat.net` muessen auf denselben Server zeigen.
|
||||
2. Certbot installieren (falls noch nicht vorhanden)
|
||||
- Ubuntu: `sudo apt update && sudo apt install certbot python3-certbot-apache`
|
||||
3. Zertifikat fuer beide Hosts ausstellen
|
||||
- `sudo certbot --apache -d ypchat.net -d www.ypchat.net`
|
||||
4. Auto-Renew testen
|
||||
- `sudo certbot renew --dry-run`
|
||||
5. Apache neu laden
|
||||
- `sudo systemctl reload apache2`
|
||||
|
||||
### Apache-Redirects (kanonischer Host + HTTPS)
|
||||
|
||||
Empfohlene Logik:
|
||||
- `http://ypchat.net/*` -> `https://www.ypchat.net/*` (301)
|
||||
- `http://www.ypchat.net/*` -> `https://www.ypchat.net/*` (301)
|
||||
- `https://ypchat.net/*` -> `https://www.ypchat.net/*` (301)
|
||||
- Nur `https://www.ypchat.net/*` liefert `200`
|
||||
|
||||
Beispiel (VirtualHost fuer Port 80):
|
||||
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName ypchat.net
|
||||
ServerAlias www.ypchat.net
|
||||
RewriteEngine On
|
||||
RewriteRule ^ https://www.ypchat.net%{REQUEST_URI} [R=301,L]
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
Beispiel (VirtualHost fuer `https://ypchat.net`):
|
||||
|
||||
```apache
|
||||
<VirtualHost *:443>
|
||||
ServerName ypchat.net
|
||||
SSLEngine on
|
||||
SSLCertificateFile /etc/letsencrypt/live/ypchat.net/fullchain.pem
|
||||
SSLCertificateKeyFile /etc/letsencrypt/live/ypchat.net/privkey.pem
|
||||
RewriteEngine On
|
||||
RewriteRule ^ https://www.ypchat.net%{REQUEST_URI} [R=301,L]
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
Verifikation:
|
||||
- `curl -I https://ypchat.net/` -> `301 Location: https://www.ypchat.net/`
|
||||
- `curl -I https://www.ypchat.net/` -> `200`
|
||||
- Browser ohne TLS-Warnung fuer beide Hosts
|
||||
|
||||
## 3) Search Console / Reindexing
|
||||
|
||||
- [ ] In Google Search Console `https://www.ypchat.net` als Hauptproperty nutzen.
|
||||
- [ ] Sitemap neu einreichen: `https://www.ypchat.net/sitemap.xml`.
|
||||
- [ ] Live-Tests ausfuehren fuer:
|
||||
- [ ] `/`
|
||||
- [ ] `/partners`
|
||||
- [ ] `/feedback`
|
||||
- [ ] Fuer diese URLs "Indexierung beantragen".
|
||||
- [ ] Nach 7-14 Tagen kontrollieren:
|
||||
- [ ] "Gecrawlt - zurzeit nicht indexiert"
|
||||
- [ ] "Gefunden - zurzeit nicht indexiert"
|
||||
- [ ] Impressionen/Klicks im Leistungsbericht.
|
||||
@@ -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');
|
||||
|
||||
@@ -75,6 +75,27 @@ const seoData = {
|
||||
}
|
||||
};
|
||||
|
||||
function buildSitemapXml() {
|
||||
const currentDate = new Date().toISOString().split('T')[0];
|
||||
const urls = Object.entries(seoData)
|
||||
.map(([route, meta]) => {
|
||||
const priority = route === '/' ? '1.0' : '0.8';
|
||||
const changefreq = route === '/' ? 'daily' : 'weekly';
|
||||
return ` <url>
|
||||
<loc>${meta.ogUrl}</loc>
|
||||
<lastmod>${currentDate}</lastmod>
|
||||
<changefreq>${changefreq}</changefreq>
|
||||
<priority>${priority}</priority>
|
||||
</url>`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${urls}
|
||||
</urlset>`;
|
||||
}
|
||||
|
||||
function escapeHtml(value = '') {
|
||||
return String(value)
|
||||
.replace(/&/g, '&')
|
||||
@@ -221,25 +242,22 @@ Sitemap: ${SITE_URL}/sitemap.xml
|
||||
});
|
||||
|
||||
app.get('/sitemap.xml', (req, res) => {
|
||||
const currentDate = new Date().toISOString().split('T')[0];
|
||||
const urls = Object.entries(seoData)
|
||||
.map(([route, meta]) => {
|
||||
const priority = route === '/' ? '1.0' : '0.8';
|
||||
const changefreq = route === '/' ? 'daily' : 'weekly';
|
||||
return ` <url>
|
||||
<loc>${meta.ogUrl}</loc>
|
||||
<lastmod>${currentDate}</lastmod>
|
||||
<changefreq>${changefreq}</changefreq>
|
||||
<priority>${priority}</priority>
|
||||
</url>`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
try {
|
||||
const sitemap = buildSitemapXml();
|
||||
// Stabilere Auslieferung fuer Crawler und Reverse-Proxy-Caches.
|
||||
res.set('Cache-Control', 'public, max-age=300, s-maxage=300, stale-while-revalidate=600');
|
||||
res.type('application/xml');
|
||||
res.status(200).send(sitemap);
|
||||
} catch (error) {
|
||||
console.error('[SEO] Fehler beim Generieren der sitemap.xml:', error);
|
||||
// Fallback: niemals 500 fuer die Sitemap ausliefern.
|
||||
const fallback = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${urls}
|
||||
<url><loc>${SITE_URL}/</loc></url>
|
||||
</urlset>`;
|
||||
res.type('application/xml');
|
||||
res.send(sitemap);
|
||||
res.set('Cache-Control', 'no-store');
|
||||
res.type('application/xml');
|
||||
res.status(200).send(fallback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user