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 try
{ {
var json = File.ReadAllText(configPath); 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); var fromFile = JsonSerializer.Deserialize<BuildConfig>(json, opts);
if (fromFile != null) if (fromFile != null)
{ {
+20 -9
View File
@@ -105,8 +105,10 @@ public static class Builder
? bannerHtml + htmlContent ? bannerHtml + htmlContent
: htmlContent; : htmlContent;
var canonical = page.Href.StartsWith("/") ? page.Href : $"/{page.Href}"; var slug = page.Href.Trim('/');
if (!canonical.EndsWith("/")) canonical += "/"; var canonical = string.IsNullOrEmpty(slug)
? $"/{lang}/"
: $"/{lang}/{slug}.html";
var defaultTitle = cfg.Site.Title.TryGetValue(lang, out var dt) ? dt : cfg.Site.Name; 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 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); var ldir = Path.Combine(Directories.DistDir, lang);
Directory.CreateDirectory(ldir); Directory.CreateDirectory(ldir);
var slug = page.Href.Trim('/'); var fileSlug = page.Href.Trim('/');
var outputFile = string.IsNullOrEmpty(slug) ? "index.html" : $"{slug}.html"; var outputFile = string.IsNullOrEmpty(fileSlug) ? "index.html" : $"{fileSlug}.html";
var outputPath = Path.Combine(ldir, outputFile); var outputPath = Path.Combine(ldir, outputFile);
File.WriteAllText(outputPath, pageHtml); File.WriteAllText(outputPath, pageHtml);
@@ -248,7 +250,7 @@ public static class Builder
var code = (li.HrefLang ?? lang).ToUpperInvariant(); var code = (li.HrefLang ?? lang).ToUpperInvariant();
string slug = ""; string slug = "";
if (langSlug != null && langSlug.TryGetValue(lang, out var h)) slug = h.Trim('/'); 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>"); 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(); return sb.ToString().Trim();
@@ -403,14 +405,15 @@ public static class Builder
htmlContent = MarkdownProcessor.TablePostprocess(htmlContent); htmlContent = MarkdownProcessor.TablePostprocess(htmlContent);
var baseName = Regex.Replace(bn, @"\.(de|en|vi)\.md$", ".html").Replace(".md", ".html"); 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 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 defaultDesc = cfg.Site.Description.TryGetValue(lang, out var dd) ? dd : "";
var seoHead = SeoHelper.RenderSeoHead(lang, title, "", $"{env.BaseUrl}{canonical}", var seoHead = SeoHelper.RenderSeoHead(lang, title, "", $"{env.BaseUrl}{canonical}",
env.Cdn, defaultTitle, defaultDesc); env.Cdn, defaultTitle, defaultDesc);
var sidebarTocHtml = BuildSidebarTocHtml(content); var sidebarTocHtml = BuildSidebarTocHtml(content);
var legalSlug = $"/{Path.GetFileNameWithoutExtension(baseName)}/"; var legalSlug = $"{legalFileName}.html";
var legalLangSlug = cfg.LanguageCodes().ToDictionary(l => l, l => legalSlug); var legalLangSlug = cfg.LanguageCodes().ToDictionary(l => l, l => $"{l}/{legalFileName}.html");
var langLinksHtml = BuildLangLinksHtml(legalLangSlug); var langLinksHtml = BuildLangLinksHtml(legalLangSlug);
var pageHtml = layout var pageHtml = layout
@@ -425,6 +428,9 @@ public static class Builder
.Replace("{{lang_links}}", langLinksHtml).Replace("{{ lang_links }}", langLinksHtml) .Replace("{{lang_links}}", langLinksHtml).Replace("{{ lang_links }}", langLinksHtml)
.Replace("{{glossary_href}}", GlossaryHref(lang)).Replace("{{ glossary_href }}", GlossaryHref(lang)) .Replace("{{glossary_href}}", GlossaryHref(lang)).Replace("{{ glossary_href }}", GlossaryHref(lang))
.Replace("{{glossary_label}}", GlossaryLabel(lang)).Replace("{{ glossary_label }}", GlossaryLabel(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("{{content}}", htmlContent).Replace("{{ content }}", htmlContent)
.Replace("{{body}}", htmlContent).Replace("{{ body }}", htmlContent); .Replace("{{body}}", htmlContent).Replace("{{ body }}", htmlContent);
pageHtml = I18n.Substitute(pageHtml, lang); pageHtml = I18n.Substitute(pageHtml, lang);
@@ -452,7 +458,12 @@ public static class Builder
urls.Add($"{baseUrl}/{lang}/"); urls.Add($"{baseUrl}/{lang}/");
var contents = ContentLoader.LoadContent(Directories.ContentDir, lang); var contents = ContentLoader.LoadContent(Directories.ContentDir, lang);
foreach (var page in contents) 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""?> var sitemap = @"<?xml version=""1.0"" encoding=""UTF-8""?>
<urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9""> <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('/')); var file = Path.Combine(distDir, reqPath.TrimStart('/'));
if (!File.Exists(file)) if (!File.Exists(file))
{ {
file = Path.Combine(distDir, reqPath.TrimStart('/') + "/index.html"); // Try appending .html (e.g. /de/slug → de/slug.html)
if (!File.Exists(file)) file = Path.Combine(distDir, "404.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)) if (File.Exists(file))
{ {