Link-Fixes: Sprachumschalter, Canonical, og:locale, Legal-Platzhalter, serve-Fallback

- Builder.cs: Sprachumschalter-Links verwenden /{lang}/{slug}.html statt
  /{lang}/{slug}/ (Dateien liegen flach als lang/slug.html)
- Builder.cs: Canonical/OG-URL enthält jetzt /{lang}/-Präfix
- Builder.cs: Legal-Seiten ersetzen {{intro}}, {{positioning}}, {{publisher}}
  mit leerem String (waren zuvor als Artefakte im HTML sichtbar)
- Builder.cs: Sitemap-URLs enthalten Sprach-Präfix + .html
- BuildConfig.cs: JsonNamingPolicy.SnakeCaseLower für korrekte Deserialisierung
  von snake_case JSON-Properties (og_locale → OgLocale, base_url → BaseUrl)
- Program.cs: webstatic serve hat .html-Fallback ( /de/slug → de/slug.html)
This commit is contained in:
2026-07-11 18:41:49 +02:00
parent 78063da79c
commit 7065c0773c
3 changed files with 34 additions and 12 deletions
+5 -1
View File
@@ -99,7 +99,11 @@ public class BuildConfig
try
{
var json = File.ReadAllText(configPath);
var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var opts = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
};
var fromFile = JsonSerializer.Deserialize<BuildConfig>(json, opts);
if (fromFile != null)
{
+20 -9
View File
@@ -105,8 +105,10 @@ public static class Builder
? bannerHtml + htmlContent
: htmlContent;
var canonical = page.Href.StartsWith("/") ? page.Href : $"/{page.Href}";
if (!canonical.EndsWith("/")) canonical += "/";
var slug = page.Href.Trim('/');
var canonical = string.IsNullOrEmpty(slug)
? $"/{lang}/"
: $"/{lang}/{slug}.html";
var defaultTitle = cfg.Site.Title.TryGetValue(lang, out var dt) ? dt : cfg.Site.Name;
var defaultDesc = cfg.Site.Description.TryGetValue(lang, out var dd) ? dd : "";
@@ -157,8 +159,8 @@ public static class Builder
var ldir = Path.Combine(Directories.DistDir, lang);
Directory.CreateDirectory(ldir);
var slug = page.Href.Trim('/');
var outputFile = string.IsNullOrEmpty(slug) ? "index.html" : $"{slug}.html";
var fileSlug = page.Href.Trim('/');
var outputFile = string.IsNullOrEmpty(fileSlug) ? "index.html" : $"{fileSlug}.html";
var outputPath = Path.Combine(ldir, outputFile);
File.WriteAllText(outputPath, pageHtml);
@@ -248,7 +250,7 @@ public static class Builder
var code = (li.HrefLang ?? lang).ToUpperInvariant();
string slug = "";
if (langSlug != null && langSlug.TryGetValue(lang, out var h)) slug = h.Trim('/');
var href = string.IsNullOrEmpty(slug) ? $"/{lang}/" : $"/{lang}/{slug}/";
var href = string.IsNullOrEmpty(slug) ? $"/{lang}/" : $"/{lang}/{slug}.html";
sb.AppendLine($" <li><a href=\"{href}\" hreflang=\"{li.HrefLang ?? lang}\"><span>{HtmlEncode(label)}</span> <span class=\"lang-dropdown__code\">{code}</span></a></li>");
}
return sb.ToString().Trim();
@@ -403,14 +405,15 @@ public static class Builder
htmlContent = MarkdownProcessor.TablePostprocess(htmlContent);
var baseName = Regex.Replace(bn, @"\.(de|en|vi)\.md$", ".html").Replace(".md", ".html");
var canonical = $"/{Path.GetFileNameWithoutExtension(baseName)}/";
var legalFileName = Path.GetFileNameWithoutExtension(baseName);
var canonical = $"/{lang}/{legalFileName}.html";
var defaultTitle = cfg.Site.Title.TryGetValue(lang, out var dt) ? dt : cfg.Site.Name;
var defaultDesc = cfg.Site.Description.TryGetValue(lang, out var dd) ? dd : "";
var seoHead = SeoHelper.RenderSeoHead(lang, title, "", $"{env.BaseUrl}{canonical}",
env.Cdn, defaultTitle, defaultDesc);
var sidebarTocHtml = BuildSidebarTocHtml(content);
var legalSlug = $"/{Path.GetFileNameWithoutExtension(baseName)}/";
var legalLangSlug = cfg.LanguageCodes().ToDictionary(l => l, l => legalSlug);
var legalSlug = $"{legalFileName}.html";
var legalLangSlug = cfg.LanguageCodes().ToDictionary(l => l, l => $"{l}/{legalFileName}.html");
var langLinksHtml = BuildLangLinksHtml(legalLangSlug);
var pageHtml = layout
@@ -425,6 +428,9 @@ public static class Builder
.Replace("{{lang_links}}", langLinksHtml).Replace("{{ lang_links }}", langLinksHtml)
.Replace("{{glossary_href}}", GlossaryHref(lang)).Replace("{{ glossary_href }}", GlossaryHref(lang))
.Replace("{{glossary_label}}", GlossaryLabel(lang)).Replace("{{ glossary_label }}", GlossaryLabel(lang))
.Replace("{{intro}}", "").Replace("{{ intro }}", "")
.Replace("{{positioning}}", "").Replace("{{ positioning }}", "")
.Replace("{{publisher}}", "").Replace("{{ publisher }}", "")
.Replace("{{content}}", htmlContent).Replace("{{ content }}", htmlContent)
.Replace("{{body}}", htmlContent).Replace("{{ body }}", htmlContent);
pageHtml = I18n.Substitute(pageHtml, lang);
@@ -452,7 +458,12 @@ public static class Builder
urls.Add($"{baseUrl}/{lang}/");
var contents = ContentLoader.LoadContent(Directories.ContentDir, lang);
foreach (var page in contents)
if (page.Meta.Render) urls.Add($"{baseUrl}{page.Href}");
{
if (!page.Meta.Render) continue;
var slug = page.Href.Trim('/');
if (!string.IsNullOrEmpty(slug))
urls.Add($"{baseUrl}/{lang}/{slug}.html");
}
}
var sitemap = @"<?xml version=""1.0"" encoding=""UTF-8""?>
<urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"">
+9 -2
View File
@@ -339,8 +339,15 @@ static async Task ServeAsync(string distDir, int port)
var file = Path.Combine(distDir, reqPath.TrimStart('/'));
if (!File.Exists(file))
{
file = Path.Combine(distDir, reqPath.TrimStart('/') + "/index.html");
if (!File.Exists(file)) file = Path.Combine(distDir, "404.html");
// Try appending .html (e.g. /de/slug → de/slug.html)
var htmlFile = file.TrimEnd('/') + ".html";
if (File.Exists(htmlFile))
file = htmlFile;
else
{
file = Path.Combine(distDir, reqPath.TrimStart('/') + "/index.html");
if (!File.Exists(file)) file = Path.Combine(distDir, "404.html");
}
}
if (File.Exists(file))
{