diff --git a/news/changelog-1.6.md b/news/changelog-1.6.md
index 65e73206dd..0c341c8285 100644
--- a/news/changelog-1.6.md
+++ b/news/changelog-1.6.md
@@ -80,6 +80,7 @@ All changes included in 1.6:
### Websites
- ([#2671](https://github.com/quarto-dev/quarto-cli/issues/2671)): Ensure that `--output-dir` works across filesystem boundaries.
+- ([#8517](https://github.com/quarto-dev/quarto-cli/issues/8571)), ([#10829](https://github.com/quarto-dev/quarto-cli/issues/10829)): Allow listing categories with non-alphanumeric characters such as apostrophes, etc.
- ([#8932](https://github.com/quarto-dev/quarto-cli/issues/8932)): Escape render ids in markdown pipeline to allow special characters in sidebars/navbars, etc.
- ([#10616](https://github.com/quarto-dev/quarto-cli/issues/10268)): Add a `z-index` setting to the 'back to top' button to ensure it is always visible.
diff --git a/src/project/types/website/listing/website-listing-categories.ts b/src/project/types/website/listing/website-listing-categories.ts
index 1da089c8c9..911aeeaca0 100644
--- a/src/project/types/website/listing/website-listing-categories.ts
+++ b/src/project/types/website/listing/website-listing-categories.ts
@@ -1,9 +1,8 @@
/*
-* website-listing-categories.ts
-*
-* Copyright (C) 2020-2022 Posit Software, PBC
-*
-*/
+ * website-listing-categories.ts
+ *
+ * Copyright (C) 2020-2022 Posit Software, PBC
+ */
import { Document } from "deno_dom/deno-dom-wasm-noinit.ts";
import {
kListingPageCategoryAll,
@@ -118,7 +117,7 @@ function categoryElement(
categoryEl.classList.add("category");
categoryEl.setAttribute(
"data-category",
- value !== undefined ? value : category,
+ value !== undefined ? btoa(value) : btoa(category),
);
categoryEl.innerHTML = contents;
return categoryEl;
diff --git a/src/project/types/website/listing/website-listing-template.ts b/src/project/types/website/listing/website-listing-template.ts
index 1e6afe3740..c3e270db03 100644
--- a/src/project/types/website/listing/website-listing-template.ts
+++ b/src/project/types/website/listing/website-listing-template.ts
@@ -454,7 +454,8 @@ export function reshapeListing(
attr["index"] = (index++).toString();
if (item.categories) {
- attr["categories"] = (item.categories as string[]).join(",");
+ const str = (item.categories as string[]).join(",");
+ attr["categories"] = btoa(str);
}
// Add magic attributes for the sortable values
diff --git a/src/resources/projects/website/listing/item-default.ejs.md b/src/resources/projects/website/listing/item-default.ejs.md
index 5e5afa6dfa..d39324c671 100644
--- a/src/resources/projects/website/listing/item-default.ejs.md
+++ b/src/resources/projects/website/listing/item-default.ejs.md
@@ -56,7 +56,7 @@ print(`
${listing.utilities.outputLi
<% if (fields.includes('categories') && item.categories) { %>
<% for (const category of item.categories) { %>
-
<%= category %>
+
<%= category %>
<% } %>
<% } %>
diff --git a/src/resources/projects/website/listing/item-grid.ejs.md b/src/resources/projects/website/listing/item-grid.ejs.md
index 7c0c92bd65..8c2423bcd0 100644
--- a/src/resources/projects/website/listing/item-grid.ejs.md
+++ b/src/resources/projects/website/listing/item-grid.ejs.md
@@ -64,7 +64,7 @@ return !["title", "image", "image-alt", "date", "author", "subtitle", "descripti
<% for (const category of item.categories) { %>
-
<%= category %>
+
<%= category %>
<% } %>
diff --git a/src/resources/projects/website/listing/quarto-listing.js b/src/resources/projects/website/listing/quarto-listing.js
index 88d209a9cc..ac3817ac0b 100644
--- a/src/resources/projects/website/listing/quarto-listing.js
+++ b/src/resources/projects/website/listing/quarto-listing.js
@@ -2,6 +2,7 @@ const kProgressiveAttr = "data-src";
let categoriesLoaded = false;
window.quartoListingCategory = (category) => {
+ category = atob(category);
if (categoriesLoaded) {
activateCategory(category);
setCategoryHash(category);
@@ -58,7 +59,7 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
);
for (const categoryEl of categoryEls) {
- const category = categoryEl.getAttribute("data-category");
+ const category = atob(categoryEl.getAttribute("data-category"));
categoryEl.onclick = () => {
activateCategory(category);
setCategoryHash(category);
@@ -208,7 +209,7 @@ function activateCategory(category) {
// Activate this category
const categoryEl = window.document.querySelector(
- `.quarto-listing-category .category[data-category='${category}'`
+ `.quarto-listing-category .category[data-category='${btoa(category)}']`
);
if (categoryEl) {
categoryEl.classList.add("active");
@@ -231,7 +232,7 @@ function filterListingCategory(category) {
list.filter(function (item) {
const itemValues = item.values();
if (itemValues.categories !== null) {
- const categories = itemValues.categories.split(",");
+ const categories = atob(itemValues.categories).split(",");
return categories.includes(category);
} else {
return false;
diff --git a/tests/docs/smoke-all/2024/10/23/issue-10829/_quarto.yml b/tests/docs/smoke-all/2024/10/23/issue-10829/_quarto.yml
new file mode 100644
index 0000000000..668e81048e
--- /dev/null
+++ b/tests/docs/smoke-all/2024/10/23/issue-10829/_quarto.yml
@@ -0,0 +1,19 @@
+project:
+ type: website
+
+website:
+ title: "issue-10829"
+ navbar:
+ right:
+ - about.qmd
+ - icon: github
+ href: https://github.com/
+ - icon: twitter
+ href: https://twitter.com
+format:
+ html:
+ theme: cosmo
+ css: styles.css
+
+
+
diff --git a/tests/docs/smoke-all/2024/10/23/issue-10829/about.qmd b/tests/docs/smoke-all/2024/10/23/issue-10829/about.qmd
new file mode 100644
index 0000000000..692676f768
--- /dev/null
+++ b/tests/docs/smoke-all/2024/10/23/issue-10829/about.qmd
@@ -0,0 +1,19 @@
+---
+title: "About"
+image: profile.jpg
+about:
+ template: jolla
+ links:
+ - icon: twitter
+ text: Twitter
+ href: https://twitter.com
+ - icon: linkedin
+ text: LinkedIn
+ href: https://linkedin.com
+ - icon: github
+ text: Github
+ href: https://github.com
+
+---
+
+About this blog
diff --git a/tests/docs/smoke-all/2024/10/23/issue-10829/index.qmd b/tests/docs/smoke-all/2024/10/23/issue-10829/index.qmd
new file mode 100644
index 0000000000..a5141c7cb0
--- /dev/null
+++ b/tests/docs/smoke-all/2024/10/23/issue-10829/index.qmd
@@ -0,0 +1,21 @@
+---
+title: "issue-10829"
+listing:
+ contents: posts
+ sort: "date desc"
+ type: default
+ categories: true
+ sort-ui: false
+ filter-ui: false
+page-layout: full
+title-block-banner: true
+_quarto:
+ render-project: true
+ tests:
+ html:
+ ensureFileRegexMatches:
+ - []
+ - ['{=html}']
+---
+
+
diff --git a/tests/docs/smoke-all/2024/10/23/issue-10829/posts/_metadata.yml b/tests/docs/smoke-all/2024/10/23/issue-10829/posts/_metadata.yml
new file mode 100644
index 0000000000..3e9dd01bc3
--- /dev/null
+++ b/tests/docs/smoke-all/2024/10/23/issue-10829/posts/_metadata.yml
@@ -0,0 +1,8 @@
+# options specified here will apply to all posts in this folder
+
+# freeze computational output
+# (see https://quarto.org/docs/projects/code-execution.html#freeze)
+freeze: true
+
+# Enable banner style title blocks
+title-block-banner: true
diff --git a/tests/docs/smoke-all/2024/10/23/issue-10829/posts/post-with-code/image.jpg b/tests/docs/smoke-all/2024/10/23/issue-10829/posts/post-with-code/image.jpg
new file mode 100644
index 0000000000..3ec04c8c4e
Binary files /dev/null and b/tests/docs/smoke-all/2024/10/23/issue-10829/posts/post-with-code/image.jpg differ
diff --git a/tests/docs/smoke-all/2024/10/23/issue-10829/posts/post-with-code/index.qmd b/tests/docs/smoke-all/2024/10/23/issue-10829/posts/post-with-code/index.qmd
new file mode 100644
index 0000000000..b19970f9e9
--- /dev/null
+++ b/tests/docs/smoke-all/2024/10/23/issue-10829/posts/post-with-code/index.qmd
@@ -0,0 +1,9 @@
+---
+title: "Post With Code"
+author: "Harlow Malloc"
+date: "2024-10-23"
+categories: [news, code, analysis, apos'trophe]
+image: "image.jpg"
+---
+
+This is a post with executable code.
diff --git a/tests/docs/smoke-all/2024/10/23/issue-10829/posts/welcome/index.qmd b/tests/docs/smoke-all/2024/10/23/issue-10829/posts/welcome/index.qmd
new file mode 100644
index 0000000000..40ac45f354
--- /dev/null
+++ b/tests/docs/smoke-all/2024/10/23/issue-10829/posts/welcome/index.qmd
@@ -0,0 +1,12 @@
+---
+title: "Welcome To My Blog"
+author: "Tristan O'Malley"
+date: "2024-10-20"
+categories: [news]
+---
+
+This is the first post in a Quarto blog. Welcome!
+
+
+
+Since this post doesn't specify an explicit `image`, the first image in the post will be used in the listing page of posts.
diff --git a/tests/docs/smoke-all/2024/10/23/issue-10829/posts/welcome/thumbnail.jpg b/tests/docs/smoke-all/2024/10/23/issue-10829/posts/welcome/thumbnail.jpg
new file mode 100644
index 0000000000..8e3107c9e0
Binary files /dev/null and b/tests/docs/smoke-all/2024/10/23/issue-10829/posts/welcome/thumbnail.jpg differ
diff --git a/tests/docs/smoke-all/2024/10/23/issue-10829/profile.jpg b/tests/docs/smoke-all/2024/10/23/issue-10829/profile.jpg
new file mode 100644
index 0000000000..9d50b914ff
Binary files /dev/null and b/tests/docs/smoke-all/2024/10/23/issue-10829/profile.jpg differ
diff --git a/tests/docs/smoke-all/2024/10/23/issue-10829/styles.css b/tests/docs/smoke-all/2024/10/23/issue-10829/styles.css
new file mode 100644
index 0000000000..2ddf50c7b4
--- /dev/null
+++ b/tests/docs/smoke-all/2024/10/23/issue-10829/styles.css
@@ -0,0 +1 @@
+/* css styles */
diff --git a/tests/integration/playwright/tests/blog-simple-blog.spec.ts b/tests/integration/playwright/tests/blog-simple-blog.spec.ts
index 1240c36656..b247fe71fc 100644
--- a/tests/integration/playwright/tests/blog-simple-blog.spec.ts
+++ b/tests/integration/playwright/tests/blog-simple-blog.spec.ts
@@ -24,9 +24,9 @@ test('Categories link are clickable', async ({ page }) => {
await page.goto('./blog/simple-blog/_site/posts/welcome/');
await page.locator('div').filter({ hasText: /^news$/ }).click();
await expect(page).toHaveURL(/_site\/index\.html#category=news$/);
- await expect(page.locator('div.category[data-category="news"]')).toHaveClass(/active/);
+ await expect(page.locator(`div.category[data-category="${btoa('news')}"]`)).toHaveClass(/active/);
await page.goto('./blog/simple-blog/_site/posts/welcome/#img-lst');
await page.locator('div').filter({ hasText: /^news$/ }).click();
await expect(page).toHaveURL(/_site\/index\.html#category=news$/);
- await expect(page.locator('div.category[data-category="news"]')).toHaveClass(/active/);
+ await expect(page.locator(`div.category[data-category="${btoa('news')}"]`)).toHaveClass(/active/);
});
\ No newline at end of file