Repository: HiDeoo/starlight-links-validator (ts)

Issue:
[bug]: Invalid link alto correct when using Astro's `base` option and multilingual site
### Describe the bug

When I use the links validator in a Starlight project with the following settings:

- multilingual
- has a [`base` option](https://docs.astro.build/en/reference/configuration-reference/#base) set
- [`errorOnFallbackPages: false`](https://starlight-links-validator.vercel.app/configuration#erroronfallbackpages)
- [`errorOnInconsistentLocale: true`](https://starlight-links-validator.vercel.app/configuration#erroroninconsistentlocale)

The plugin throws an error for **fallback** pages.

### To Reproduce

1. Clone https://github.com/trueberryless/repros/tree/starlight-links-validator-base-i18n-wrong-invalid-link
2. Run `pnpm i` and `pnpm dev`
3. Visit http://localhost:4321/base-option/zh-cn/
4. Click on the Link and see that it works in dev mode
5. Stop the dev server
6. Run `pnpm build` and see that links-validator throws unexpected error message: `Invalid link`

### Expected behavior

A link with the correct base option and locale which links to a fallback page with these settings should be valid.

### How often does this bug happen?

Every time

### System Info

_No response_

### Additional Context

_No response_

Pull request:
Fix issue with i18n + Astro `base` + `errorOnFallbackPages` set to `false`
Close #97 

Patch:
diff --git a/.changeset/rotten-apricots-invent.md b/.changeset/rotten-apricots-invent.md
new file mode 100644
index 0000000..590c2b6
--- /dev/null
+++ b/.changeset/rotten-apricots-invent.md
@@ -0,0 +1,5 @@
+---
+'starlight-links-validator': patch
+---
+
+Fixes validation issue with the [Astro `base` option](https://docs.astro.build/en/reference/configuration-reference/#base) and the [`errorOnFallbackPages` plugin option](https://starlight-links-validator.vercel.app/configuration#erroronfallbackpages) set to `false` in a multilingual project.
diff --git a/packages/starlight-links-validator/libs/i18n.ts b/packages/starlight-links-validator/libs/i18n.ts
index b506170..ecc9357 100644
--- a/packages/starlight-links-validator/libs/i18n.ts
+++ b/packages/starlight-links-validator/libs/i18n.ts
@@ -1,4 +1,4 @@
-import { ensureLeadingSlash, ensureTrailingSlash } from './path'
+import { ensureLeadingSlash, ensureTrailingSlash, stripLeadingSlash } from './path'
 import type { Headings } from './remark'
 import type { StarlightUserConfig } from './validation'
 
@@ -32,14 +32,20 @@ export function getFallbackHeadings(
   path: string,
   headings: Headings,
   localeConfig: LocaleConfig | undefined,
+  base: string,
 ): string[] | undefined {
   if (!localeConfig) return
 
+  const isPathWithBase = base !== '/'
+  const normalizedBase = isPathWithBase ? ensureTrailingSlash(stripLeadingSlash(base)) : ''
+  const normalizedPath = isPathWithBase ? path.replace(normalizedBase, '') : path
+
   for (const locale of localeConfig.locales) {
-    if (path.startsWith(`${locale}/`)) {
+    if (normalizedPath.startsWith(`${locale}/`)) {
       const fallbackPath = path.replace(
-        new RegExp(`^${locale}/`),
-        localeConfig.defaultLocale === '' ? localeConfig.defaultLocale : `${localeConfig.defaultLocale}/`,
+        new RegExp(`^${normalizedBase}${locale}/`),
+        normalizedBase +
+          (localeConfig.defaultLocale === '' ? localeConfig.defaultLocale : `${localeConfig.defaultLocale}/`),
       )
 
       return headings.get(fallbackPath === '' ? '/' : fallbackPath)
diff --git a/packages/starlight-links-validator/libs/validation.ts b/packages/starlight-links-validator/libs/validation.ts
index cf8c1f4..030e585 100644
--- a/packages/starlight-links-validator/libs/validation.ts
+++ b/packages/starlight-links-validator/libs/validation.ts
@@ -195,11 +195,11 @@ function validateLink(context: ValidationContext) {
   }
 }
 
-function getFileHeadings(path: string, { headings, localeConfig, options }: ValidationContext) {
+function getFileHeadings(path: string, { astroConfig, headings, localeConfig, options }: ValidationContext) {
   let heading = headings.get(path === '' ? '/' : path)
 
   if (!options.errorOnFallbackPages && !heading && localeConfig) {
-    heading = getFallbackHeadings(path, headings, localeConfig)
+    heading = getFallbackHeadings(path, headings, localeConfig, astroConfig.base)
   }
 
   return heading


Test patch:
diff --git a/packages/starlight-links-validator/tests/base-path.test.ts b/packages/starlight-links-validator/tests/base-path.test.ts
index 0171bcb..6d00e49 100644
--- a/packages/starlight-links-validator/tests/base-path.test.ts
+++ b/packages/starlight-links-validator/tests/base-path.test.ts
@@ -9,7 +9,7 @@ test('validates links when the `base` Astro option is set', async () => {
 
   expect(status).toBe('error')
 
-  expectValidationErrorCount(output, 16, 1)
+  expectValidationErrorCount(output, 20, 2)
 
   expectValidationErrors(output, 'test/test/', [
     ['/guides/example', ValidationErrorType.InvalidLink],
@@ -29,4 +29,11 @@ test('validates links when the `base` Astro option is set', async () => {
     ['/guides/page-with-custom-slug', ValidationErrorType.InvalidLink],
     ['/guides/page-with-custom-slug/', ValidationErrorType.InvalidLink],
   ])
+
+  expectValidationErrors(output, 'test/fr/test/', [
+    ['/guides/example', ValidationErrorType.InvalidLink],
+    ['/guides/example/', ValidationErrorType.InvalidLink],
+    ['/guides/example#description', ValidationErrorType.InvalidLink],
+    ['/guides/example/#description', ValidationErrorType.InvalidLink],
+  ])
 })
diff --git a/packages/starlight-links-validator/tests/fixtures/base-path/astro.config.ts b/packages/starlight-links-validator/tests/fixtures/base-path/astro.config.ts
index e2f2259..fe15bda 100644
--- a/packages/starlight-links-validator/tests/fixtures/base-path/astro.config.ts
+++ b/packages/starlight-links-validator/tests/fixtures/base-path/astro.config.ts
@@ -6,9 +6,13 @@ export default defineConfig({
   base: '/test',
   integrations: [
     starlight({
+      locales: {
+        root: { label: 'English', lang: 'en' },
+        fr: { label: 'Français', lang: 'fr' },
+      },
       pagefind: false,
-      plugins: [starlightLinksValidator()],
-      title: 'Starlight Links Validator Tests - trailing always',
+      plugins: [starlightLinksValidator({ errorOnFallbackPages: false })],
+      title: 'Starlight Links Validator Tests - base path',
     }),
   ],
 })
diff --git a/packages/starlight-links-validator/tests/fixtures/base-path/src/content/docs/fr/test.md b/packages/starlight-links-validator/tests/fixtures/base-path/src/content/docs/fr/test.md
new file mode 100644
index 0000000..5c0514c
--- /dev/null
+++ b/packages/starlight-links-validator/tests/fixtures/base-path/src/content/docs/fr/test.md
@@ -0,0 +1,17 @@
+---
+title: Test
+---
+
+# Links to a fallback page
+
+- [Example page](/test/fr/guides/example)
+- [Example page](/test/fr/guides/example/)
+
+- [Example page with hash](/test/fr/guides/example#description)
+- [Example page with hash](/test/fr/guides/example/#description)
+
+- [Example page with missing base](/guides/example)
+- [Example page with missing base](/guides/example/)
+
+- [Example page with missing base and hash](/guides/example#description)
+- [Example page with missing base and hash](/guides/example/#description)