Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/anchor-links-same-tab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"emdash": patch
---

Fixes same-page anchor links (e.g. `#section`) in Portable Text content opening in a new tab. On-page jumps now always stay in the same tab, even when the link's "open in new window" option is set.
6 changes: 5 additions & 1 deletion packages/core/src/components/marks/Link.astro
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ export interface Props {

const { node } = Astro.props;
const href = sanitizeHref(node?.markDef?.href);
const blank = node?.markDef?.blank;
// Same-page anchor links (`#section`) are on-page jumps — opening them in a
// new tab is never desirable, so force same-tab regardless of the mark's
// `blank` flag.
const isSamePageAnchor = href.startsWith("#");
const blank = !isSamePageAnchor && node?.markDef?.blank;
---

<a
Expand Down
61 changes: 61 additions & 0 deletions packages/core/tests/repro/link-mark-anchor.render.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Same-page anchor links (`#section`) must never open in a new tab, even when
* the stored link mark has `blank: true`. Opening an on-page jump in a new tab
* is never desirable — it should stay in the same tab (`_self`).
*
* Regression: external links with `blank: true` must still open in a new tab.
*/
import { experimental_AstroContainer as AstroContainer } from "astro/container";
import { describe, expect, test } from "vitest";

import Link from "../../src/components/marks/Link.astro";

const anchorTag = (html: string) => html.match(/<a\b[^>]*>/)?.[0] ?? "";

async function renderLink(markDef: Record<string, unknown>) {
const c = await AstroContainer.create();
return c.renderToString(Link, {
props: { node: { markDef } },
slots: { default: "jump" },
});
}

describe("Link mark: same-page anchor target handling", () => {
test("same-page anchor with blank=true stays in same tab", async () => {
const html = await renderLink({
_type: "link",
_key: "k1",
href: "#section",
blank: true,
});
const tag = anchorTag(html);
expect(tag).toContain('href="#section"');
expect(tag).not.toContain('target="_blank"');
expect(tag).not.toContain("noopener");
});

test("external link with blank=true still opens in new tab", async () => {
const html = await renderLink({
_type: "link",
_key: "k2",
href: "https://example.com",
blank: true,
});
const tag = anchorTag(html);
expect(tag).toContain('href="https://example.com"');
expect(tag).toContain('target="_blank"');
expect(tag).toContain("noopener");
});

test("same-page anchor without blank stays in same tab", async () => {
const html = await renderLink({
_type: "link",
_key: "k3",
href: "#top",
blank: false,
});
const tag = anchorTag(html);
expect(tag).toContain('href="#top"');
expect(tag).not.toContain('target="_blank"');
});
});
Loading