Skip to content
Open
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
1 change: 1 addition & 0 deletions news/changelog-1.10.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ All changes included in 1.10:
### Websites

- ([#13565](https://github.com/quarto-dev/quarto-cli/issues/13565), [#14353](https://github.com/quarto-dev/quarto-cli/issues/14353)): Fix sidebar logo not appearing on secondary sidebars in multi-sidebar website layouts.
- ([#14493](https://github.com/quarto-dev/quarto-cli/issues/14493)): Fix category links on a post going to the wrong listing when the post appears in multiple listings.

## Commands

Expand Down
24 changes: 12 additions & 12 deletions src/resources/formats/html/quarto.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,22 +311,22 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
}

const findNearestParentListing = (href, listingHrefs) => {
if (!href || !listingHrefs) {
if (!href || !listingHrefs?.length) {
return undefined;
}
// Look up the tree for a nearby linting and use that if we find one
const relativeParts = href.substring(1).split("/");
while (relativeParts.length > 0) {
const path = relativeParts.join("/");
for (const listingHref of listingHrefs) {
if (listingHref.startsWith(path)) {
return listingHref;
}
// Match listings whose directory is a prefix of the post directory at a
// path-segment boundary, then keep the deepest one.
const postDir = href.replace(/[^/]+$/, "");
let best;
let bestLen = -1;
for (const listingHref of listingHrefs) {
const listingDir = listingHref.replace(/[^/]+$/, "");
if (postDir.startsWith(listingDir) && listingDir.length > bestLen) {
best = listingHref;
bestLen = listingDir.length;
}
relativeParts.pop();
}

return undefined;
return best;
};

const manageSidebarVisiblity = (el, placeholderDescriptor) => {
Expand Down
2 changes: 2 additions & 0 deletions tests/docs/playwright/blog/multi-listing-blog/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/.quarto/
/_site/
13 changes: 13 additions & 0 deletions tests/docs/playwright/blog/multi-listing-blog/_quarto.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
project:
type: website

website:
title: "Multi-listing blog"
navbar:
left:
- href: index.qmd
text: Home
- href: blog/index.qmd
text: Blog

format: html
8 changes: 8 additions & 0 deletions tests/docs/playwright/blog/multi-listing-blog/blog/index.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: "Blog"
listing:
contents: "*/index.qmd"
type: default
sort: "date desc"
categories: true
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: "Post one"
date: "2026-01-02"
categories:
- alpha
- beta
---

Body of post one.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: "Post two"
date: "2026-01-01"
categories:
- alpha
---

Body of post two.
15 changes: 15 additions & 0 deletions tests/docs/playwright/blog/multi-listing-blog/index.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
title: "Home"
listing:
id: latest
contents: blog/*/index.qmd
type: default
sort: "date desc"
filter-ui: false
sort-ui: false
---

Site root with a listing of latest posts.

::: {#latest}
:::
42 changes: 42 additions & 0 deletions tests/integration/playwright/tests/blog-multi-listing.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { expect, Page, test } from "@playwright/test";
import { getUrl } from "../src/utils";

// Regression test for https://github.com/quarto-dev/quarto-cli/issues/14493:
// when a post is in multiple listings, the post's category link must target
// the nearest parent listing, not just the first entry in listings.json.

const fixtureRoot = "blog/multi-listing-blog/_site";
const alphaHrefPattern = /\/blog\/index\.html#category=alpha$/;

const alphaLinkLocator = (page: Page) =>
page
.locator("header.quarto-title-block .quarto-category", { hasText: "alpha" })
.locator("a");

test("Category link on a post resolves to the nearest parent listing", async ({ page }) => {
await page.goto(`./${fixtureRoot}/blog/post-one/`);

await expect(alphaLinkLocator(page)).toHaveAttribute("href", alphaHrefPattern);

await alphaLinkLocator(page).click();
await expect(page).toHaveURL(
getUrl(`${fixtureRoot}/blog/index.html#category=alpha`),
);
await expect(
page.locator(
`div.category[data-category="${btoa(encodeURIComponent("alpha"))}"]`,
),
).toHaveClass(/active/);
await expect(page.getByRole("link", { name: "Post one", exact: true })).toBeVisible();
await expect(page.getByRole("link", { name: "Post two", exact: true })).toBeVisible();
});

test("Category link on a post does not point at the homepage listing", async ({ page }) => {
// Reach the post from the homepage so document.referrer triggers the
// fallback branch that previously selected the wrong listing.
await page.goto(`./${fixtureRoot}/`);
await page.getByRole("link", { name: "Post one", exact: true }).click();
await expect(page).toHaveURL(getUrl(`${fixtureRoot}/blog/post-one/`));

await expect(alphaLinkLocator(page)).toHaveAttribute("href", alphaHrefPattern);
});
Loading