diff --git a/.changeset/brown-lights-show.md b/.changeset/brown-lights-show.md
new file mode 100644
index 000000000..06d3c1299
--- /dev/null
+++ b/.changeset/brown-lights-show.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-svelte': patch
+---
+
+fix(no-navigation-without-resolve): do not report if there is `data-sveltekit-reload` or `rel="external"` in ``
diff --git a/docs/rules/no-navigation-without-resolve.md b/docs/rules/no-navigation-without-resolve.md
index 05ea2ad9a..88dec8ac3 100644
--- a/docs/rules/no-navigation-without-resolve.md
+++ b/docs/rules/no-navigation-without-resolve.md
@@ -16,7 +16,13 @@ since: 'v3.12.0'
This rule reports navigation using HTML `` tags, SvelteKit's `goto()`, `pushState()` and `replaceState()` functions without resolving a relative URL. All four of these may be used for navigation, with `goto()`, `pushState()` and `replaceState()` being intended solely for internal navigation (i.e. not leaving the site), while `` tags may be used for both internal and external navigation. When using any way of internal navigation, the URL must be resolved using SvelteKit's `resolve()`, otherwise the site may break. For programmatic navigation to external URLs, using `window.location` is advised.
-This rule checks all 4 navigation options for the presence of the `resolve()` function call, with an exception for `` links to absolute URLs (and fragment URLs), which are assumed to be used for external navigation and so do not require the `resolve()` function, and for shallow routing functions with an empty string as the path, which keeps the current URL.
+This rule checks all 4 navigation options for the presence of the `resolve()` function call, with exceptions for:
+
+- `` links to absolute URLs (and fragment URLs), which are assumed to be used for external navigation and so do not require the `resolve()` function
+- `` links that opt out of SvelteKit navigation using `data-sveltekit-reload`
+- `` links with `rel="external"` (including multi-token values like `"external nofollow"`), which SvelteKit also treats as full-page navigations
+
+See SvelteKit link options for details: [Link options — data-sveltekit-reload](https://svelte.dev/docs/kit/link-options#data-sveltekit-reload).
@@ -52,6 +58,10 @@ This rule checks all 4 navigation options for the presence of the `resolve()` fu
Click me!
Click me!
+
+
+Click me!
+Click me!
```
## :wrench: Options
@@ -82,6 +92,7 @@ This rule checks all 4 navigation options for the presence of the `resolve()` fu
- [`goto()` documentation](https://svelte.dev/docs/kit/$app-navigation#goto)
- [`pushState()` documentation](https://svelte.dev/docs/kit/$app-navigation#pushState)
- [`replaceState()` documentation](https://svelte.dev/docs/kit/$app-navigation#replaceState)
+- [Link options — data-sveltekit-reload](https://svelte.dev/docs/kit/link-options#data-sveltekit-reload)
## :rocket: Version
diff --git a/packages/eslint-plugin-svelte/src/rules/no-navigation-without-resolve.ts b/packages/eslint-plugin-svelte/src/rules/no-navigation-without-resolve.ts
index 06ad91df8..c418b667a 100644
--- a/packages/eslint-plugin-svelte/src/rules/no-navigation-without-resolve.ts
+++ b/packages/eslint-plugin-svelte/src/rules/no-navigation-without-resolve.ts
@@ -95,6 +95,9 @@ export default createRule('no-navigation-without-resolve', {
) {
return;
}
+ if (anchorHasSveltekitReload(node) || anchorRelIncludesExternal(node)) {
+ return;
+ }
if (
(node.value[0].type === 'SvelteLiteral' &&
!expressionIsAbsolute(new FindVariableContext(context), node.value[0]) &&
@@ -373,3 +376,43 @@ function templateLiteralIsFragment(
function urlValueIsFragment(url: string): boolean {
return url.startsWith('#');
}
+
+function anchorHasSveltekitReload(node: AST.SvelteAttribute): boolean {
+ const startTag = node.parent;
+ return startTag.attributes.some((attr): attr is AST.SvelteAttribute => {
+ return attr.type === 'SvelteAttribute' && attr.key.name === 'data-sveltekit-reload';
+ });
+}
+
+function relTokenListIncludesExternal(value: string): boolean {
+ return /(?:^|\s)external(?:\s|$)/i.test(value);
+}
+
+function anchorRelIncludesExternal(node: AST.SvelteAttribute): boolean {
+ const startTag = node.parent;
+ const relAttr = startTag.attributes.find((attr): attr is AST.SvelteAttribute => {
+ return attr.type === 'SvelteAttribute' && attr.key.name === 'rel';
+ });
+ if (!relAttr) return false;
+ // Handle literal values like rel="external" or rel="external nofollow"
+ for (const v of relAttr.value) {
+ if (v.type === 'SvelteLiteral') {
+ if (relTokenListIncludesExternal(v.value)) return true;
+ }
+ if (v.type === 'SvelteMustacheTag') {
+ // Best-effort: detect simple string literals in mustache, e.g., rel={'external'}
+ const expr = v.expression;
+ if (expr.type === 'Literal' && typeof expr.value === 'string') {
+ if (relTokenListIncludesExternal(expr.value)) return true;
+ }
+ if (
+ expr.type === 'TemplateLiteral' &&
+ expr.expressions.length === 0 &&
+ expr.quasis.length === 1
+ ) {
+ if (relTokenListIncludesExternal(expr.quasis[0].value.raw)) return true;
+ }
+ }
+ }
+ return false;
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-partial-asset01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-partial-asset01-errors.yaml
index 7b8b81c68..2d1f90f38 100644
--- a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-partial-asset01-errors.yaml
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-partial-asset01-errors.yaml
@@ -1,8 +1,8 @@
-- message: Found a link with a url that isn't resolved.
+- message: Unexpected href link without resolve().
line: 5
column: 9
suggestions: null
-- message: Found a link with a url that isn't resolved.
+- message: Unexpected href link without resolve().
line: 6
column: 9
suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-rel-not-external-mustache01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-rel-not-external-mustache01-errors.yaml
new file mode 100644
index 000000000..12f99fa21
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-rel-not-external-mustache01-errors.yaml
@@ -0,0 +1,4 @@
+- message: Unexpected href link without resolve().
+ line: 1
+ column: 28
+ suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-rel-not-external-mustache01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-rel-not-external-mustache01-input.svelte
new file mode 100644
index 000000000..f1f795d6e
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-rel-not-external-mustache01-input.svelte
@@ -0,0 +1 @@
+Click me!
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-rel-not-external-template01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-rel-not-external-template01-errors.yaml
new file mode 100644
index 000000000..12f99fa21
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-rel-not-external-template01-errors.yaml
@@ -0,0 +1,4 @@
+- message: Unexpected href link without resolve().
+ line: 1
+ column: 28
+ suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-rel-not-external-template01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-rel-not-external-template01-input.svelte
new file mode 100644
index 000000000..9d129fefb
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-rel-not-external-template01-input.svelte
@@ -0,0 +1 @@
+Click me!
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-rel-not-external01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-rel-not-external01-errors.yaml
new file mode 100644
index 000000000..bc7ce2088
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-rel-not-external01-errors.yaml
@@ -0,0 +1,4 @@
+- message: Unexpected href link without resolve().
+ line: 1
+ column: 26
+ suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-rel-not-external01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-rel-not-external01-input.svelte
new file mode 100644
index 000000000..33cd93af7
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/invalid/link-rel-not-external01-input.svelte
@@ -0,0 +1 @@
+Click me!
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-rel-external-multi01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-rel-external-multi01-input.svelte
new file mode 100644
index 000000000..bc4be163a
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-rel-external-multi01-input.svelte
@@ -0,0 +1 @@
+Click me!
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-rel-external-mustache01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-rel-external-mustache01-input.svelte
new file mode 100644
index 000000000..ed7c85630
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-rel-external-mustache01-input.svelte
@@ -0,0 +1 @@
+Click me!
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-rel-external-template01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-rel-external-template01-input.svelte
new file mode 100644
index 000000000..a23b5a7cb
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-rel-external-template01-input.svelte
@@ -0,0 +1 @@
+Click me!
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-rel-external01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-rel-external01-input.svelte
new file mode 100644
index 000000000..459d1ff97
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-rel-external01-input.svelte
@@ -0,0 +1 @@
+Click me!
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-reload01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-reload01-input.svelte
new file mode 100644
index 000000000..c9eef28dd
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-navigation-without-resolve/valid/link-reload01-input.svelte
@@ -0,0 +1 @@
+Click me!