Fork of Chiri for Astro for my blog
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: toc to remark chore: clean up types

the3ash 620eabe3 46ec44b0

+221 -139
+2 -1
astro.config.ts
··· 9 9 import rehypeCleanup from './src/plugins/rehype-cleanup.mjs' 10 10 import rehypeImageProcessor from './src/plugins/rehype-image-processor.mjs' 11 11 import rehypeCopyCode from './src/plugins/rehype-copy-code.mjs' 12 + import remarkTOC from './src/plugins/remark-toc.mjs' 12 13 import { SITE } from './src/config' 13 14 import path from 'path' 14 15 ··· 31 32 theme: 'css-variables', 32 33 wrap: false 33 34 }, 34 - remarkPlugins: [remarkMath, remarkDirective, remarkEmbeddedMedia, remarkReadingTime], 35 + remarkPlugins: [remarkMath, remarkDirective, remarkEmbeddedMedia, remarkReadingTime, remarkTOC], 35 36 rehypePlugins: [rehypeKatex, rehypeCleanup, rehypeImageProcessor, rehypeCopyCode] 36 37 }, 37 38 integrations: [mdx(), sitemap()],
+85 -90
src/components/ui/TableOfContents.astro
··· 1 1 --- 2 2 import { SITE } from '@/config' 3 + import type { TOCProps } from '@/types' 4 + 5 + const { toc = [] }: TOCProps = Astro.props 3 6 --- 4 7 5 8 <div class="toc-container" id="toc"> 6 9 <nav class="toc-nav"> 7 - <ul class="toc-list" id="toc-list"></ul> 10 + <ul class="toc-list" id="toc-list"> 11 + <!-- Back to top link --> 12 + <li class="toc-item toc-level-0"> 13 + <a href="#" class="toc-link toc-title" title="Back to top" data-text="Back to top"> 14 + Back to top 15 + </a> 16 + </li> 17 + 18 + <!-- TOC items --> 19 + { 20 + toc.map((item) => ( 21 + <li class={`toc-item toc-level-${item.level}`}> 22 + <a 23 + href={`#${item.id}`} 24 + class="toc-link" 25 + title={item.text} 26 + data-text={item.text} 27 + data-heading-id={item.id} 28 + > 29 + {item.text} 30 + </a> 31 + </li> 32 + )) 33 + } 34 + </ul> 8 35 </nav> 9 36 </div> 10 37 ··· 17 44 }} 18 45 > 19 46 ;(function () { 47 + let isInitialized = false 48 + 20 49 // TOC positioning logic (similar to BackButton) 21 50 function adjustTOCPosition() { 22 51 const tocContainer = document.querySelector('.toc-container') ··· 51 80 } 52 81 } 53 82 54 - // Extract headings and paragraphs and create TOC 55 - function createTOC() { 56 - const tocList = document.getElementById('toc-list') 57 - if (!tocList) return 58 - 59 - const tocItems = [] 60 - 61 - // Get all content elements in document order 62 - const allElements = Array.from(document.querySelectorAll('h1, h2, h3')) 63 - 64 - // Process elements in document order 65 - allElements.forEach((element, index) => { 66 - if (element.tagName === 'H1' || element.tagName === 'H2' || element.tagName === 'H3') { 67 - // Skip the main title (first h1) 68 - if (index === 0 && element.tagName === 'H1') return 69 - 70 - const level = parseInt(element.tagName.charAt(1)) 71 - if (level > 3) return // Only include h1, h2, h3 72 - 73 - const text = element.textContent || '' 74 - const id = element.id || `heading-${index}` 75 - element.id = id 76 - 77 - tocItems.push({ 78 - level, 79 - text, 80 - id, 81 - element 82 - }) 83 - } 84 - }) 83 + // Hide TOC if there are no headings (only title) 84 + function checkTOCVisibility() { 85 + const tocContainer = document.querySelector('.toc-container') 86 + const tocItems = document.querySelectorAll('.toc-item:not(.toc-level-0)') 85 87 86 - // Hide TOC if there are no headings (only title) 87 - const tocContainerElement = document.querySelector('.toc-container') 88 - if (tocContainerElement && tocItems.length === 0) { 89 - tocContainerElement.style.display = 'none' 90 - return 88 + if (tocContainer && tocItems.length === 0) { 89 + tocContainer.style.display = 'none' 91 90 } 91 + } 92 92 93 - // Clear existing TOC 94 - tocList.innerHTML = '' 93 + // Add click handlers to TOC links using event delegation 94 + function addTOCEventListeners() { 95 + const tocContainer = document.querySelector('.toc-container') 96 + if (!tocContainer) return 95 97 96 - // Add title link at the top 97 - const titleLi = document.createElement('li') 98 - titleLi.className = 'toc-item toc-level-0' 98 + // Use event delegation for better performance and reliability 99 + tocContainer.addEventListener('click', function (e) { 100 + const link = e.target.closest('.toc-link') 101 + if (!link) return 99 102 100 - const titleLink = document.createElement('a') 101 - titleLink.href = '#' 102 - titleLink.className = 'toc-link toc-title' 103 - titleLink.setAttribute('title', 'Back to top') 104 - titleLink.setAttribute('data-text', 'Back to top') 105 - 106 - // Add click handler to scroll to top 107 - titleLink.addEventListener('click', (e) => { 108 103 e.preventDefault() 109 - window.scrollTo({ top: 0, behavior: 'smooth' }) 110 - // Update URL without page jump 111 - history.pushState(null, null, '#') 112 - }) 113 104 114 - titleLi.appendChild(titleLink) 115 - tocList.appendChild(titleLi) 116 - 117 - // Create TOC items 118 - tocItems.forEach((item) => { 119 - const li = document.createElement('li') 120 - li.className = `toc-item toc-level-${item.level}` 121 - 122 - const link = document.createElement('a') 123 - link.href = `#${item.id}` 124 - link.className = 'toc-link' 125 - link.setAttribute('title', item.text) // Add title for accessibility 126 - link.setAttribute('data-text', item.text) // Add data-text for CSS content 127 - link.textContent = item.text // Add text content for hover display 128 - 129 - link.addEventListener('click', (e) => { 130 - e.preventDefault() 131 - const target = document.getElementById(item.id) 132 - if (target) { 133 - const rect = target.getBoundingClientRect() 134 - const scrollTop = window.pageYOffset || document.documentElement.scrollTop 135 - const offset = rect.top + scrollTop - 96 // 6rem = 96px 136 - window.scrollTo({ top: offset, behavior: 'smooth' }) 137 - history.pushState(null, null, `#${item.id}`) 105 + if (link.classList.contains('toc-title')) { 106 + // Back to top 107 + window.scrollTo({ top: 0, behavior: 'smooth' }) 108 + history.pushState(null, null, '#') 109 + } else { 110 + // TOC item 111 + const href = link.getAttribute('href') 112 + if (href && href.startsWith('#')) { 113 + const targetId = href.substring(1) 114 + const target = document.getElementById(targetId) 115 + if (target) { 116 + const rect = target.getBoundingClientRect() 117 + const scrollTop = window.pageYOffset || document.documentElement.scrollTop 118 + const offset = rect.top + scrollTop - 96 // 6rem = 96px 119 + window.scrollTo({ top: offset, behavior: 'smooth' }) 120 + history.pushState(null, null, href) 121 + } 138 122 } 139 - }) 140 - 141 - li.appendChild(link) 142 - tocList.appendChild(li) 123 + } 143 124 }) 144 - 145 - updateActiveTOCItem() 146 125 } 147 126 148 127 // Update active TOC item based on scroll position ··· 182 161 } 183 162 184 163 function initTOC() { 164 + if (isInitialized) return 165 + 185 166 adjustTOCPosition() 186 - createTOC() 167 + checkTOCVisibility() 168 + addTOCEventListeners() 169 + updateActiveTOCItem() // Initial active state 170 + 171 + isInitialized = true 187 172 } 188 173 189 - document.addEventListener('astro:page-load', () => { 174 + // Initialize on page load 175 + function handlePageLoad() { 190 176 initTOC() 191 - }) 177 + } 192 178 193 - document.addEventListener('DOMContentLoaded', () => { 194 - initTOC() 195 - }) 179 + // Listen for various page load events 180 + document.addEventListener('astro:page-load', handlePageLoad) 181 + document.addEventListener('DOMContentLoaded', handlePageLoad) 196 182 183 + // Also initialize immediately if DOM is already loaded 184 + if (document.readyState === 'loading') { 185 + document.addEventListener('DOMContentLoaded', handlePageLoad) 186 + } else { 187 + handlePageLoad() 188 + } 189 + 190 + // Re-initialize on resize and scroll 197 191 window.addEventListener('resize', adjustTOCPosition) 198 192 window.addEventListener('scroll', updateActiveTOCItem) 199 193 })() ··· 256 250 color 0.2s ease-out, 257 251 font-size 0.2s ease-out, 258 252 text-indent 0.2s ease-out; 253 + cursor: pointer; 259 254 } 260 255 261 256 .toc-link::after {
+3 -12
src/layouts/PostLayout.astro
··· 1 1 --- 2 2 import '@/styles/global.css' 3 - import type { CollectionEntry } from 'astro:content' 3 + import type { PostLayoutProps } from '@/types' 4 4 import FormattedDate from '@/components/features/FormattedDate.astro' 5 5 import FootnoteScroll from '@/components/features/FootnoteScroll.astro' 6 6 import BaseHead from '@/components/layout/BaseHead.astro' ··· 16 16 17 17 import { SITE } from '@/config' 18 18 19 - type Props = CollectionEntry<'posts'>['data'] & { 20 - readingTime?: { 21 - text: string 22 - minutes: number 23 - time: number 24 - words: number 25 - } 26 - } 27 - 28 - const { title, pubDate, readingTime } = Astro.props 19 + const { title, pubDate, readingTime, toc } = Astro.props as PostLayoutProps 29 20 --- 30 21 31 22 <BaseLayout title={`${title} · ${SITE.title}`} description={SITE.description} type="post"> ··· 35 26 <div class="prose"> 36 27 <GradientMask /> 37 28 <BackButton /> 38 - {SITE.toc && <TableOfContents />} 29 + {SITE.toc && <TableOfContents toc={toc} />} 39 30 <div class="title"> 40 31 <h1>{title}</h1> 41 32 <div class="date">
+2 -1
src/pages/[...slug].astro
··· 18 18 const { Content, remarkPluginFrontmatter } = await render(post) 19 19 20 20 const readingTime = remarkPluginFrontmatter.readingTime 21 + const toc = remarkPluginFrontmatter.toc || [] 21 22 --- 22 23 23 - <PostLayout {...post.data} readingTime={readingTime}> 24 + <PostLayout {...post.data} readingTime={readingTime} toc={toc}> 24 25 <Content /> 25 26 </PostLayout>
+55
src/plugins/remark-toc.mjs
··· 1 + import { visit } from 'unist-util-visit' 2 + 3 + export default function remarkTOC() { 4 + return function (tree, file) { 5 + const headings = [] 6 + let headingIndex = 0 7 + 8 + // Extract headings from AST 9 + visit(tree, 'heading', (node) => { 10 + const level = node.depth 11 + 12 + // Only process h1, h2, h3 13 + if (level > 3) return 14 + 15 + // Skip the first h1 16 + if (level === 1 && headingIndex === 0) { 17 + headingIndex++ 18 + return 19 + } 20 + 21 + const text = extractTextContent(node) 22 + if (!text) return 23 + 24 + const id = `heading-${headingIndex}` 25 + 26 + if (!node.data) node.data = {} 27 + if (!node.data.hProperties) node.data.hProperties = {} 28 + node.data.hProperties.id = id 29 + 30 + headings.push({ 31 + level, 32 + text, 33 + id, 34 + index: headingIndex 35 + }) 36 + 37 + headingIndex++ 38 + }) 39 + 40 + // Store TOC data in file.data.astro.frontmatter 41 + if (!file.data.astro) file.data.astro = {} 42 + if (!file.data.astro.frontmatter) file.data.astro.frontmatter = {} 43 + file.data.astro.frontmatter.toc = headings 44 + } 45 + } 46 + 47 + function extractTextContent(node) { 48 + let text = '' 49 + 50 + visit(node, 'text', (textNode) => { 51 + text += textNode.value 52 + }) 53 + 54 + return text.trim() 55 + }
+27
src/types/component.types.ts
··· 1 + import type { TOCItem, ReadingTime } from './content.types' 2 + 3 + // TOC component props interface 4 + export interface TOCProps { 5 + toc?: TOCItem[] 6 + } 7 + 8 + // Post layout props interface (generic, not tied to specific data source) 9 + export interface PostLayoutProps { 10 + title: string 11 + pubDate: Date 12 + image?: string 13 + readingTime?: ReadingTime 14 + toc?: TOCItem[] 15 + } 16 + 17 + // Transition props interface 18 + export interface TransitionProps { 19 + type: 'post' | 'page' 20 + class?: string 21 + } 22 + 23 + // Layout props interface 24 + export interface LayoutProps extends TransitionProps { 25 + title?: string 26 + description?: string 27 + }
+26
src/types/config.types.ts
··· 1 + // Date format types 2 + export type DateFormat = 3 + | 'YYYY-MM-DD' 4 + | 'MM-DD-YYYY' 5 + | 'DD-MM-YYYY' 6 + | 'MONTH DAY YYYY' 7 + | 'DAY MONTH YYYY' 8 + 9 + // Site configuration type 10 + export interface SiteConfig { 11 + website: string 12 + title: string 13 + author: string 14 + description: string 15 + language: string 16 + contentWidth: string 17 + centeredLayout: boolean 18 + favicon: boolean 19 + footer: boolean 20 + fadeAnimation: boolean 21 + dateFormat: DateFormat 22 + dateSeparator: string 23 + readingTime: boolean 24 + imageViewer: boolean 25 + copyCode: boolean 26 + }
+15
src/types/content.types.ts
··· 1 + // Reading time interface 2 + export interface ReadingTime { 3 + text: string 4 + minutes: number 5 + time: number 6 + words: number 7 + } 8 + 9 + // TOC item interface 10 + export interface TOCItem { 11 + level: number 12 + text: string 13 + id: string 14 + index: number 15 + }
+6 -26
src/types/index.ts
··· 1 - export * from './layout.types' 1 + // Configuration types 2 + export * from './config.types' 2 3 3 - // Date format types 4 - export type DateFormat = 5 - | 'YYYY-MM-DD' 6 - | 'MM-DD-YYYY' 7 - | 'DD-MM-YYYY' 8 - | 'MONTH DAY YYYY' 9 - | 'DAY MONTH YYYY' 4 + // Content types 5 + export * from './content.types' 10 6 11 - // Site configuration type 12 - export interface SiteConfig { 13 - website: string 14 - title: string 15 - author: string 16 - description: string 17 - language: string 18 - contentWidth: string 19 - centeredLayout: boolean 20 - favicon: boolean 21 - footer: boolean 22 - fadeAnimation: boolean 23 - dateFormat: DateFormat 24 - dateSeparator: string 25 - readingTime: boolean 26 - imageViewer: boolean 27 - copyCode: boolean 28 - } 7 + // Component types 8 + export * from './component.types'
-9
src/types/layout.types.ts
··· 1 - export interface TransitionProps { 2 - type: 'post' | 'page' 3 - class?: string 4 - } 5 - 6 - export interface LayoutProps extends TransitionProps { 7 - title?: string 8 - description?: string 9 - }