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: restructure config system with improved type safety

the3ash 094351e3 8de92ffc

+416 -146
+5 -3
astro.config.ts
··· 10 10 import rehypeImageProcessor from './src/plugins/rehype-image-processor.mjs' 11 11 import rehypeCopyCode from './src/plugins/rehype-copy-code.mjs' 12 12 import remarkTOC from './src/plugins/remark-toc.mjs' 13 - import { SITE } from './src/config' 13 + import { themeConfig } from './src/config' 14 + import { imageConfig } from './src/utils/image-config' 14 15 import path from 'path' 15 16 16 17 export default defineConfig({ 17 - site: SITE.website, 18 + site: themeConfig.site.website, 18 19 devToolbar: { 19 20 enabled: false 20 21 }, ··· 24 25 }, 25 26 image: { 26 27 service: { 27 - entrypoint: 'astro/assets/services/sharp' 28 + entrypoint: 'astro/assets/services/sharp', 29 + config: imageConfig 28 30 } 29 31 }, 30 32 markdown: {
+4 -3
src/components/features/FormattedDate.astro
··· 1 1 --- 2 + import { themeConfig } from '@/config' 2 3 import { formatDate } from '@/utils/date' 3 - import { SITE } from '@/config' 4 4 5 5 interface Props { 6 6 date: Date ··· 12 12 13 13 <time 14 14 datetime={date.toISOString()} 15 - class={!SITE.dateOnRight && 16 - (SITE.dateFormat === 'MONTH DAY YYYY' || SITE.dateFormat === 'DAY MONTH YYYY') 15 + class={!themeConfig.date.dateOnRight && 16 + (themeConfig.date.dateFormat === 'MONTH DAY YYYY' || 17 + themeConfig.date.dateFormat === 'DAY MONTH YYYY') 17 18 ? 'date-left' 18 19 : ''} 19 20 >
+5 -5
src/components/features/PostList.astro
··· 1 1 --- 2 2 import FormattedDate from '@/components/features/FormattedDate.astro' 3 3 import type { CollectionEntry } from 'astro:content' 4 - import { SITE } from '@/config' 4 + import { themeConfig } from '@/config' 5 5 6 6 interface Props { 7 7 posts: CollectionEntry<'posts'>[] ··· 15 15 posts.map((post) => ( 16 16 <li> 17 17 <a href={`/${post.id}/`}> 18 - <div class={`post-item ${!SITE.dateOnRight ? 'date-left' : ''}`}> 19 - {!SITE.dateOnRight && ( 18 + <div class={`post-item ${!themeConfig.date.dateOnRight ? 'date-left' : ''}`}> 19 + {!themeConfig.date.dateOnRight && ( 20 20 <p class="date font-features"> 21 21 <FormattedDate date={post.data.pubDate} /> 22 22 </p> 23 23 )} 24 24 <p class="title">{post.data.title}</p> 25 - {SITE.dateOnRight && <div class="divider" />} 26 - {SITE.dateOnRight && ( 25 + {themeConfig.date.dateOnRight && <div class="divider" />} 26 + {themeConfig.date.dateOnRight && ( 27 27 <p class="date font-features"> 28 28 <FormattedDate date={post.data.pubDate} /> 29 29 </p>
+23 -5
src/components/layout/BaseHead.astro
··· 1 1 --- 2 2 // Import the global.css file here so that it is included on 3 3 // all pages through the use of the <BaseHead /> component. 4 - import { SITE } from '@/config' 4 + import { themeConfig } from '@/config' 5 5 import type { BaseHeadProps } from '@/types/component.types' 6 6 import 'katex/dist/katex.min.css' 7 7 ··· 13 13 <!-- Global Metadata --> 14 14 <meta charset="utf-8" /> 15 15 <meta name="viewport" content="width=device-width,initial-scale=1" /> 16 - {SITE.fadeAnimation && <meta name="view-transition" content="same-origin" />} 16 + {themeConfig.general.fadeAnimation && <meta name="view-transition" content="same-origin" />} 17 17 <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> 18 18 <link rel="apple-touch-icon" href="/apple-touch-icon.png" /> 19 19 <link rel="preload" href="/fonts/Inter.woff2" as="font" type="font/woff2" crossorigin="anonymous" /> ··· 28 28 <link 29 29 rel="alternate" 30 30 type="application/rss+xml" 31 - title={SITE.title} 31 + title={themeConfig.site.title} 32 32 href={new URL('rss.xml', Astro.site)} 33 33 /> 34 34 <meta name="generator" content={Astro.generator} /> ··· 38 38 39 39 <!-- Primary Meta Tags --> 40 40 <title> 41 - {title || SITE.title} 41 + {title || themeConfig.site.title} 42 42 </title> 43 43 <meta name="title" content={title} /> 44 44 <meta name="description" content={description} /> ··· 58 58 <meta property="twitter:image" content={new URL('/chiri-og.png', Astro.url)} /> 59 59 60 60 <!-- Transitions Initialization --> 61 - <script is:inline define:vars={{ fadeAnimation: SITE.fadeAnimation }}> 61 + <script is:inline define:vars={{ fadeAnimation: themeConfig.general.fadeAnimation }}> 62 62 function initMotionPref(doc = document) { 63 63 const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches 64 64 const supportsViewTransitions = 'startViewTransition' in document ··· 69 69 'disable-transitions', 70 70 !fadeAnimation || !supportsViewTransitions 71 71 ) 72 + 73 + // Dynamically control view transition navigation behavior 74 + let viewTransitionStyle = doc.getElementById('view-transition-style') 75 + 76 + if (fadeAnimation && supportsViewTransitions && !prefersReducedMotion) { 77 + // Enable view transition navigation 78 + if (!viewTransitionStyle) { 79 + viewTransitionStyle = doc.createElement('style') 80 + viewTransitionStyle.id = 'view-transition-style' 81 + viewTransitionStyle.textContent = '@view-transition { navigation: auto; }' 82 + doc.head.appendChild(viewTransitionStyle) 83 + } 84 + } else { 85 + // Disable view transition navigation 86 + if (viewTransitionStyle) { 87 + viewTransitionStyle.remove() 88 + } 89 + } 72 90 73 91 doc.documentElement.classList.add('js') 74 92 }
+2 -2
src/components/layout/Footer.astro
··· 1 1 --- 2 - import { SITE } from '@/config' 2 + import { themeConfig } from '@/config' 3 3 const today = new Date() 4 4 --- 5 5 ··· 10 10 &copy; 11 11 {today.getFullYear()} 12 12 </span> 13 - {SITE.author} 13 + {themeConfig.site.author} 14 14 </div> 15 15 <div class="powered-by"> 16 16 Powered by{' '}
+3 -3
src/components/layout/Header.astro
··· 1 1 --- 2 - import { SITE } from '@/config' 2 + import { themeConfig } from '@/config' 3 3 import ThemeToggle from '@/components/ui/ThemeToggle.astro' 4 4 --- 5 5 6 6 <header> 7 7 <nav> 8 8 <div class="logo-container"> 9 - {SITE.favicon && <img src="/favicon.svg" alt="favicon" class="favicon" />} 10 - <a href="/">{SITE.title}</a> 9 + {themeConfig.general.favicon && <img src="/favicon.svg" alt="favicon" class="favicon" />} 10 + <a href="/">{themeConfig.site.title}</a> 11 11 </div> 12 12 <ThemeToggle /> 13 13 </nav>
+2 -2
src/components/layout/TransitionWrapper.astro
··· 1 1 --- 2 - import { SITE } from '@/config' 2 + import { themeConfig } from '@/config' 3 3 import type { TransitionProps } from '@/types' 4 4 5 5 type Props = TransitionProps ··· 9 9 --- 10 10 11 11 { 12 - SITE.fadeAnimation ? ( 12 + themeConfig.general.fadeAnimation ? ( 13 13 <div transition:name={transitionName} transition:animate="initial" class={className}> 14 14 <slot /> 15 15 </div>
+9 -5
src/components/ui/BackButton.astro
··· 1 1 --- 2 - import { SITE } from '@/config' 2 + import { themeConfig } from '@/config' 3 3 --- 4 4 5 5 <a href="/" class="back-button"> ··· 17 17 <script 18 18 is:inline 19 19 define:vars={{ 20 - contentWidth: SITE.contentWidth, 21 - centeredLayout: SITE.centeredLayout, 22 - toc: SITE.toc 20 + contentWidth: themeConfig.general.contentWidth, 21 + centeredLayout: themeConfig.general.centeredLayout, 22 + toc: themeConfig.post.toc 23 23 }} 24 24 > 25 25 ;(function () { ··· 38 38 // Calculate available margin space for positioning 39 39 const pageWidth = window.innerWidth 40 40 const contentWidthValue = parseFloat(contentWidth) 41 - const margin = (pageWidth - contentWidthValue * 16) / 2 41 + // Apply the same minimum width logic as in BaseLayout 42 + const widthValue = Math.min(contentWidthValue, 50) 43 + const shouldUseCustomWidth = widthValue > 25 44 + const finalWidthValue = shouldUseCustomWidth ? widthValue : 25 45 + const margin = (pageWidth - finalWidthValue * 16) / 2 42 46 const baseMinSpace = 11 * 16 // Base minimum space needed 43 47 // If toc is enabled, need additional 2.5rem (40px) space 44 48 const minSpace = toc ? baseMinSpace + 40 : baseMinSpace
+4 -2
src/components/ui/CopyCode.astro
··· 1 1 --- 2 - import { SITE } from '@/config' 2 + import { themeConfig } from '@/config' 3 3 --- 4 4 5 - <script define:vars={{ copyCode: SITE.copyCode }}> 5 + <script define:vars={{ copyCode: themeConfig.post.copyCode }}> 6 6 function initCopyCode() { 7 7 const copyIcon = ` 8 8 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="14" height="14" fill="currentColor"> ··· 143 143 justify-content: center; 144 144 border: 1px solid var(--border); 145 145 backdrop-filter: blur(48px); 146 + opacity: 0; 147 + pointer-events: none; 146 148 } 147 149 148 150 [data-copy-code='disabled'] .copy-button {
+100
src/components/ui/ImageOptimizer.astro
··· 1 + --- 2 + import { Image } from 'astro:assets' 3 + import type { ImageOptimizerProps } from '../../types/component.types' 4 + 5 + const { 6 + src, 7 + alt, 8 + width, 9 + height, 10 + quality = 85, 11 + format = 'webp', 12 + loading = 'lazy', 13 + decoding = 'async', 14 + class: className, 15 + caption, 16 + priority = false 17 + } = Astro.props as ImageOptimizerProps 18 + 19 + // Use eager loading for priority images 20 + const actualLoading = priority ? 'eager' : loading 21 + 22 + // Generate responsive image sizes 23 + const sizes = '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw' 24 + --- 25 + 26 + <figure class={`optimized-image ${className || ''}`}> 27 + { 28 + typeof src === 'string' ? ( 29 + <img 30 + src={src} 31 + alt={alt} 32 + width={width} 33 + height={height} 34 + loading={actualLoading} 35 + decoding={decoding} 36 + fetchpriority={priority ? 'high' : 'auto'} 37 + sizes={sizes} 38 + class="responsive-image" 39 + /> 40 + ) : ( 41 + <Image 42 + src={src} 43 + alt={alt} 44 + width={width} 45 + height={height} 46 + quality={quality} 47 + format={format} 48 + loading={actualLoading} 49 + decoding={decoding} 50 + fetchpriority={priority ? 'high' : 'auto'} 51 + sizes={sizes} 52 + class="responsive-image" 53 + /> 54 + ) 55 + } 56 + {caption && <figcaption>{caption}</figcaption>} 57 + </figure> 58 + 59 + <style> 60 + .optimized-image { 61 + margin: 1.25em 0; 62 + text-align: center; 63 + } 64 + 65 + .optimized-image .responsive-image { 66 + max-width: 100%; 67 + height: auto; 68 + border-radius: 4px; 69 + transition: opacity 0.3s ease; 70 + background-color: var(--bg-secondary); 71 + } 72 + 73 + .optimized-image figcaption { 74 + color: var(--text-secondary); 75 + font-size: var(--font-size-s); 76 + margin-top: 0.75em; 77 + text-align: center; 78 + } 79 + 80 + /* Loading state */ 81 + .optimized-image .responsive-image[loading='lazy'] { 82 + opacity: 0; 83 + animation: fadeIn 0.3s ease forwards; 84 + } 85 + 86 + @keyframes fadeIn { 87 + from { 88 + opacity: 0; 89 + } 90 + to { 91 + opacity: 1; 92 + } 93 + } 94 + 95 + /* Hover effect for interactive images */ 96 + .optimized-image .responsive-image:hover { 97 + transform: scale(1.02); 98 + transition: transform 0.3s ease; 99 + } 100 + </style>
+32 -30
src/components/ui/TableOfContents.astro
··· 1 1 --- 2 - import { SITE } from '@/config' 2 + import { themeConfig } from '@/config' 3 3 import type { TOCProps } from '@/types' 4 4 5 5 const { toc = [] }: TOCProps = Astro.props ··· 38 38 <script 39 39 is:inline 40 40 define:vars={{ 41 - contentWidth: SITE.contentWidth, 42 - centeredLayout: SITE.centeredLayout, 43 - toc: SITE.toc 41 + contentWidth: themeConfig.general.contentWidth, 42 + centeredLayout: themeConfig.general.centeredLayout, 43 + toc: themeConfig.post.toc 44 44 }} 45 45 > 46 46 ;(function () { 47 - let isInitialized = false 48 - 49 47 // TOC positioning logic (similar to BackButton) 50 48 function adjustTOCPosition() { 51 49 const tocContainer = document.querySelector('.toc-container') ··· 60 58 // Calculate available margin space for positioning 61 59 const pageWidth = window.innerWidth 62 60 const contentWidthValue = parseFloat(contentWidth) 63 - const margin = (pageWidth - contentWidthValue * 16) / 2 61 + // Apply the same minimum width logic as in BaseLayout 62 + const widthValue = Math.min(contentWidthValue, 50) 63 + const shouldUseCustomWidth = widthValue > 25 64 + const finalWidthValue = shouldUseCustomWidth ? widthValue : 25 65 + const margin = (pageWidth - finalWidthValue * 16) / 2 64 66 const baseMinSpace = 11 * 16 // Base minimum space needed 65 67 // If toc is enabled, need additional 2.5rem (40px) space 66 68 const minSpace = toc ? baseMinSpace + 40 : baseMinSpace ··· 73 75 // If toc is enabled, move 2.5rem (40px) further left 74 76 const leftPosition = toc ? basePosition - 40 : basePosition 75 77 tocContainer.style.left = `${leftPosition}px` 78 + 79 + // Update active state after TOC becomes visible 80 + setTimeout(() => updateActiveTOCItem(), 100) 76 81 } else { 77 82 tocContainer.style.display = 'none' 78 83 tocContainer.classList.remove('fixed-position') ··· 160 165 } 161 166 } 162 167 163 - function initTOC() { 164 - if (isInitialized) return 168 + // Initialize immediately to prevent layout shift 169 + adjustTOCPosition() 170 + checkTOCVisibility() 171 + addTOCEventListeners() 172 + updateActiveTOCItem() // Set initial active state immediately 165 173 174 + // Listen for various page load events 175 + document.addEventListener('astro:page-load', () => { 166 176 adjustTOCPosition() 167 - checkTOCVisibility() 168 177 addTOCEventListeners() 169 - updateActiveTOCItem() // Initial active state 170 - 171 - isInitialized = true 172 - } 173 - 174 - // Initialize on page load 175 - function handlePageLoad() { 176 - initTOC() 177 - } 178 - 179 - // Listen for various page load events 180 - document.addEventListener('astro:page-load', handlePageLoad) 181 - document.addEventListener('DOMContentLoaded', handlePageLoad) 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 - } 178 + updateActiveTOCItem() // Update active state after page load 179 + }) 180 + document.addEventListener('DOMContentLoaded', () => { 181 + adjustTOCPosition() 182 + addTOCEventListeners() 183 + updateActiveTOCItem() // Update active state after DOM ready 184 + }) 189 185 190 186 // Re-initialize on resize and scroll 191 187 window.addEventListener('resize', adjustTOCPosition) ··· 198 194 width: 12rem; 199 195 position: relative; 200 196 left: -0.175em; 197 + opacity: 0; 198 + transition: opacity 0.2s ease-out; 199 + } 200 + 201 + .toc-container.fixed-position { 202 + opacity: 1; 201 203 } 202 204 203 205 .toc-nav {
+2 -2
src/components/ui/ThemeToggle.astro
··· 1 1 --- 2 - import { SITE } from '@/config' 2 + import { themeConfig } from '@/config' 3 3 --- 4 4 5 5 { 6 - SITE.themeToggle && ( 6 + themeConfig.general.themeToggle && ( 7 7 <button id="theme-toggle" class="theme-toggle" aria-label="Toggle theme"> 8 8 <div class="theme-icon hollow-circle" /> 9 9 <div class="theme-icon solid-circle" />
+29 -19
src/config.ts
··· 1 - export const SITE = { 1 + import type { ThemeConfig } from './types' 2 + 3 + export const themeConfig: ThemeConfig = { 2 4 // SITE INFO /////////////////////////////////////////////////////////////////////////////////////////// 3 - website: 'https://astro-chiri.netlify.app/', // Site domain 4 - title: 'CHIRI', // Site title 5 - author: '3ASH', // Author name 6 - description: 'Minimal blog built by Astro', // Site description 7 - language: 'en-US', // Default language 5 + site: { 6 + website: 'https://astro-chiri.netlify.app/', // Site domain 7 + title: 'CHIRI', // Site title 8 + author: '3ASH', // Author name 9 + description: 'Minimal blog built by Astro', // Site description 10 + language: 'en-US' // Default language 11 + }, 8 12 9 13 // GENERAL SETTINGS //////////////////////////////////////////////////////////////////////////////////// 10 - contentWidth: '35rem', // Content area width 11 - centeredLayout: true, // Use centered layout (false for left-aligned) 12 - favicon: false, // Show favicon on index page 13 - themeToggle: false, // Show theme toggle button (uses system theme by default) 14 - footer: true, // Show footer 15 - fadeAnimation: true, // Enable fade animations 14 + general: { 15 + contentWidth: '35rem', // Content area width 16 + centeredLayout: true, // Use centered layout (false for left-aligned) 17 + favicon: false, // Show favicon on index page 18 + themeToggle: false, // Show theme toggle button (uses system theme by default) 19 + footer: true, // Show footer 20 + fadeAnimation: true // Enable fade animations 21 + }, 16 22 17 23 // DATE SETTINGS /////////////////////////////////////////////////////////////////////////////////////// 18 - dateFormat: 'YYYY-MM-DD', // Date format: YYYY-MM-DD, MM-DD-YYYY, DD-MM-YYYY, MONTH DAY YYYY, DAY MONTH YYYY 19 - dateSeparator: '.', // Date separator: . - / (except for MONTH DAY YYYY and DAY MONTH YYYY) 20 - dateOnRight: true, // Date position in post list (true for right, false for left) 24 + date: { 25 + dateFormat: 'YYYY-MM-DD', // Date format: YYYY-MM-DD, MM-DD-YYYY, DD-MM-YYYY, MONTH DAY YYYY, DAY MONTH YYYY 26 + dateSeparator: '.', // Date separator: . - / (except for MONTH DAY YYYY and DAY MONTH YYYY) 27 + dateOnRight: true // Date position in post list (true for right, false for left) 28 + }, 21 29 22 30 // POST SETTINGS /////////////////////////////////////////////////////////////////////////////////////// 23 - readingTime: false, // Show reading time in posts 24 - toc: true, // Show the table of contents when there is enough page width 25 - imageViewer: true, // Enable image viewer 26 - copyCode: true // Enable copy button in code blocks 31 + post: { 32 + readingTime: false, // Show reading time in posts 33 + toc: true, // Show the table of contents (when there is enough page width) 34 + imageViewer: true, // Enable image viewer 35 + copyCode: true // Enable copy button in code blocks 36 + } 27 37 }
-13
src/content/config.ts
··· 1 - import { defineCollection, z } from 'astro:content' 2 - 3 - const posts = defineCollection({ 4 - type: 'content', 5 - schema: z.object({ 6 - title: z.string(), 7 - pubDate: z.date() 8 - }) 9 - }) 10 - 11 - export const collections = { 12 - posts 13 - }
+29 -9
src/content/posts/theme-guide.md
··· 14 14 15 15 ## Main Files & Directories 16 16 17 - - `src/content/about/about.md` - Edit the about section of the index page. Leave it empty if you don’t want any content. 17 + - `src/content/about/about.md` - Edit the about section of the index page. Leave it empty if you don't want any content. 18 18 - `src/content/posts/` - All blog posts are stored here 19 19 - `src/config.ts` - Configure main site info and settings ↓ 20 20 21 + - Site Info 22 + 21 23 ```ts 22 - export const SITE = { 24 + site: { 23 25 // Site domain 24 26 website: 'https://astro-chiri.netlify.app/', 25 27 // Site title ··· 29 31 // Site description 30 32 description: 'Minimal blog built by Astro', 31 33 // Default language 32 - language: 'en-US', 34 + language: 'en-US' 35 + }, 36 + ``` 37 + 38 + - General Settings 33 39 40 + ```ts 41 + general: { 34 42 // Content area width 35 43 contentWidth: '35rem', 36 44 // Use centered layout (false for left-aligned) 37 45 centeredLayout: true, 38 46 // Show favicon on index page 39 47 favicon: false, 40 - // Show theme toggle button 48 + // Show theme toggle button (uses system theme by default) 41 49 themeToggle: false, 42 50 // Show footer 43 51 footer: true, 44 52 // Enable fade animations 45 - fadeAnimation: true, 53 + fadeAnimation: true 54 + }, 55 + ``` 46 56 57 + - Date Settings 58 + 59 + ```ts 60 + date: { 47 61 // Date format: YYYY-MM-DD, MM-DD-YYYY, DD-MM-YYYY, MONTH DAY YYYY, DAY MONTH YYYY 48 62 dateFormat: 'YYYY-MM-DD', 49 63 // Date separator: . - / (except for MONTH DAY YYYY and DAY MONTH YYYY) 50 64 dateSeparator: '.', 51 65 // Date position in post list (true for right, false for left) 52 - dateOnRight: true, 66 + dateOnRight: true 67 + }, 68 + ``` 69 + 70 + - Post Settings 53 71 72 + ```text 73 + post: { 54 74 // Show reading time in posts 55 75 readingTime: false, 56 - // Show table of contents 76 + // Show the table of contents (when there is enough page width) 57 77 toc: true, 58 78 // Enable image viewer 59 79 imageViewer: true, 60 80 // Enable copy button in code blocks 61 - copyCode: false 81 + copyCode: true 62 82 } 63 83 ``` 64 84 ··· 66 86 67 87 Only `title` and `pubDate` are required fields 68 88 69 - ```md 89 + ```ts 70 90 --- 71 91 title: 'Post Title' 72 92 pubDate: '2025-07-10'
+9 -8
src/layouts/BaseLayout.astro
··· 1 1 --- 2 - import { SITE } from '@/config' 2 + import { themeConfig } from '@/config' 3 3 import TransitionWrapper from '@/components/layout/TransitionWrapper.astro' 4 4 import type { LayoutProps } from '@/types' 5 5 6 6 type Props = LayoutProps 7 7 8 8 const { type = 'page' } = Astro.props 9 - const contentWidth = SITE.contentWidth 9 + const language = themeConfig.site.language || 'en-US' 10 + const contentWidth = themeConfig.general.contentWidth 10 11 const widthValue = Math.min(parseFloat(contentWidth), 50) 11 12 const shouldUseCustomWidth = widthValue > 25 12 13 const finalWidth = shouldUseCustomWidth ? `${widthValue}rem` : '25rem' 13 - 14 - const language = SITE.language || 'en-US' 15 14 --- 16 15 17 16 <html 18 17 lang={language} 19 - class={SITE.fadeAnimation ? 'view-transitions-enabled' : 'view-transitions-disabled'} 20 - {...SITE.fadeAnimation ? { 'transition:animate': 'initial' } : {}} 18 + class={themeConfig.general.fadeAnimation 19 + ? 'view-transitions-enabled' 20 + : 'view-transitions-disabled'} 21 + {...themeConfig.general.fadeAnimation ? { 'transition:animate': 'initial' } : {}} 21 22 > 22 23 <head> 23 24 <slot name="head" /> 24 25 </head> 25 26 <body 26 - data-centered={SITE.centeredLayout} 27 - class={!SITE.fadeAnimation ? 'no-fade' : ''} 27 + data-centered={themeConfig.general.centeredLayout} 28 + class={!themeConfig.general.fadeAnimation ? 'no-fade' : ''} 28 29 style={` 29 30 max-width: ${finalWidth}; 30 31 ${shouldUseCustomWidth ? `--content-width: ${widthValue}rem;` : ''}
+2 -2
src/layouts/IndexLayout.astro
··· 5 5 import Footer from '@/components/layout/Footer.astro' 6 6 import BaseLayout from '@/layouts/BaseLayout.astro' 7 7 import GradientMask from '@/components/ui/GradientMask.astro' 8 - import { SITE } from '@/config' 8 + import { themeConfig } from '@/config' 9 9 10 10 const { title, description } = Astro.props 11 11 --- ··· 21 21 <slot /> 22 22 </main> 23 23 { 24 - SITE.footer && ( 24 + themeConfig.general.footer && ( 25 25 <div> 26 26 <Footer /> 27 27 </div>
+15 -7
src/layouts/PostLayout.astro
··· 14 14 import CopyCode from '@/components/ui/CopyCode.astro' 15 15 import BaseLayout from '@/layouts/BaseLayout.astro' 16 16 17 - import { SITE } from '@/config' 17 + import { themeConfig } from '@/config' 18 18 19 19 const { title, pubDate, readingTime, toc } = Astro.props as PostLayoutProps 20 20 --- 21 21 22 - <BaseLayout title={`${title} · ${SITE.title}`} description={SITE.description} type="post"> 23 - <BaseHead title={`${title} · ${SITE.title}`} description={SITE.description} slot="head" /> 22 + <BaseLayout 23 + title={`${title} · ${themeConfig.site.title}`} 24 + description={themeConfig.site.description} 25 + type="post" 26 + > 27 + <BaseHead 28 + title={`${title} · ${themeConfig.site.title}`} 29 + description={themeConfig.site.description} 30 + slot="head" 31 + /> 24 32 <div class="post-container"> 25 33 <main> 26 34 <div class="prose"> 27 35 <GradientMask /> 28 36 <BackButton /> 29 - {SITE.toc && <TableOfContents toc={toc} />} 37 + {themeConfig.post.toc && <TableOfContents toc={toc} />} 30 38 <div class="title"> 31 39 <h1>{title}</h1> 32 40 <div class="date"> 33 41 <FormattedDate date={pubDate} /> 34 42 { 35 - SITE.readingTime && readingTime && ( 43 + themeConfig.post.readingTime && readingTime && ( 36 44 <span class="reading-time"> 37 45 <span class="separator">·</span> 38 46 {readingTime.text} ··· 48 56 <CopyCode /> 49 57 <GitHubCard /> 50 58 <XPOST /> 51 - {SITE.imageViewer && <ImageViewer />} 52 - {SITE.footer && <Footer />} 59 + {themeConfig.post.imageViewer && <ImageViewer />} 60 + {themeConfig.general.footer && <Footer />} 53 61 </div> 54 62 </BaseLayout> 55 63
+2 -2
src/pages/404.astro
··· 1 1 --- 2 + import { themeConfig } from '@/config' 2 3 import IndexLayout from '@/layouts/IndexLayout.astro' 3 - import { SITE } from '@/config' 4 4 --- 5 5 6 - <IndexLayout title={`404 - ${SITE.title}`} description="Not Found"> 6 + <IndexLayout title={`404 - ${themeConfig.site.title}`} description="Not Found"> 7 7 <style> 8 8 .error-container { 9 9 color: var(--text-secondary);
+2 -2
src/pages/index.astro
··· 2 2 import IndexLayout from '@/layouts/IndexLayout.astro' 3 3 import About from '@/components/pages/About.astro' 4 4 import PostList from '@/components/features/PostList.astro' 5 - import { SITE } from '@/config' 5 + import { themeConfig } from '@/config' 6 6 import { getSortedFilteredPosts } from '@/utils/draft' 7 7 8 8 const posts = await getSortedFilteredPosts() 9 9 --- 10 10 11 - <IndexLayout title={SITE.title} description={SITE.description}> 11 + <IndexLayout title={themeConfig.site.title} description={themeConfig.site.description}> 12 12 <About /> 13 13 <main> 14 14 <PostList posts={posts} />
+10 -2
src/plugins/rehype-image-processor.mjs
··· 1 1 import { visit } from 'unist-util-visit' 2 - import { SITE } from '../config.ts' 2 + import { themeConfig } from '../config.ts' 3 3 4 4 /** 5 5 * Rehype plugin that processes images in markdown content: 6 6 * - Wraps images with alt text in figure/figcaption elements 7 7 * - Adds data-preview attribute for image viewer functionality 8 + * - Adds lazy loading for better performance 8 9 * - Handles multiple images in a single paragraph 9 10 */ 10 11 export default function rehypeImageProcessor() { ··· 37 38 for (const imgNode of imgNodes) { 38 39 const alt = imgNode.properties?.alt?.trim() 39 40 41 + // Enhanced image properties with performance optimizations 40 42 imgNode.properties = { 41 43 ...imgNode.properties, 42 - 'data-preview': SITE.imageViewer ? 'true' : 'false' 44 + 'data-preview': themeConfig.post.imageViewer ? 'true' : 'false', 45 + // Add lazy loading for better performance 46 + loading: 'lazy', 47 + // Add decoding hint for better performance 48 + decoding: 'async', 49 + // Add fetchpriority for critical images (first image gets high priority) 50 + fetchpriority: newNodes.length === 0 ? 'high' : 'auto' 43 51 } 44 52 45 53 if (!alt || alt.includes('_')) {
+5 -6
src/styles/global.css
··· 133 133 view-transition-name: enabled; 134 134 } 135 135 136 - @view-transition { 137 - navigation: auto; 138 - } 139 - 140 136 html.view-transitions-disabled { 141 137 view-transition-name: none; 142 138 } 143 139 144 140 @media (prefers-reduced-motion: reduce) { 145 - @view-transition { 146 - navigation: none; 141 + * { 142 + animation-duration: 0.01ms !important; 143 + transition-duration: 0.01ms !important; 144 + animation-iteration-count: 1 !important; 145 + scroll-behavior: auto !important; 147 146 } 148 147 } 149 148
+44 -1
src/styles/post.css
··· 100 100 font-weight: var(--font-weight-bold); 101 101 } 102 102 103 - /* Images */ 103 + /* Images with performance optimizations */ 104 104 .prose img { 105 105 max-width: 100%; 106 106 height: auto; 107 107 display: block; 108 108 margin: 1.25em 0; 109 + background-color: var(--astro-code-background); 110 + transition: opacity 0.3s ease-in-out; 111 + } 112 + 113 + /* Loading state for images */ 114 + .prose img[loading='lazy'] { 115 + opacity: 0; 116 + animation: fadeIn 0.3s ease-in-out forwards; 117 + } 118 + 119 + @keyframes fadeIn { 120 + from { 121 + opacity: 0; 122 + } 123 + to { 124 + opacity: 1; 125 + } 126 + } 127 + 128 + /* Ensure images don't cause layout shifts */ 129 + .prose img:not([width]):not([height]) { 130 + aspect-ratio: attr(width) / attr(height); 131 + } 132 + 133 + /* Add skeleton loading effect */ 134 + .prose img[loading='lazy']:not([src]) { 135 + background: linear-gradient( 136 + 90deg, 137 + var(--bg-secondary) 25%, 138 + var(--bg-tertiary) 50%, 139 + var(--bg-secondary) 75% 140 + ); 141 + background-size: 200% 100%; 142 + animation: loading 1.5s infinite; 143 + } 144 + 145 + @keyframes loading { 146 + 0% { 147 + background-position: 200% 0; 148 + } 149 + 100% { 150 + background-position: -200% 0; 151 + } 109 152 } 110 153 111 154 .prose figure {
+15
src/types/component.types.ts
··· 31 31 title: string 32 32 description: string 33 33 } 34 + 35 + // ImageOptimizer component props interface 36 + export interface ImageOptimizerProps { 37 + src: string | ImageMetadata 38 + alt: string 39 + width?: number 40 + height?: number 41 + quality?: number 42 + format?: 'avif' | 'webp' | 'jpeg' | 'png' 43 + loading?: 'lazy' | 'eager' 44 + decoding?: 'async' | 'sync' | 'auto' 45 + class?: string 46 + caption?: string 47 + priority?: boolean 48 + }
+25 -2
src/types/config.types.ts
··· 6 6 | 'MONTH DAY YYYY' 7 7 | 'DAY MONTH YYYY' 8 8 9 - // Site configuration type 10 - export interface SiteConfig { 9 + // Site info configuration type 10 + export interface SiteInfo { 11 11 website: string 12 12 title: string 13 13 author: string 14 14 description: string 15 15 language: string 16 + } 17 + 18 + // General settings configuration type 19 + export interface GeneralSettings { 16 20 contentWidth: string 17 21 centeredLayout: boolean 18 22 favicon: boolean 23 + themeToggle: boolean 19 24 footer: boolean 20 25 fadeAnimation: boolean 26 + } 27 + 28 + // Date settings configuration type 29 + export interface DateSettings { 21 30 dateFormat: DateFormat 22 31 dateSeparator: string 32 + dateOnRight: boolean 33 + } 34 + 35 + // Post settings configuration type 36 + export interface PostSettings { 23 37 readingTime: boolean 38 + toc: boolean 24 39 imageViewer: boolean 25 40 copyCode: boolean 26 41 } 42 + 43 + // Theme configuration type 44 + export interface ThemeConfig { 45 + site: SiteInfo 46 + general: GeneralSettings 47 + date: DateSettings 48 + post: PostSettings 49 + }
+3 -3
src/utils/date.ts
··· 1 - import { SITE } from '@/config' 1 + import { themeConfig } from '@/config' 2 2 3 3 const MONTHS_EN = [ 4 4 'Jan', ··· 23 23 * @returns 24 24 */ 25 25 export function formatDate(date: Date, format?: string): string { 26 - const formatStr = (format || SITE.dateFormat).trim() 27 - const configSeparator = SITE.dateSeparator || '-' 26 + const formatStr = (format || themeConfig.date.dateFormat).trim() 27 + const configSeparator = themeConfig.date.dateSeparator || '-' 28 28 29 29 const separator = VALID_SEPARATORS.includes(configSeparator.trim()) ? configSeparator.trim() : '.' 30 30
+1 -1
src/utils/draft.ts
··· 5 5 */ 6 6 export async function getFilteredPosts() { 7 7 const posts = await getCollection('posts') 8 - return posts.filter((post) => !post.id.startsWith('_')) 8 + return posts.filter((post: CollectionEntry<'posts'>) => !post.id.startsWith('_')) 9 9 } 10 10 11 11 /**
+7 -7
src/utils/feed.ts
··· 1 1 import { getCollection, type CollectionEntry } from 'astro:content' 2 - import { SITE } from '@/config' 2 + import { themeConfig } from '@/config' 3 3 import type { APIContext } from 'astro' 4 4 5 5 export async function generateRSS(context: APIContext) { 6 6 const posts = await getCollection('posts') 7 - const filteredPosts = posts.filter((post) => !post.id.startsWith('_')) 7 + const filteredPosts = posts.filter((post: CollectionEntry<'posts'>) => !post.id.startsWith('_')) 8 8 const sortedPosts = filteredPosts.sort( 9 9 (a: CollectionEntry<'posts'>, b: CollectionEntry<'posts'>) => 10 10 b.data.pubDate.valueOf() - a.data.pubDate.valueOf() ··· 13 13 const rss = `<?xml version="1.0" encoding="UTF-8" ?> 14 14 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"> 15 15 <channel> 16 - <title>${SITE.title}</title> 16 + <title>${themeConfig.site.title}</title> 17 17 <link>${context.site}</link> 18 - <description>${SITE.description}</description> 18 + <description>${themeConfig.site.description}</description> 19 19 <language>zh-CN</language> 20 20 <lastBuildDate>${new Date().toUTCString()}</lastBuildDate> 21 21 <atom:link href="${context.site}/rss.xml" rel="self" type="application/rss+xml" /> ··· 44 44 45 45 export async function generateAtom(context: APIContext) { 46 46 const posts = await getCollection('posts') 47 - const filteredPosts = posts.filter((post) => !post.id.startsWith('_')) 47 + const filteredPosts = posts.filter((post: CollectionEntry<'posts'>) => !post.id.startsWith('_')) 48 48 const sortedPosts = filteredPosts.sort( 49 49 (a: CollectionEntry<'posts'>, b: CollectionEntry<'posts'>) => 50 50 b.data.pubDate.valueOf() - a.data.pubDate.valueOf() ··· 52 52 53 53 const atom = `<?xml version="1.0" encoding="utf-8"?> 54 54 <feed xmlns="http://www.w3.org/2005/Atom"> 55 - <title>${SITE.title}</title> 56 - <subtitle>${SITE.description}</subtitle> 55 + <title>${themeConfig.site.title}</title> 56 + <subtitle>${themeConfig.site.description}</subtitle> 57 57 <link href="${context.site}/atom.xml" rel="self" type="application/atom+xml" /> 58 58 <link href="${context.site}" /> 59 59 <id>${context.site}</id>
+27
src/utils/image-config.ts
··· 1 + export const imageConfig = { 2 + // Enhanced image optimization settings 3 + limitInputPixels: 268402689, // ~16K x 16K pixels 4 + jpeg: { 5 + quality: 85, 6 + progressive: true, 7 + optimizeScans: true, 8 + mozjpeg: true 9 + }, 10 + png: { 11 + quality: 85, 12 + progressive: true, 13 + compressionLevel: 9, 14 + adaptiveFiltering: true 15 + }, 16 + webp: { 17 + quality: 85, 18 + lossless: false, 19 + nearLossless: true, 20 + smartSubsample: true 21 + }, 22 + avif: { 23 + quality: 85, 24 + lossless: false, 25 + speed: 5 26 + } 27 + }