diff --git a/.gitattributes b/.gitattributes index bdb0cabc..956bc5ed 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,12 @@ # Auto detect text files and perform LF normalization * text=auto +# Visual Studio solution files are canonically CRLF. Pin the line-ending +# so cross-platform edits (mixed-OS contributors, scripted regenerators +# that emit LF, editors that drop the trailing CR on save) do not drift +# the file off the format Visual Studio + MSBuild expect. +*.sln text eol=crlf + # Custom for Visual Studio *.cs diff=csharp diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 7e59f6be..233313d1 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -2,6 +2,14 @@ import { defineConfig } from 'vitepress'; import { withMermaid } from 'vitepress-plugin-mermaid'; import { apiSidebar, referenceSidebar } from './sidebar'; +// Absolute base URL for the deployed site. Social-card crawlers (LinkedIn, +// Slack unfurl, the classic X crawler) require absolute `https://…` URLs in +// og:image / twitter:image; root-relative paths are silently discarded. +// Override at workflow level via DOCS_CANONICAL_URL for custom domains. +const docsCanonicalUrl = + process.env.DOCS_CANONICAL_URL ?? 'https://trakhound.github.io/MTConnect.NET'; +const ogImage = `${docsCanonicalUrl}/logo.png`; + /** * VitePress config for the MTConnect.NET documentation site. * @@ -62,7 +70,7 @@ export default withMermaid( 'The .NET implementation of the MTConnect Standard — 100% public-surface API coverage, 100% MTConnect Standard compliance.', }, ], - ['meta', { property: 'og:image', content: '/logo.png' }], + ['meta', { property: 'og:image', content: ogImage }], ['meta', { name: 'twitter:card', content: 'summary_large_image' }], ['meta', { name: 'twitter:title', content: 'MTConnect.NET' }], [ @@ -72,7 +80,7 @@ export default withMermaid( content: 'The .NET implementation of the MTConnect Standard.', }, ], - ['meta', { name: 'twitter:image', content: '/logo.png' }], + ['meta', { name: 'twitter:image', content: ogImage }], ], themeConfig: { diff --git a/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs b/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs index 7b0486cc..87d479ec 100644 --- a/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs +++ b/tests/MTConnect.NET-Docs-Tests/RouteCheckTests.cs @@ -302,9 +302,13 @@ public async Task Landing_Page_Carries_The_House_Style_Surfaces() "favicon type attribute is not 'image/png'"); // Open Graph — title + image surface so social previews render. + // og:image must be an absolute https:// URL; LinkedIn, Slack unfurl, + // and the classic X crawler reject root-relative paths for social cards. Assert.That(probes.OgTitle, Is.EqualTo("MTConnect.NET"), "og:title meta does not match the expected site title"); Assert.That(probes.OgImage, Is.Not.Null.And.Not.Empty, "og:image meta missing"); + Assert.That(probes.OgImage, Does.StartWith("https://"), + $"og:image must be an absolute https:// URL for social-card crawlers — got '{probes.OgImage}'"); Assert.That(probes.OgImage, Does.EndWith("/logo.png"), $"og:image does not point at /logo.png — got '{probes.OgImage}'"); @@ -312,6 +316,8 @@ public async Task Landing_Page_Carries_The_House_Style_Surfaces() Assert.That(probes.TwitterCard, Is.EqualTo("summary_large_image"), "twitter:card meta is not 'summary_large_image'"); Assert.That(probes.TwitterImage, Is.Not.Null.And.Not.Empty, "twitter:image meta missing"); + Assert.That(probes.TwitterImage, Does.StartWith("https://"), + $"twitter:image must be an absolute https:// URL for social-card crawlers — got '{probes.TwitterImage}'"); Assert.That(probes.TwitterImage, Does.EndWith("/logo.png"), $"twitter:image does not point at /logo.png — got '{probes.TwitterImage}'");