diff --git a/.turbo/turbo-test.log b/.turbo/turbo-test.log new file mode 100644 index 0000000..e4c3099 --- /dev/null +++ b/.turbo/turbo-test.log @@ -0,0 +1,955 @@ + +> @jackyzha0/quartz@5.0.0 test /home/saberzero1/Repos/dev-mode/repos/quartz +> tsx --test + +TAP version 13 +# Subtest: symlinkOrCopySync + # Subtest: creates a symlink on success + ok 1 - creates a symlink on success + --- + duration_ms: 1.794289 + type: 'test' + ... + # Subtest: silently succeeds when link already exists (EEXIST) + ok 2 - silently succeeds when link already exists (EEXIST) + --- + duration_ms: 0.881952 + type: 'test' + ... + # Subtest: re-throws non-EPERM errors + ok 3 - re-throws non-EPERM errors + --- + duration_ms: 9.41571 + type: 'test' + ... + # Subtest: falls back to junction on Windows EPERM + ok 4 - falls back to junction on Windows EPERM + --- + duration_ms: 1.366338 + type: 'test' + ... + # Subtest: falls back to copy when both symlink and junction fail on Windows + ok 5 - falls back to copy when both symlink and junction fail on Windows + --- + duration_ms: 1.424093 + type: 'test' + ... + # Subtest: does not fall back on EPERM when not on Windows + ok 6 - does not fall back on EPERM when not on Windows + --- + duration_ms: 0.661469 + type: 'test' + ... + 1..6 +ok 1 - symlinkOrCopySync + --- + duration_ms: 16.577591 + type: 'suite' + ... +# Subtest: symlinkOrCopy + # Subtest: creates a symlink on success + ok 1 - creates a symlink on success + --- + duration_ms: 6.646098 + type: 'test' + ... + # Subtest: silently succeeds when link already exists (EEXIST) + ok 2 - silently succeeds when link already exists (EEXIST) + --- + duration_ms: 3.596443 + type: 'test' + ... + # Subtest: falls back to copy when both symlink and junction fail on Windows + ok 3 - falls back to copy when both symlink and junction fail on Windows + --- + duration_ms: 2.929394 + type: 'test' + ... + # Subtest: does not fall back on EPERM when not on Windows + ok 4 - does not fall back on EPERM when not on Windows + --- + duration_ms: 0.814288 + type: 'test' + ... + 1..4 +ok 2 - symlinkOrCopy + --- + duration_ms: 14.388319 + type: 'suite' + ... +# Warning: Skipping circular transclusion: pageA -> pageA +# Warning: Skipping circular transclusion: self -> self +# Subtest: renderTranscludes + # Subtest: resolves a single page transclusion + ok 1 - resolves a single page transclusion + --- + duration_ms: 1.26955 + type: 'test' + ... + # Subtest: allows the same page to be embedded twice as siblings + ok 2 - allows the same page to be embedded twice as siblings + --- + duration_ms: 0.22418 + type: 'test' + ... + # Subtest: allows different sections of the same page to be embedded + ok 3 - allows different sections of the same page to be embedded + --- + duration_ms: 0.37216 + type: 'test' + ... + # Subtest: detects actual circular transclusion (A -> B -> A) + ok 4 - detects actual circular transclusion (A -> B -> A) + --- + duration_ms: 0.965395 + type: 'test' + ... + # Subtest: self-referencing transclusion is blocked + ok 5 - self-referencing transclusion is blocked + --- + duration_ms: 0.169319 + type: 'test' + ... + 1..5 +ok 3 - renderTranscludes + --- + duration_ms: 3.861318 + type: 'suite' + ... +# Subtest: pageResources + # Subtest: uses baseDir prefix for resource paths in production mode + ok 1 - uses baseDir prefix for resource paths in production mode + --- + duration_ms: 0.415289 + type: 'test' + ... + # Subtest: omits subpath prefix when baseDir is empty (serve mode) + ok 2 - omits subpath prefix when baseDir is empty (serve mode) + --- + duration_ms: 0.272739 + type: 'test' + ... + # Subtest: contentIndex path reflects baseDir + ok 3 - contentIndex path reflects baseDir + --- + duration_ms: 0.296542 + type: 'test' + ... + 1..3 +ok 4 - pageResources + --- + duration_ms: 1.250977 + type: 'suite' + ... +# Subtest: showPopover on cache-hit with hash + # Subtest: does not reference any lexical popoverInner from an outer scope + ok 1 - does not reference any lexical popoverInner from an outer scope + --- + duration_ms: 1.304254 + type: 'test' + ... + # Subtest: skips scroll when hash is empty + ok 2 - skips scroll when hash is empty + --- + duration_ms: 0.276135 + type: 'test' + ... + # Subtest: skips scroll when heading is not found + ok 3 - skips scroll when heading is not found + --- + duration_ms: 0.200547 + type: 'test' + ... + # Subtest: decodes percent-encoded fragments when building the selector + ok 4 - decodes percent-encoded fragments when building the selector + --- + duration_ms: 0.281885 + type: 'test' + ... + 1..4 +ok 5 - showPopover on cache-hit with hash + --- + duration_ms: 3.103934 + type: 'suite' + ... +# Subtest: buggy showPopover (lexical-capture pattern) regression guard + # Subtest: accessing a capture-before-declaration variable throws ReferenceError (TDZ simulation) + ok 1 - accessing a capture-before-declaration variable throws ReferenceError (TDZ simulation) + --- + duration_ms: 0.569761 + type: 'test' + ... + # Subtest: same pattern does NOT throw when hash is empty (explains why first link without fragment works) + ok 2 - same pattern does NOT throw when hash is empty (explains why first link without fragment works) + --- + duration_ms: 0.276566 + type: 'test' + ... + 1..2 +ok 6 - buggy showPopover (lexical-capture pattern) regression guard + --- + duration_ms: 1.093709 + type: 'suite' + ... +# Subtest: search encoder + # Subtest: English text + # Subtest: should tokenize simple English words + ok 1 - should tokenize simple English words + --- + duration_ms: 1.127912 + type: 'test' + ... + # Subtest: should handle multiple spaces + ok 2 - should handle multiple spaces + --- + duration_ms: 0.411602 + type: 'test' + ... + # Subtest: should handle tabs and newlines + ok 3 - should handle tabs and newlines + --- + duration_ms: 0.155003 + type: 'test' + ... + # Subtest: should lowercase all text + ok 4 - should lowercase all text + --- + duration_ms: 0.091617 + type: 'test' + ... + 1..4 + ok 1 - English text + --- + duration_ms: 2.536476 + type: 'suite' + ... + # Subtest: CJK text + # Subtest: should tokenize Japanese Hiragana character by character + ok 1 - should tokenize Japanese Hiragana character by character + --- + duration_ms: 0.233617 + type: 'test' + ... + # Subtest: should tokenize Japanese Katakana character by character + ok 2 - should tokenize Japanese Katakana character by character + --- + duration_ms: 0.247863 + type: 'test' + ... + # Subtest: should tokenize Japanese Kanji character by character + ok 3 - should tokenize Japanese Kanji character by character + --- + duration_ms: 0.130799 + type: 'test' + ... + # Subtest: should tokenize Korean Hangul character by character + ok 4 - should tokenize Korean Hangul character by character + --- + duration_ms: 0.321528 + type: 'test' + ... + # Subtest: should tokenize Chinese characters character by character + ok 5 - should tokenize Chinese characters character by character + --- + duration_ms: 0.207129 + type: 'test' + ... + # Subtest: should handle mixed Hiragana/Katakana/Kanji + ok 6 - should handle mixed Hiragana/Katakana/Kanji + --- + duration_ms: 0.194125 + type: 'test' + ... + 1..6 + ok 2 - CJK text + --- + duration_ms: 1.739019 + type: 'suite' + ... + # Subtest: Mixed CJK and English + # Subtest: should handle Japanese with English words + ok 1 - should handle Japanese with English words + --- + duration_ms: 0.152819 + type: 'test' + ... + # Subtest: should handle English with Japanese words + ok 2 - should handle English with Japanese words + --- + duration_ms: 0.081549 + type: 'test' + ... + # Subtest: should handle complex mixed content + ok 3 - should handle complex mixed content + --- + duration_ms: 0.084444 + type: 'test' + ... + # Subtest: should handle mixed Korean and English + ok 4 - should handle mixed Korean and English + --- + duration_ms: 0.096957 + type: 'test' + ... + # Subtest: should handle mixed Chinese and English + ok 5 - should handle mixed Chinese and English + --- + duration_ms: 0.072713 + type: 'test' + ... + 1..5 + ok 3 - Mixed CJK and English + --- + duration_ms: 0.613962 + type: 'suite' + ... + # Subtest: Edge cases + # Subtest: should handle empty string + ok 1 - should handle empty string + --- + duration_ms: 0.191751 + type: 'test' + ... + # Subtest: should handle only whitespace + ok 2 - should handle only whitespace + --- + duration_ms: 0.091076 + type: 'test' + ... + # Subtest: should handle single character + ok 3 - should handle single character + --- + duration_ms: 0.073254 + type: 'test' + ... + # Subtest: should handle single CJK character + ok 4 - should handle single CJK character + --- + duration_ms: 0.084565 + type: 'test' + ... + # Subtest: should handle CJK with trailing whitespace + ok 5 - should handle CJK with trailing whitespace + --- + duration_ms: 0.089614 + type: 'test' + ... + # Subtest: should handle English with trailing whitespace + ok 6 - should handle English with trailing whitespace + --- + duration_ms: 0.079265 + type: 'test' + ... + 1..6 + ok 4 - Edge cases + --- + duration_ms: 0.83129 + type: 'suite' + ... + 1..4 +ok 7 - search encoder + --- + duration_ms: 6.293504 + type: 'suite' + ... +# Subtest: resolveLayout + # Subtest: footer defaults to [] when sharedDefaults omits footer + ok 1 - footer defaults to [] when sharedDefaults omits footer + --- + duration_ms: 0.775438 + type: 'test' + ... + # Subtest: header defaults to [] when sharedDefaults omits header + ok 2 - header defaults to [] when sharedDefaults omits header + --- + duration_ms: 0.088121 + type: 'test' + ... + # Subtest: footer from sharedDefaults is used when no override + ok 3 - footer from sharedDefaults is used when no override + --- + duration_ms: 0.137291 + type: 'test' + ... + # Subtest: byPageType override replaces footer + ok 4 - byPageType override replaces footer + --- + duration_ms: 0.135939 + type: 'test' + ... + # Subtest: byPageType override clears footer with [] + ok 5 - byPageType override clears footer with [] + --- + duration_ms: 0.078884 + type: 'test' + ... + # Subtest: byPageType override clears header with [] + ok 6 - byPageType override clears header with [] + --- + duration_ms: 0.081689 + type: 'test' + ... + 1..6 +ok 8 - resolveLayout + --- + duration_ms: 1.943983 + type: 'suite' + ... +# Subtest: resolveLayout frame resolution + # Subtest: config override frame wins over page type frame + ok 1 - config override frame wins over page type frame + --- + duration_ms: 0.33387 + type: 'test' + ... + # Subtest: page type frame wins when no config override + ok 2 - page type frame wins when no config override + --- + duration_ms: 0.19159 + type: 'test' + ... + # Subtest: defaults to 'default' when no frame specified + ok 3 - defaults to 'default' when no frame specified + --- + duration_ms: 0.216105 + type: 'test' + ... + 1..3 +ok 9 - resolveLayout frame resolution + --- + duration_ms: 1.03427 + type: 'suite' + ... +# Subtest: FileTrie + # Subtest: constructor + # Subtest: should create an empty trie + ok 1 - should create an empty trie + --- + duration_ms: 1.219169 + type: 'test' + ... + # Subtest: should set displayName from data title + ok 2 - should set displayName from data title + --- + duration_ms: 0.34463 + type: 'test' + ... + # Subtest: should be able to set displayName + ok 3 - should be able to set displayName + --- + duration_ms: 0.2462 + type: 'test' + ... + 1..3 + ok 1 - constructor + --- + duration_ms: 2.714932 + type: 'suite' + ... + # Subtest: add + # Subtest: should add a file at root level + ok 1 - should add a file at root level + --- + duration_ms: 0.437219 + type: 'test' + ... + # Subtest: should handle index files + ok 2 - should handle index files + --- + duration_ms: 0.315296 + type: 'test' + ... + # Subtest: should add nested files + ok 3 - should add nested files + --- + duration_ms: 0.297815 + type: 'test' + ... + # Subtest: last-insert-wins on folder-note collision (matches emitter semantics) + ok 4 - last-insert-wins on folder-note collision (matches emitter semantics) + --- + duration_ms: 0.238426 + type: 'test' + ... + # Subtest: last-insert-wins on root-level index collision + ok 5 - last-insert-wins on root-level index collision + --- + duration_ms: 0.255106 + type: 'test' + ... + # Subtest: collision does not affect sibling files in the same folder + ok 6 - collision does not affect sibling files in the same folder + --- + duration_ms: 0.337747 + type: 'test' + ... + 1..6 + ok 2 - add + --- + duration_ms: 2.485002 + type: 'suite' + ... + # Subtest: filter + # Subtest: should filter nodes based on condition + ok 1 - should filter nodes based on condition + --- + duration_ms: 0.313664 + type: 'test' + ... + 1..1 + ok 3 - filter + --- + duration_ms: 0.406112 + type: 'suite' + ... + # Subtest: map + # Subtest: should apply function to all nodes + ok 1 - should apply function to all nodes + --- + duration_ms: 0.242464 + type: 'test' + ... + # Subtest: map over folders should work + ok 2 - map over folders should work + --- + duration_ms: 0.215254 + type: 'test' + ... + 1..2 + ok 4 - map + --- + duration_ms: 0.563751 + type: 'suite' + ... + # Subtest: entries + # Subtest: should return all entries + ok 1 - should return all entries + --- + duration_ms: 0.53589 + type: 'test' + ... + 1..1 + ok 5 - entries + --- + duration_ms: 0.618851 + type: 'suite' + ... + # Subtest: fromEntries + # Subtest: nested + ok 1 - nested + --- + duration_ms: 0.259985 + type: 'test' + ... + 1..1 + ok 6 - fromEntries + --- + duration_ms: 0.331937 + type: 'suite' + ... + # Subtest: findNode + # Subtest: should find root node with empty path + ok 1 - should find root node with empty path + --- + duration_ms: 0.249265 + type: 'test' + ... + # Subtest: should find node at first level + ok 2 - should find node at first level + --- + duration_ms: 0.187964 + type: 'test' + ... + # Subtest: should find nested node + ok 3 - should find nested node + --- + duration_ms: 0.36677 + type: 'test' + ... + # Subtest: should return undefined for non-existent path + ok 4 - should return undefined for non-existent path + --- + duration_ms: 0.413606 + type: 'test' + ... + # Subtest: should return undefined for partial path + ok 5 - should return undefined for partial path + --- + duration_ms: 0.193163 + type: 'test' + ... + 1..5 + ok 7 - findNode + --- + duration_ms: 1.618028 + type: 'suite' + ... + # Subtest: getFolderPaths + # Subtest: should return all folder paths + ok 1 - should return all folder paths + --- + duration_ms: 0.272088 + type: 'test' + ... + 1..1 + ok 8 - getFolderPaths + --- + duration_ms: 0.341484 + type: 'suite' + ... + # Subtest: sort + # Subtest: should sort nodes according to sort function + ok 1 - should sort nodes according to sort function + --- + duration_ms: 5.333489 + type: 'test' + ... + 1..1 + ok 9 - sort + --- + duration_ms: 5.418013 + type: 'suite' + ... + # Subtest: pathToNode + # Subtest: should return root node for empty path + ok 1 - should return root node for empty path + --- + duration_ms: 0.244226 + type: 'test' + ... + # Subtest: should return root node for index path + ok 2 - should return root node for index path + --- + duration_ms: 0.111654 + type: 'test' + ... + # Subtest: should return path to first level node + ok 3 - should return path to first level node + --- + duration_ms: 0.110121 + type: 'test' + ... + # Subtest: should return path to nested node + ok 4 - should return path to nested node + --- + duration_ms: 0.105232 + type: 'test' + ... + # Subtest: should return undefined for non-existent path + ok 5 - should return undefined for non-existent path + --- + duration_ms: 0.089062 + type: 'test' + ... + # Subtest: should return file data for intermediate folders + ok 6 - should return file data for intermediate folders + --- + duration_ms: 0.130208 + type: 'test' + ... + # Subtest: should return path for partial path + ok 7 - should return path for partial path + --- + duration_ms: 0.111404 + type: 'test' + ... + 1..7 + ok 10 - pathToNode + --- + duration_ms: 1.075696 + type: 'suite' + ... + 1..10 +ok 10 - FileTrie + --- + duration_ms: 16.524563 + type: 'suite' + ... +# Subtest: typeguards + # Subtest: isSimpleSlug + ok 1 - isSimpleSlug + --- + duration_ms: 0.883505 + type: 'test' + ... + # Subtest: isRelativeURL + ok 2 - isRelativeURL + --- + duration_ms: 0.172896 + type: 'test' + ... + # Subtest: isAbsoluteURL + ok 3 - isAbsoluteURL + --- + duration_ms: 0.165823 + type: 'test' + ... + # Subtest: isFullSlug + ok 4 - isFullSlug + --- + duration_ms: 0.170051 + type: 'test' + ... + # Subtest: isFilePath + ok 5 - isFilePath + --- + duration_ms: 0.089053 + type: 'test' + ... + 1..5 +ok 11 - typeguards + --- + duration_ms: 2.272023 + type: 'suite' + ... +# Subtest: transforms + # Subtest: simplifySlug + ok 1 - simplifySlug + --- + duration_ms: 0.26798 + type: 'test' + ... + # Subtest: slugifyFilePath + ok 2 - slugifyFilePath + --- + duration_ms: 0.697315 + type: 'test' + ... + # Subtest: slugifyFilePath + simplifySlug end-to-end canonicalization + ok 3 - slugifyFilePath + simplifySlug end-to-end canonicalization + --- + duration_ms: 0.206718 + type: 'test' + ... + # Subtest: transformInternalLink + ok 4 - transformInternalLink + --- + duration_ms: 0.893644 + type: 'test' + ... + # Subtest: pathToRoot + ok 5 - pathToRoot + --- + duration_ms: 0.282005 + type: 'test' + ... + # Subtest: joinSegments + ok 6 - joinSegments + --- + duration_ms: 0.129106 + type: 'test' + ... + 1..6 +ok 12 - transforms + --- + duration_ms: 2.744245 + type: 'suite' + ... +# Subtest: link strategies + # Subtest: absolute + # Subtest: from a/b/c + ok 1 - from a/b/c + --- + duration_ms: 0.427531 + type: 'test' + ... + # Subtest: from a/b/index + ok 2 - from a/b/index + --- + duration_ms: 0.132623 + type: 'test' + ... + # Subtest: from index + ok 3 - from index + --- + duration_ms: 0.119779 + type: 'test' + ... + 1..3 + ok 1 - absolute + --- + duration_ms: 0.822824 + type: 'suite' + ... + # Subtest: shortest + # Subtest: from a/b/c + ok 1 - from a/b/c + --- + duration_ms: 0.283478 + type: 'test' + ... + # Subtest: from a/b/index + ok 2 - from a/b/index + --- + duration_ms: 0.12545 + type: 'test' + ... + # Subtest: from index + ok 3 - from index + --- + duration_ms: 0.160313 + type: 'test' + ... + 1..3 + ok 2 - shortest + --- + duration_ms: 0.665467 + type: 'suite' + ... + # Subtest: relative + # Subtest: from a/b/c + ok 1 - from a/b/c + --- + duration_ms: 0.177294 + type: 'test' + ... + # Subtest: from a/b/index + ok 2 - from a/b/index + --- + duration_ms: 0.097328 + type: 'test' + ... + # Subtest: from index + ok 3 - from index + --- + duration_ms: 0.08224 + type: 'test' + ... + 1..3 + ok 3 - relative + --- + duration_ms: 0.438261 + type: 'suite' + ... + 1..3 +ok 13 - link strategies + --- + duration_ms: 2.063622 + type: 'suite' + ... +# Subtest: resolveRelative + # Subtest: from index + ok 1 - from index + --- + duration_ms: 0.105263 + type: 'test' + ... + # Subtest: from nested page + ok 2 - from nested page + --- + duration_ms: 0.068305 + type: 'test' + ... + # Subtest: with index paths + ok 3 - with index paths + --- + duration_ms: 0.063787 + type: 'test' + ... + # Subtest: with simple slugs + ok 4 - with simple slugs + --- + duration_ms: 0.079125 + type: 'test' + ... + 1..4 +ok 14 - resolveRelative + --- + duration_ms: 0.411662 + type: 'suite' + ... +# Subtest: detectSlugCollisions + # Subtest: returns empty array when there are no collisions + ok 1 - returns empty array when there are no collisions + --- + duration_ms: 1.516302 + type: 'test' + ... + # Subtest: returns empty array for empty input + ok 2 - returns empty array for empty input + --- + duration_ms: 0.194716 + type: 'test' + ... + # Subtest: detects a two-file collision with winner = last file + ok 3 - detects a two-file collision with winner = last file + --- + duration_ms: 0.174559 + type: 'test' + ... + # Subtest: detects a three-file collision with all files listed, winner = last + ok 4 - detects a three-file collision with all files listed, winner = last + --- + duration_ms: 0.14758 + type: 'test' + ... + # Subtest: detects multiple separate collisions + ok 5 - detects multiple separate collisions + --- + duration_ms: 0.275203 + type: 'test' + ... + # Subtest: ignores entries without a slug + ok 6 - ignores entries without a slug + --- + duration_ms: 0.161285 + type: 'test' + ... + # Subtest: winner annotation matches fileTrie last-insert-wins semantics + ok 7 - winner annotation matches fileTrie last-insert-wins semantics + --- + duration_ms: 0.133424 + type: 'test' + ... + 1..7 +ok 15 - detectSlugCollisions + --- + duration_ms: 3.771814 + type: 'suite' + ... +# Subtest: formatCollisionWarning + # Subtest: returns empty string for empty input + ok 1 - returns empty string for empty input + --- + duration_ms: 0.356021 + type: 'test' + ... + # Subtest: formats single collision with winner and shadowed markers + ok 2 - formats single collision with winner and shadowed markers + --- + duration_ms: 0.479707 + type: 'test' + ... + # Subtest: formats multiple collisions with count in header + ok 3 - formats multiple collisions with count in header + --- + duration_ms: 0.334 + type: 'test' + ... + # Subtest: output mentions Folder Notes convention as a common cause + ok 4 - output mentions Folder Notes convention as a common cause + --- + duration_ms: 0.156646 + type: 'test' + ... + # Subtest: falls back to filePath when relativePath is missing + ok 5 - falls back to filePath when relativePath is missing + --- + duration_ms: 0.189837 + type: 'test' + ... + 1..5 +ok 16 - formatCollisionWarning + --- + duration_ms: 1.94738 + type: 'suite' + ... +1..16 +# tests 118 +# suites 33 +# pass 118 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0 +# duration_ms 225.288364 diff --git a/docs/advanced/making plugins.md b/docs/advanced/making plugins.md index 9f53a59..1c33f57 100644 --- a/docs/advanced/making plugins.md +++ b/docs/advanced/making plugins.md @@ -412,13 +412,13 @@ export const MyFrame: PageFrame = { grid-template-areas: "center"; } `, - render({ componentData, pageBody: Content, footer: Footer }: PageFrameProps): unknown { + render({ componentData, pageBody: Content, footer: Footers }: PageFrameProps): unknown { const renderSlot = (C: (props: typeof componentData) => unknown): ComponentChildren => C(componentData) as ComponentChildren return (
{(Content as any)(componentData)} - {(Footer as any)(componentData)} + {Footers.map((Footer) => (Footer as any)(componentData))}
) }, @@ -428,7 +428,7 @@ export const MyFrame: PageFrame = { Key requirements: - `name`: A unique string identifier. This is what page types and YAML config reference. -- `render()`: Receives all layout slots (header, sidebars, content, footer) and returns JSX for the inner page structure. +- `render()`: Receives all layout slots (header, sidebars, content, footer) and returns JSX for the inner page structure. Note that `footer` is a `QuartzComponent[]` (an array) — frames should iterate over it with `.map()` to render all footer components. - `css` (optional): Frame-specific CSS. Scope it with `.page[data-frame="my-frame"]` selectors to avoid conflicts. **2. Re-export the frame:** diff --git a/docs/getting-started/upgrading.md b/docs/getting-started/upgrading.md index 0a45ca0..19f6096 100644 --- a/docs/getting-started/upgrading.md +++ b/docs/getting-started/upgrading.md @@ -28,6 +28,42 @@ npx quartz plugin install --latest See the [[upgrade|CLI reference for upgrade]] for more details on available flags. +### Layout System Changes + +The `footer` layout slot is now an array of components, consistent with other layout slots like `header`, `left`, and `right`. Additionally, `header` and `footer` are now configurable layout positions — plugins can declare `layout: { position: header }` or `layout: { position: footer }` in their YAML config. + +**If you override layouts in `quartz.ts`**, update any `footer` assignments to use arrays: + +```ts title="quartz.ts" +// Before +export const layout = await loadQuartzLayout({ + defaults: { footer: MyFooterComponent }, +}) + +// After +export const layout = await loadQuartzLayout({ + defaults: { footer: [MyFooterComponent] }, +}) +``` + +**If you have a custom page frame**, update the `render` function to iterate the footer array: + +```tsx +// Before +render({ footer: Footer, ...rest }: PageFrameProps) { + return