fix(folders): Use real folder names instead of splicing slugs

In breadcrumbs and folder pages (both folder page titles and page
lists) the name of the folder was derived from the slug unless
overriden, which is.. wonky.

This is much more noticeable if you change the slugify function to make
all slugs lowercase - which I did, and which may be a followup PR.

The patch was pretty straightforward though, we just use the real
folder names from the relativePath.
This commit is contained in:
Anton Bulakh 2024-12-28 17:35:52 +02:00
parent 04423d4931
commit 4e0c34730d
No known key found for this signature in database
GPG Key ID: 071FE3E324DD7333
3 changed files with 73 additions and 54 deletions

View File

@ -40,11 +40,8 @@ const defaultOptions: BreadcrumbOptions = {
showCurrentPage: true,
}
function formatCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData {
return {
displayName: displayName.replaceAll("-", " "),
path: resolveRelative(baseSlug, currentSlug),
}
function newCrumb(displayName: string, baseSlug: FullSlug, currentSlug: SimpleSlug): CrumbData {
return { displayName, path: resolveRelative(baseSlug, currentSlug) }
}
export default ((opts?: Partial<BreadcrumbOptions>) => {
@ -65,7 +62,7 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
}
// Format entry for root element
const firstEntry = formatCrumb(options.rootName, fileData.slug!, "/" as SimpleSlug)
const firstEntry = newCrumb(options.rootName, fileData.slug!, "/" as SimpleSlug)
const crumbs: CrumbData[] = [firstEntry]
if (!folderIndex && options.resolveFrontmatterTitle) {
@ -81,6 +78,7 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
// Split slug into hierarchy/parts
const slugParts = fileData.slug?.split("/")
const pathParts = fileData.relativePath?.split("/")
if (slugParts) {
// is tag breadcrumb?
const isTagPath = slugParts[0] === "tags"
@ -89,7 +87,7 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
let currentPath = ""
for (let i = 0; i < slugParts.length - 1; i++) {
let curPathSegment = slugParts[i]
let curPathSegment = pathParts?.[i] ?? slugParts[i]
// Try to resolve frontmatter folder title
const currentFile = folderIndex?.get(slugParts.slice(0, i + 1).join("/"))
@ -105,7 +103,7 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
const includeTrailingSlash = !isTagPath || i < 1
// Format and add current crumb
const crumb = formatCrumb(
const crumb = newCrumb(
curPathSegment,
fileData.slug!,
(currentPath + (includeTrailingSlash ? "/" : "")) as SimpleSlug,

View File

@ -23,6 +23,11 @@ const defaultOptions: FolderContentOptions = {
showSubfolders: true,
}
type Subfolder = {
name: string
contents: QuartzPluginData[]
}
export default ((opts?: Partial<FolderContentOptions>) => {
const options: FolderContentOptions = { ...defaultOptions, ...opts }
@ -31,51 +36,56 @@ export default ((opts?: Partial<FolderContentOptions>) => {
const folderSlug = stripSlashes(simplifySlug(fileData.slug!))
const folderParts = folderSlug.split(path.posix.sep)
const allPagesInFolder: QuartzPluginData[] = []
const allPagesInSubfolders: Map<FullSlug, QuartzPluginData[]> = new Map()
const shownPages: QuartzPluginData[] = []
const subfolders: Map<FullSlug, Subfolder> = new Map()
allFiles.forEach((file) => {
for (const file of allFiles) {
const fileSlug = stripSlashes(simplifySlug(file.slug!))
const prefixed = fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug
const fileParts = fileSlug.split(path.posix.sep)
const isDirectChild = fileParts.length === folderParts.length + 1
if (!prefixed) {
return
// check only files in our folder or nested folders
if (!fileSlug.startsWith(folderSlug) || fileSlug === folderSlug) {
continue
}
if (isDirectChild) {
allPagesInFolder.push(file)
} else if (options.showSubfolders) {
const fileParts = fileSlug.split(path.posix.sep)
// If the file is directly in the folder we just show it
if (fileParts.length === folderParts.length + 1) {
shownPages.push(file)
continue
}
if (options.showSubfolders) {
const subfolderSlug = joinSegments(
...fileParts.slice(0, folderParts.length + 1),
) as FullSlug
const pagesInFolder = allPagesInSubfolders.get(subfolderSlug) || []
allPagesInSubfolders.set(subfolderSlug, [...pagesInFolder, file])
}
})
allPagesInSubfolders.forEach((files, subfolderSlug) => {
const hasIndex = allPagesInFolder.some(
(file) => subfolderSlug === stripSlashes(simplifySlug(file.slug!)),
)
let subfolder = subfolders.get(subfolderSlug)
if (!subfolder) {
const subfolderName = file.relativePath!.split(path.posix.sep).at(folderParts.length)!
subfolders.set(subfolderSlug, (subfolder = { name: subfolderName, contents: [] }))
}
subfolder.contents.push(file)
}
}
for (const [slug, subfolder] of subfolders.entries()) {
const hasIndex = shownPages.some((file) => slug === stripSlashes(simplifySlug(file.slug!)))
if (!hasIndex) {
const subfolderDates = files.sort(byDateAndAlphabetical(cfg))[0].dates
const subfolderTitle = subfolderSlug.split(path.posix.sep).at(-1)!
allPagesInFolder.push({
slug: subfolderSlug,
const subfolderDates = subfolder.contents.sort(byDateAndAlphabetical(cfg))[0].dates
shownPages.push({
slug: slug,
dates: subfolderDates,
frontmatter: { title: subfolderTitle, tags: ["folder"] },
frontmatter: { title: subfolder.name, tags: ["folder"] },
})
}
})
}
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
const classes = cssClasses.join(" ")
const listProps = {
...props,
sort: options.sort,
allFiles: allPagesInFolder,
allFiles: shownPages,
}
const content =
@ -90,7 +100,7 @@ export default ((opts?: Partial<FolderContentOptions>) => {
{options.showFolderCount && (
<p>
{i18n(cfg.locale).pages.folderContent.itemsUnderFolder({
count: allPagesInFolder.length,
count: shownPages.length,
})}
</p>
)}

View File

@ -74,13 +74,27 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (user
const allFiles = content.map((c) => c[1].data)
const cfg = ctx.cfg.configuration
const folderNames: Record<SimpleSlug, string> = {}
const folders: Set<SimpleSlug> = new Set(
allFiles.flatMap((data) => {
return data.slug
? _getFolders(data.slug).filter(
(folderName) => folderName !== "." && folderName !== "tags",
)
: []
if (!data.slug || !data.relativePath) {
return []
}
let folderSlug = path.dirname(data.slug) as SimpleSlug
let folderFs = path.dirname(data.relativePath) as SimpleSlug
folderNames[folderSlug] = folderFs
const folders = [folderSlug]
while (folderSlug !== ".") {
folderSlug = path.dirname(folderSlug) as SimpleSlug
folders.push(folderSlug)
folderFs = path.dirname(folderFs) as SimpleSlug
folderNames[folderSlug] = folderFs
}
return folders.filter((f) => f !== "." && f !== "tags")
}),
)
@ -89,8 +103,9 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (user
folder,
defaultProcessedContent({
slug: joinSegments(folder, "index") as FullSlug,
relativePath: joinSegments(folderNames[folder], "index.html") as FilePath, // this is used by breadcrumbs
frontmatter: {
title: `${i18n(cfg.locale).pages.folderContent.folder}: ${folder}`,
title: `${i18n(cfg.locale).pages.folderContent.folder}: ${folderNames[folder]}`,
tags: [],
},
}),
@ -100,7 +115,14 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (user
for (const [tree, file] of content) {
const slug = stripSlashes(simplifySlug(file.data.slug!)) as SimpleSlug
if (folders.has(slug)) {
folderDescriptions[slug] = [tree, file]
if (file.data.frontmatter?.title === "index") {
// sadly we need to avoid changing the original file title for things like explorer to work
const clonedFile = structuredClone(file)
clonedFile.data.frontmatter!.title = `${i18n(cfg.locale).pages.folderContent.folder}: ${folderNames[slug]}`
folderDescriptions[slug] = [tree, clonedFile]
} else {
folderDescriptions[slug] = [tree, file]
}
}
}
@ -132,14 +154,3 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FolderPageOptions>> = (user
},
}
}
function _getFolders(slug: FullSlug): SimpleSlug[] {
var folderName = path.dirname(slug ?? "") as SimpleSlug
const parentFolderNames = [folderName]
while (folderName !== ".") {
folderName = path.dirname(folderName ?? "") as SimpleSlug
parentFolderNames.push(folderName)
}
return parentFolderNames
}