@@ -10,6 +10,12 @@ export interface PostListingOptions {
1010 */
1111 limit ?: number
1212
13+ /**
14+ * Number of posts to show before collapsing the rest into a details element.
15+ * If undefined, all posts are shown without collapsing.
16+ */
17+ collapsedItemCount ?: number
18+
1319 /**
1420 * Sorting function for posts. Defaults to byDateAndAlphabetical.
1521 */
@@ -21,9 +27,16 @@ export interface PostListingOptions {
2127 */
2228 excludeTags ?: string [ ]
2329
30+ /**
31+ * If true, filters posts to only those that have the current page's tag.
32+ * Only works on tag pages (pages with slug starting with "tags/").
33+ * Default: false
34+ */
35+ filterToCurrentTag ?: boolean
36+
2437 /**
2538 * Filter function to determine which pages to include.
26- * Takes precedence over excludeTags if both are specified.
39+ * Takes precedence over excludeTags and filterToCurrentTag if specified.
2740 */
2841 filter ?: ( file : QuartzPluginData ) => boolean
2942
@@ -75,12 +88,27 @@ export default ((userOpts?: Partial<PostListingOptions>) => {
7588 if ( opts . filter ) {
7689 // Use custom filter if provided
7790 filteredFiles = filteredFiles . filter ( opts . filter )
78- } else if ( opts . excludeTags && opts . excludeTags . length > 0 ) {
79- // Use excludeTags filter
80- filteredFiles = filteredFiles . filter ( ( file ) => {
81- const tags = file . frontmatter ?. tags ?? [ ]
82- return ! tags . some ( ( tag ) => opts . excludeTags ! . includes ( tag ) )
83- } )
91+ } else {
92+ // Apply filterToCurrentTag if on a tag page
93+ if ( opts . filterToCurrentTag && fileData . slug ?. startsWith ( "tags/" ) ) {
94+ // Extract tag from slug: "tags/horticulture/index" -> "horticulture"
95+ const currentTag = fileData . slug
96+ . replace ( / ^ t a g s \/ / , "" )
97+ . replace ( / \/ i n d e x $ / , "" )
98+ . replace ( / \/ $ / , "" )
99+ filteredFiles = filteredFiles . filter ( ( file ) => {
100+ const tags = file . frontmatter ?. tags ?? [ ]
101+ return tags . includes ( currentTag )
102+ } )
103+ }
104+
105+ // Apply excludeTags filter
106+ if ( opts . excludeTags && opts . excludeTags . length > 0 ) {
107+ filteredFiles = filteredFiles . filter ( ( file ) => {
108+ const tags = file . frontmatter ?. tags ?? [ ]
109+ return ! tags . some ( ( tag ) => opts . excludeTags ! . includes ( tag ) )
110+ } )
111+ }
84112 }
85113
86114 // Apply sorting
@@ -96,47 +124,65 @@ export default ((userOpts?: Partial<PostListingOptions>) => {
96124 return < p class = "post-listing-empty" > { opts . emptyMessage } </ p >
97125 }
98126
99- return (
100- < ul class = "section-ul" >
101- { list . map ( ( page ) => {
102- const title = page . frontmatter ?. title
103- const tags = page . frontmatter ?. tags ?? [ ]
104-
105- return (
106- < li class = "section-li" >
107- < div class = "section" >
108- { opts . showDates && page . dates && (
109- < p class = "meta" >
110- < Date date = { getDate ( cfg , page ) ! } locale = { cfg . locale } />
111- </ p >
112- ) }
113- < div class = "desc" >
114- < h3 >
115- < a href = { resolveRelative ( fileData . slug ! , page . slug ! ) } class = "internal" >
116- { title }
117- </ a >
118- </ h3 >
119- </ div >
120- { opts . showTags && tags . length > 0 && (
121- < ul class = "tags" >
122- { tags . map ( ( tag ) => (
123- < li >
124- < a
125- class = "internal tag-link"
126- href = { resolveRelative ( fileData . slug ! , `tags/${ tag } ` as FullSlug ) }
127- >
128- { tag }
129- </ a >
130- </ li >
131- ) ) }
132- </ ul >
133- ) }
127+ // Render list items
128+ const renderListItems = ( items : QuartzPluginData [ ] ) =>
129+ items . map ( ( page ) => {
130+ const title = page . frontmatter ?. title
131+ const tags = page . frontmatter ?. tags ?? [ ]
132+
133+ return (
134+ < li class = "section-li" >
135+ < div class = "section" >
136+ { opts . showDates && page . dates && (
137+ < p class = "meta" >
138+ < Date date = { getDate ( cfg , page ) ! } locale = { cfg . locale } />
139+ </ p >
140+ ) }
141+ < div class = "desc" >
142+ < h3 >
143+ < a href = { resolveRelative ( fileData . slug ! , page . slug ! ) } class = "internal" >
144+ { title }
145+ </ a >
146+ </ h3 >
134147 </ div >
135- </ li >
136- )
137- } ) }
138- </ ul >
139- )
148+ { opts . showTags && tags . length > 0 && (
149+ < ul class = "tags" >
150+ { tags . map ( ( tag ) => (
151+ < li >
152+ < a
153+ class = "internal tag-link"
154+ href = { resolveRelative ( fileData . slug ! , `tags/${ tag } ` as FullSlug ) }
155+ >
156+ { tag }
157+ </ a >
158+ </ li >
159+ ) ) }
160+ </ ul >
161+ ) }
162+ </ div >
163+ </ li >
164+ )
165+ } )
166+
167+ // Handle collapsible list if collapsedItemCount is set
168+ if ( opts . collapsedItemCount && list . length > opts . collapsedItemCount ) {
169+ const visibleItems = list . slice ( 0 , opts . collapsedItemCount )
170+ const collapsedItems = list . slice ( opts . collapsedItemCount )
171+
172+ return (
173+ < div >
174+ < ul class = "section-ul" > { renderListItems ( visibleItems ) } </ ul >
175+ < details class = "post-listing-collapse" >
176+ < summary >
177+ Show { collapsedItems . length } more { collapsedItems . length === 1 ? "post" : "posts" }
178+ </ summary >
179+ < ul class = "section-ul" > { renderListItems ( collapsedItems ) } </ ul >
180+ </ details >
181+ </ div >
182+ )
183+ }
184+
185+ return < ul class = "section-ul" > { renderListItems ( list ) } </ ul >
140186 }
141187
142188 PostListing . css = `
@@ -152,6 +198,25 @@ export default ((userOpts?: Partial<PostListingOptions>) => {
152198 color: var(--gray);
153199 font-style: italic;
154200}
201+
202+ .post-listing-collapse {
203+ margin-top: 1rem;
204+ }
205+
206+ .post-listing-collapse > summary {
207+ cursor: pointer;
208+ color: var(--secondary);
209+ font-weight: 600;
210+ padding: 0.5rem 0;
211+ }
212+
213+ .post-listing-collapse > summary:hover {
214+ color: var(--tertiary);
215+ }
216+
217+ .post-listing-collapse > .section-ul {
218+ margin-top: 1rem;
219+ }
155220`
156221
157222 return PostListing
0 commit comments