feat: enhance post listing and reading time display

- Implement dynamic post listing grouped by year with improved date formatting
- Add reading time display for each post
- Update index pages for both default and localized routes
- Modify content utility functions to support reading time metadata
- Refactor global styles and type definitions to support new features
This commit is contained in:
radishzzz 2025-01-26 02:59:39 +00:00
parent e9e318e02d
commit ae39d7b08c
18 changed files with 558 additions and 117 deletions

View file

@ -2,9 +2,9 @@
import themeConfig from '@/config' import themeConfig from '@/config'
const { title, subtitle } = themeConfig.site const { title, subtitle } = themeConfig.site
const { titleSpace = 2 } = themeConfig.global const { titleSpace } = themeConfig.global
const marginClass = { const marginBottom = {
1: 'mb-1', 1: 'mb-1',
2: 'mb-2', 2: 'mb-2',
3: 'mb-3', 3: 'mb-3',
@ -13,18 +13,13 @@ const marginClass = {
--- ---
<header> <header>
<h1 <h1 class={`${marginBottom} mt--5.2 text-12 c-primary font-bold font-title`}>
class={`${marginClass} mt--5.2 text-12 c-primary font-bold font-title`}
aria-label="Title Tag"
>
<a href="/"> <a href="/">
{title} {title}
</a> </a>
</h1> </h1>
<h2
class="text-5.6 c-secondary font-navbar" <h2 class="text-5.6 c-secondary font-navbar">
aria-label="Meta Description"
>
{subtitle} {subtitle}
</h2> </h2>
</header> </header>

View file

@ -6,26 +6,30 @@ const langs = ['', ...themeConfig.global.moreLocale]
const currentLocale = themeConfig.global.locale const currentLocale = themeConfig.global.locale
function getLanguageDisplayName(code: string) { function getLanguageDisplayName(code: string) {
if (!code) if (!code) {
return 'Default' return 'Default'
}
return new Intl.DisplayNames(['en'], { type: 'language' }).of(code) || code return new Intl.DisplayNames(['en'], { type: 'language' }).of(code) || code
} }
--- ---
<button <button
type="button"
id="language-switcher" id="language-switcher"
class="absolute right-25.6 top-20 z-99 aspect-square w-6.6 c-secondary active:scale-92" class="absolute right-25.6 top-20 z-9 aspect-square w-6.6 c-secondary active:scale-90"
aria-label="Switch Language" aria-label={`Current Language: ${getLanguageDisplayName(currentLocale)}. Click to switch to next language.`}
title={`Current Language: ${getLanguageDisplayName(currentLocale)}`} >
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-full w-full"
aria-hidden="true"
> >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-full w-full">
<path d="M19 21 12.3 2h-1L4.7 21l-2.5.2v.8h6.3v-.8L5.7 21l2-5.9h7.5l2 5.9-3.3.2v.8h7.9v-.8zM8 14.3l3.4-10.1 3.5 10.1z" fill="currentColor" /> <path d="M19 21 12.3 2h-1L4.7 21l-2.5.2v.8h6.3v-.8L5.7 21l2-5.9h7.5l2 5.9-3.3.2v.8h7.9v-.8zM8 14.3l3.4-10.1 3.5 10.1z" fill="currentColor" />
</svg> </svg>
<span class="sr-only">Switch Language</span>
</button> </button>
<script is:inline define:vars={{ langs }}> <script is:inline define:vars={{ langs }}>
// Move event binding into astro:page-load event
document.addEventListener('astro:page-load', () => { document.addEventListener('astro:page-load', () => {
const langSwitch = document.getElementById('language-switcher') const langSwitch = document.getElementById('language-switcher')
@ -33,24 +37,32 @@ document.addEventListener('astro:page-load', () => {
const { pathname, search, hash } = window.location const { pathname, search, hash } = window.location
const segments = pathname.split('/').filter(Boolean) const segments = pathname.split('/').filter(Boolean)
const firstSegment = segments[0] || '' const firstSegment = segments[0] || ''
// Check if first segment is a valid language code
const currentLang = langs.includes(firstSegment) ? firstSegment : '' // Get current language or empty string if invalid
// Get next language in rotation (empty string means default locale) const currentLang = langs.includes(firstSegment)
? firstSegment
: ''
const currentIndex = langs.indexOf(currentLang) const currentIndex = langs.indexOf(currentLang)
const nextLang = langs[(currentIndex + 1) % langs.length] const nextLang = langs[(currentIndex + 1) % langs.length]
const newPath = buildNewPath(currentLang, nextLang, segments, pathname) || '/' const newPath = buildNewPath(currentLang, nextLang, segments, pathname) || '/'
window.location.href = `${newPath}${search}${hash}` window.location.href = `${newPath}${search}${hash}`
}) })
}) })
// Handle path construction for both cases:
// 1. Current path has language prefix
// 2. Current path has no language prefix
function buildNewPath(currentLang, nextLang, segments, pathname) { function buildNewPath(currentLang, nextLang, segments, pathname) {
if (currentLang) { if (currentLang) {
segments[0] = nextLang || segments[0] segments[0] = nextLang || segments[0]
return nextLang ? `/${segments.join('/')}` : `/${segments.slice(1).join('/')}`
// Handle path with or without language code
return nextLang
? `/${segments.join('/')}`
: `/${segments.slice(1).join('/')}`
} }
return nextLang ? `/${nextLang}${pathname}` : pathname
return nextLang
? `/${nextLang}${pathname}`
: pathname
} }
</script> </script>

View file

@ -51,13 +51,13 @@ const isTagActive = isTagPage(currentPath)
const isAboutActive = isAboutPage(currentPath) const isAboutActive = isAboutPage(currentPath)
--- ---
<nav class="mb-20 mt-13 text-5.6 text-secondary font-semibold leading-13 font-navbar"> <nav class="mb-16 mt-13 text-5.6 font-semibold leading-13 font-navbar">
<ul> <ul>
<li> <li>
<a <a
href={getLocalizedPath('/')} href={getLocalizedPath('/')}
class:list={[ class:list={[
isPostActive ? 'font-bold text-primary' : 'text-secondary', isPostActive && 'font-bold text-primary'
]} ]}
> >
{currentUI.posts} {currentUI.posts}
@ -67,7 +67,7 @@ const isAboutActive = isAboutPage(currentPath)
<a <a
href={getLocalizedPath('/tags')} href={getLocalizedPath('/tags')}
class:list={[ class:list={[
isTagActive ? 'font-bold text-primary' : 'text-secondary', isTagActive && 'font-bold text-primary'
]} ]}
> >
{currentUI.tags} {currentUI.tags}
@ -77,7 +77,7 @@ const isAboutActive = isAboutPage(currentPath)
<a <a
href={getLocalizedPath('/about')} href={getLocalizedPath('/about')}
class:list={[ class:list={[
isAboutActive ? 'font-bold text-primary' : 'text-secondary', isAboutActive && 'font-bold text-primary'
]} ]}
> >
{currentUI.about} {currentUI.about}

View file

@ -25,7 +25,7 @@ function toggleTheme() {
// Execute transition // Execute transition
const transition = document.startViewTransition(() => { const transition = document.startViewTransition(() => {
switchTheme() switchTheme()
}) as any })
// Clean up after transition // Clean up after transition
transition.finished.then(() => { transition.finished.then(() => {
@ -63,9 +63,13 @@ window.addEventListener('pageshow', (event) => {
<button <button
aria-pressed="false" aria-pressed="false"
aria-label="Theme Toggle Button" aria-label="Theme Toggle Button"
class="absolute right-9.6 top-19.8 z-99 aspect-square w-7 c-secondary active:scale-92" class="absolute right-9.6 top-19.8 z-9 aspect-square w-7 c-secondary active:scale-92"
>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
> >
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<path d="m12 1c-6.1 0-11 4.9-11 11s4.9 11 11 11 11-4.9 11-11-4.9-11-11-11m0 20c-5.8 0-10.5-4-10.5-9s4.7-9 10.5-9 10.5 4 10.5 9-4.7 9-10.5 9" /> <path d="m12 1c-6.1 0-11 4.9-11 11s4.9 11 11 11 11-4.9 11-11-4.9-11-11-11m0 20c-5.8 0-10.5-4-10.5-9s4.7-9 10.5-9 10.5 4 10.5 9-4.7 9-10.5 9" />
</svg> </svg>
</button> </button>

View file

@ -16,16 +16,16 @@ export const themeConfig: ThemeConfig = {
color: { color: {
mode: 'light', // light, dark. Matches system theme by default, falls back to configured theme mode if not available. mode: 'light', // light, dark. Matches system theme by default, falls back to configured theme mode if not available.
light: { light: {
primary: '#17191A', // title text color in light mode primary: '#17191A', // title font color
secondary: '#505050', // post text color in light mode secondary: '#505050', // post font color
background: '#FAEDE4', // background color in light mode background: '#FAEDE4', // background color
codeTheme: 'github-light', // code block theme in light mode. See more at https://shiki.style/themes and https://vscodethemes.com/ codeTheme: 'github-light', // code block theme. See more at https://shiki.style/themes and https://vscodethemes.com/
}, },
dark: { dark: {
primary: '#BEBEBE', // title text color in dark mode primary: '#BEBEBE', // title font color
secondary: '#A0A09F', // post text color in dark mode secondary: '#A0A09F', // post font color
background: '#161616', // background color in dark mode background: '#161616', // background color
codeTheme: 'github-dark', // code block theme in dark mode. See more at https://shiki.style/themes and https://vscodethemes.com/ codeTheme: 'github-dark', // code block theme. See more at https://shiki.style/themes and https://vscodethemes.com/
}, },
}, },
// COLOR SETTINGS >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> END // COLOR SETTINGS >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> END

View file

@ -0,0 +1,185 @@
---
title: 漫记京都街巷富士山下与 Collector
published: 2025-01-18
tags: ["鲁迅"]
---
![](https://image.radishzz.cc/image/gallery/03.webp)
我冒了严寒,回到相隔二千余里,别了二十余年的故乡去。
时候既然是深冬;渐近故乡时,天气又阴晦了,冷风吹进船舱中,呜呜的响,从蓬隙向外一望,苍黄的天底下,远近横着几个萧索的荒村,没有一些活气。我的心禁不住悲凉起来了。
阿!这不是我二十年来时时记得的故乡?
我所记得的故乡全不如此。我的故乡好得多了。但要我记起他的美丽,说出他的佳处来,却又没有影像,没有言辞了。仿佛也就如此。于是我自己解释说:故乡本也如此,——虽然没有进步,也未必有如我所感的悲凉,这只是我自己心情的改变罢了,因为我这次回乡,本没有什么好心绪。
我这次是专为了别他而来的。我们多年聚族而居的老屋,已经公同卖给别姓了,交屋的期限,只在本年,所以必须赶在正月初一以前,永别了熟识的老屋,而且远离了熟识的故乡,搬家到我在谋食的异地去。
第二日清早晨我到了我家的门口了。瓦楞上许多枯草的断茎当风抖着,正在说明这老屋难免易主的原因。几房的本家大约已经搬走了,所以很寂静。我到了自家的房外,我的母亲早已迎着出来了,接着便飞出了八岁的侄儿宏儿。
我的母亲很高兴,但也藏着许多凄凉的神情,教我坐下,歇息,喝茶,且不谈搬家的事。宏儿没有见过我,远远的对面站着只是看。
但我们终于谈到搬家的事。我说外间的寓所已经租定了,又买了几件家具,此外须将家里所有的木器卖去,再去增添。母亲也说好,而且行李也略已齐集,木器不便搬运的,也小半卖去了,只是收不起钱来。
「你休息一两天,去拜望亲戚本家一回,我们便可以走了。」母亲说。
「是的。」
「还有闰土,他每到我家来时,总问起你,很想见你一回面。我已经将你到家的大约日期通知他,他也许就要来了。」
这时候,我的脑里忽然闪出一幅神异的图画来:深蓝的天空中挂着一轮金黄的圆月,下面是海边的沙地,都种著一望无际的碧绿的西瓜,其间有一个十一二岁的少年,项带银圈,手捏一柄钢叉,向一匹猹尽力的刺去,那猹却将身一扭,反从他的胯下逃走了。
这少年便是闰土。我认识他时,也不过十多岁,离现在将有三十年了;那时我的父亲还在世,家景也好,我正是一个少爷。那一年,我家是一件大祭祀的值年。这祭祀,说是三十多年才能轮到一回,所以很郑重;正月里供祖像,供品很多,祭器很讲究,拜的人也很多,祭器也很要防偷去。我家只有一个忙月(我们这里给人做工的分三种:整年给一定人家做工的叫长工;按日给人做工的叫短工;自己也种地,只在过年过节以及收租时候来给一定人家做工的称忙月),忙不过来,他便对父亲说,可以叫他的儿子闰土来管祭器的。
我的父亲允许了;我也很高兴,因为我早听到闰土这名字,而且知道他和我仿佛年纪,闰月生的,五行缺土,所以他的父亲叫他闰土。他是能装弶捉小鸟雀的。
我于是日日盼望新年,新年到,闰土也就到了。好容易到了年末,有一日,母亲告诉我,闰土来了,我便飞跑的去看。他正在厨房里,紫色的圆脸,头戴一顶小毡帽,颈上套一个明晃晃的银项圈,这可见他的父亲十分爱他,怕他死去,所以在神佛面前许下愿心,用圈子将他套住了。他见人很怕羞,只是不怕我,没有旁人的时候,便和我说话,于是不到半日,我们便熟识了。
我们那时候不知道谈些什么,只记得闰土很高兴,说是上城之后,见了许多没有见过的东西。
第二日,我便要他捕鸟。他说:
“这不能。须大雪下了才好。我们沙地上,下了雪,我扫出一块空地来,用短棒支起一个大竹匾,撒下秕谷,看鸟雀来吃时,我远远地将缚在棒上的绳子只一拉,那鸟雀就罩在竹匾下了。什么都有:稻鸡,角鸡,鹁鸪,蓝背……”
我于是又很盼望下雪。
闰土又对我说:
“现在太冷,你夏天到我们这里来。我们日里到海边捡贝壳去,红的绿的都有,鬼见怕也有,观音手也有。晚上我和爹管西瓜去,你也去。”
“管贼么?”
“不是。走路的人口渴了摘一个瓜吃,我们这里是不算偷的。要管的是獾猪,刺猬,猹。月亮底下,你听,啦啦的响了,猹在咬瓜了。你便捏了胡叉,轻轻地走去……”
我那时并不知道这所谓猹的是怎么一件东西——便是现在也没有知道——只是无端的觉得状如小狗而很凶猛。
“他不咬人么?”
“有胡叉呢。走到了,看见猹了,你便刺。这畜生很伶俐,倒向你奔来,反从胯下窜了。他的皮毛是油一般的滑……”
我素不知道天下有这许多新鲜事:海边有如许五色的贝壳;西瓜有这样危险的经历,我先前单知道他在水果店里出卖罢了。
“我们沙地里,潮汛要来的时候,就有许多跳鱼儿只是跳,都有青蛙似的两个脚……”
阿!闰土的心里有无穷无尽的希奇的事,都是我往常的朋友所不知道的。他们不知道一些事,闰土在海边时,他们都和我一样只看见院子里高墙上的四角的天空。
可惜正月过去了,闰土须回家里去,我急得大哭,他也躲到厨房里,哭着不肯出门,但终于被他父亲带走了。他后来还托他的父亲带给我一包贝壳和几支很好看的鸟毛,我也曾送他一两次东西,但从此没有再见面。
现在我的母亲提起了他,我这儿时的记忆,忽而全都闪电似的苏生过来,似乎看到了我的美丽的故乡了。我应声说:
“这好极!他,——怎样?……”
“他?……他景况也很不如意……”母亲说着,便向房外看,“这些人又来了。说是买木器,顺手也就随便拿走的,我得去看看。”
母亲站起身,出去了。门外有几个女人的声音。我便招宏儿走近面前,和他闲话:问他可会写字,可愿意出门。
“我们坐火车去么?”
“我们坐火车去。”
“船呢?”
“先坐船,……”
“哈!这模样了!胡子这么长了!”一种尖利的怪声突然大叫起来。
我吃了一吓,赶忙抬起头,却见一个凸颧骨、薄嘴唇、五十岁上下的女人站在我面前,两手搭在髀间,没有系裙,张着两脚,正像一个画图仪器里细脚伶仃的圆规。
我愕然了。
“不认识了么?我还抱过你咧!”
我愈加愕然了。幸而我的母亲也就进来,从旁说:
“他多年出门,统忘却了。你该记得罢,”便向着我说,“这是斜对门的杨二嫂,……开豆腐店的。”
哦,我记得了。我孩子时候,在斜对门的豆腐店里确乎终日坐着一个杨二嫂,人都叫伊“豆腐西施”。但是擦着白粉,颧骨没有这么高,嘴唇也没有这么薄,而且终日坐着,我也从没有见过这圆规式的姿势。那时人说:因为伊,这豆腐店的买卖非常好。但这大约因为年龄的关系,我却并未蒙着一毫感化,所以竟完全忘却了。然而圆规很不平,显出鄙夷的神色,仿佛嗤笑法国人不知道拿破仑,美国人不知道华盛顿似的,冷笑说:
“忘了?这真是贵人眼高……”
“那有这事……我……”我惶恐着,站起来说。
“那么,我对你说。迅哥儿,你阔了,搬动又笨重,你还要什么这些破烂木器,让我拿去罢。我们小户人家,用得着。”
“我并没有阔哩。我须卖了这些,再去……”
“阿呀呀,你放了道台了,还说不阔?你现在有三房姨太太;出门便是八抬的大轿,还说不阔?吓,什么都瞒不过我。”
我知道无话可说了,便闭了口,默默的站着。
“阿呀阿呀,真是愈有钱,便愈是一毫不肯放松,愈是一毫不肯放松,便愈有钱……”圆规一面愤愤的回转身,一面絮絮的说,慢慢向外走,顺便将我母亲的一副手套塞在裤腰里,出去了。
此后又有近处的本家和亲戚来访问我。我一面应酬,偷空便收拾些行李,这样的过了三四天。
一日是天气很冷的午后,我吃过午饭,坐着喝茶,觉得外面有人进来了,便回头去看。我看时,不由的非常出惊,慌忙站起身,迎着走去。
这来的便是闰土。虽然我一见便知道是闰土,但又不是我这记忆上的闰土了。他身材增加了一倍;先前的紫色的圆脸,已经变作灰黄,而且加上了很深的皱纹;眼睛也像他父亲一样,周围都肿得通红,这我知道,在海边种地的人,终日吹着海风,大抵是这样的。他头上是一顶破毡帽,身上只一件极薄的棉衣,浑身瑟索着;手里提着一个纸包和一支长烟管,那手也不是我所记得的红活圆实的手,却又粗又笨而且开裂,像是松树皮了。
我这时很兴奋,但不知道怎么说才好,只是说:
“阿!闰土哥,——你来了?……”
我接着便有许多话,想要连珠一般涌出:角鸡,跳鱼儿,贝壳,猹,……但又总觉得被什么挡着似的,单在脑里面回旋,吐不出口外去。
他站住了,脸上现出欢喜和凄凉的神情;动着嘴唇,却没有作声。他的态度终于恭敬起来了,分明的叫道:
“老爷!……”
我似乎打了一个寒噤;我就知道,我们之间已经隔了一层可悲的厚障壁了。我也说不出话。
他回过头去说,“水生,给老爷磕头。”便拖出躲在背后的孩子来,这正是一个廿年前的闰土,只是黄瘦些,颈子上没有银圈罢了。 “这是第五个孩子,没有见过世面,躲躲闪闪……”
母亲和宏儿下楼来了,他们大约也听到了声音。
“老太太。信是早收到了。我实在喜欢的不得了,知道老爷回来……”闰土说。
“阿,你怎的这样客气起来。你们先前不是哥弟称呼么?还是照旧:迅哥儿。”母亲高兴的说。
“阿呀,老太太真是……这成什么规矩。那时是孩子,不懂事……”闰土说着,又叫水生上来打拱,那孩子却害羞,紧紧的只贴在他背后。
“他就是水生?第五个?都是生人,怕生也难怪的;还是宏儿和他去走走。”母亲说。
宏儿听得这话,便来招水生,水生却松松爽爽同他一路出去了。母亲叫闰土坐,他迟疑了一回,终于就了坐,将长烟管靠在桌旁,递过纸包来,说:
“冬天没有什么东西了。这一点干青豆倒是自家晒在那里的,请老爷……”
我问问他的景况。他只是摇头。
“非常难。第六个孩子也会帮忙了,却总是吃不够……又不太平……什么地方都要钱,没有规定……收成又坏。种出东西来,挑去卖,总要捐几回钱,折了本;不去卖,又只能烂掉……”
他只是摇头;脸上虽然刻着许多皱纹,却全然不动,仿佛石像一般。他大约只是觉得苦,却又形容不出,沉默了片时,便拿起烟管来默默的吸烟了。
母亲问他,知道他的家里事务忙,明天便得回去;又没有吃过午饭,便叫他自己到厨下炒饭吃去。
他出去了;母亲和我都叹息他的景况:多子,饥荒,苛税,兵,匪,官,绅,都苦得他像一个木偶人了。母亲对我说,凡是不必搬走的东西,尽可以送他,可以听他自己去拣择。
下午,他拣好了几件东西:两条长桌,四个椅子,一副香炉和烛台,一杆抬秤。他又要所有的草灰(我们这里煮饭是烧稻草的,那灰,可以做沙地的肥料),待我们启程的时候,他用船来载去。
夜间,我们又谈些闲天,都是无关紧要的话;第二天早晨,他就领了水生回去了。
又过了九日,是我们启程的日期。闰土早晨便到了,水生没有同来,却只带着一个五岁的女儿管船只。我们终日很忙碌,再没有谈天的工夫。来客也不少,有送行的,有拿东西的,有送行兼拿东西的。待到傍晚我们上船的时候,这老屋里的所有破旧大小粗细东西,已经一扫而空了。
我们的船向前走,两岸的青山在黄昏中,都装成了深黛颜色,连着退向船后梢去。
宏儿和我靠着船窗,同看外面模糊的风景,他忽然问道:
“大伯!我们什么时候回来?”
“回来?你怎么还没有走就想回来了。”
“可是,水生约我到他家玩去咧……”他睁着大的黑眼睛,痴痴的想。
我和母亲也都有些惘然,于是又提起闰土来。母亲说,那豆腐西施的杨二嫂,自从我家收拾行李以来,本是每日必到的,前天伊在灰堆里,掏出十多个碗碟来,议论之后,便定说是闰土埋着的,他可以在运灰的时候,一齐搬回家里去;杨二嫂发见了这件事,自己很以为功,便拿了那狗气杀(这是我们这里养鸡的器具,木盘上面有着栅栏,内盛食料,鸡可以伸进颈子去啄,狗却不能,只能看着气死),飞也似的跑了,亏伊装着这么高低的小脚,竟跑得这样快。
老屋离我愈远了;故乡的山水也都渐渐远离了我,但我却并不感到怎样的留恋。我只觉得我四面有看不见的高墙,将我隔成孤身,使我非常气闷;那西瓜地上的银项圈的小英雄的影像,我本来十分清楚,现在却忽地模糊了,又使我非常的悲哀。
母亲和宏儿都睡着了。
我躺着,听船底潺潺的水声,知道我在走我的路。我想:我竟与闰土隔绝到这地步了,但我们的后辈还是一气,宏儿不是正在想念水生么。我希望他们不再像我,又大家隔膜起来……然而我又不愿意他们因为要一气,都如我的辛苦展转而生活,也不愿意他们都如闰土的辛苦麻木而生活,也不愿意都如别人的辛苦恣睢而生活。他们应该有新的生活,为我们所未经生活过的。
我想到希望,忽然害怕起来了。闰土要香炉和烛台的时候,我还暗地里笑他,以为他总是崇拜偶像,什么时候都不忘却。现在我所谓希望,不也是我自己手制的偶像么?只是他的愿望切近,我的愿望茫远罢了。
我在朦胧中,眼前展开一片海边碧绿的沙地来,上面深蓝的天空中挂着一轮金黄的圆月。我想:希望本是无所谓有,无所谓无的。这正如地上的路;其实地上本没有路,走的人多了,也便成了路。
一九二一年一月

View file

@ -0,0 +1,185 @@
---
title: 开源的心理建设
published: 2025-07-18
tags: ["鲁迅"]
---
![](https://image.radishzz.cc/image/gallery/03.webp)
我冒了严寒,回到相隔二千余里,别了二十余年的故乡去。
时候既然是深冬;渐近故乡时,天气又阴晦了,冷风吹进船舱中,呜呜的响,从蓬隙向外一望,苍黄的天底下,远近横着几个萧索的荒村,没有一些活气。我的心禁不住悲凉起来了。
阿!这不是我二十年来时时记得的故乡?
我所记得的故乡全不如此。我的故乡好得多了。但要我记起他的美丽,说出他的佳处来,却又没有影像,没有言辞了。仿佛也就如此。于是我自己解释说:故乡本也如此,——虽然没有进步,也未必有如我所感的悲凉,这只是我自己心情的改变罢了,因为我这次回乡,本没有什么好心绪。
我这次是专为了别他而来的。我们多年聚族而居的老屋,已经公同卖给别姓了,交屋的期限,只在本年,所以必须赶在正月初一以前,永别了熟识的老屋,而且远离了熟识的故乡,搬家到我在谋食的异地去。
第二日清早晨我到了我家的门口了。瓦楞上许多枯草的断茎当风抖着,正在说明这老屋难免易主的原因。几房的本家大约已经搬走了,所以很寂静。我到了自家的房外,我的母亲早已迎着出来了,接着便飞出了八岁的侄儿宏儿。
我的母亲很高兴,但也藏着许多凄凉的神情,教我坐下,歇息,喝茶,且不谈搬家的事。宏儿没有见过我,远远的对面站着只是看。
但我们终于谈到搬家的事。我说外间的寓所已经租定了,又买了几件家具,此外须将家里所有的木器卖去,再去增添。母亲也说好,而且行李也略已齐集,木器不便搬运的,也小半卖去了,只是收不起钱来。
「你休息一两天,去拜望亲戚本家一回,我们便可以走了。」母亲说。
「是的。」
「还有闰土,他每到我家来时,总问起你,很想见你一回面。我已经将你到家的大约日期通知他,他也许就要来了。」
这时候,我的脑里忽然闪出一幅神异的图画来:深蓝的天空中挂着一轮金黄的圆月,下面是海边的沙地,都种著一望无际的碧绿的西瓜,其间有一个十一二岁的少年,项带银圈,手捏一柄钢叉,向一匹猹尽力的刺去,那猹却将身一扭,反从他的胯下逃走了。
这少年便是闰土。我认识他时,也不过十多岁,离现在将有三十年了;那时我的父亲还在世,家景也好,我正是一个少爷。那一年,我家是一件大祭祀的值年。这祭祀,说是三十多年才能轮到一回,所以很郑重;正月里供祖像,供品很多,祭器很讲究,拜的人也很多,祭器也很要防偷去。我家只有一个忙月(我们这里给人做工的分三种:整年给一定人家做工的叫长工;按日给人做工的叫短工;自己也种地,只在过年过节以及收租时候来给一定人家做工的称忙月),忙不过来,他便对父亲说,可以叫他的儿子闰土来管祭器的。
我的父亲允许了;我也很高兴,因为我早听到闰土这名字,而且知道他和我仿佛年纪,闰月生的,五行缺土,所以他的父亲叫他闰土。他是能装弶捉小鸟雀的。
我于是日日盼望新年,新年到,闰土也就到了。好容易到了年末,有一日,母亲告诉我,闰土来了,我便飞跑的去看。他正在厨房里,紫色的圆脸,头戴一顶小毡帽,颈上套一个明晃晃的银项圈,这可见他的父亲十分爱他,怕他死去,所以在神佛面前许下愿心,用圈子将他套住了。他见人很怕羞,只是不怕我,没有旁人的时候,便和我说话,于是不到半日,我们便熟识了。
我们那时候不知道谈些什么,只记得闰土很高兴,说是上城之后,见了许多没有见过的东西。
第二日,我便要他捕鸟。他说:
“这不能。须大雪下了才好。我们沙地上,下了雪,我扫出一块空地来,用短棒支起一个大竹匾,撒下秕谷,看鸟雀来吃时,我远远地将缚在棒上的绳子只一拉,那鸟雀就罩在竹匾下了。什么都有:稻鸡,角鸡,鹁鸪,蓝背……”
我于是又很盼望下雪。
闰土又对我说:
“现在太冷,你夏天到我们这里来。我们日里到海边捡贝壳去,红的绿的都有,鬼见怕也有,观音手也有。晚上我和爹管西瓜去,你也去。”
“管贼么?”
“不是。走路的人口渴了摘一个瓜吃,我们这里是不算偷的。要管的是獾猪,刺猬,猹。月亮底下,你听,啦啦的响了,猹在咬瓜了。你便捏了胡叉,轻轻地走去……”
我那时并不知道这所谓猹的是怎么一件东西——便是现在也没有知道——只是无端的觉得状如小狗而很凶猛。
“他不咬人么?”
“有胡叉呢。走到了,看见猹了,你便刺。这畜生很伶俐,倒向你奔来,反从胯下窜了。他的皮毛是油一般的滑……”
我素不知道天下有这许多新鲜事:海边有如许五色的贝壳;西瓜有这样危险的经历,我先前单知道他在水果店里出卖罢了。
“我们沙地里,潮汛要来的时候,就有许多跳鱼儿只是跳,都有青蛙似的两个脚……”
阿!闰土的心里有无穷无尽的希奇的事,都是我往常的朋友所不知道的。他们不知道一些事,闰土在海边时,他们都和我一样只看见院子里高墙上的四角的天空。
可惜正月过去了,闰土须回家里去,我急得大哭,他也躲到厨房里,哭着不肯出门,但终于被他父亲带走了。他后来还托他的父亲带给我一包贝壳和几支很好看的鸟毛,我也曾送他一两次东西,但从此没有再见面。
现在我的母亲提起了他,我这儿时的记忆,忽而全都闪电似的苏生过来,似乎看到了我的美丽的故乡了。我应声说:
“这好极!他,——怎样?……”
“他?……他景况也很不如意……”母亲说着,便向房外看,“这些人又来了。说是买木器,顺手也就随便拿走的,我得去看看。”
母亲站起身,出去了。门外有几个女人的声音。我便招宏儿走近面前,和他闲话:问他可会写字,可愿意出门。
“我们坐火车去么?”
“我们坐火车去。”
“船呢?”
“先坐船,……”
“哈!这模样了!胡子这么长了!”一种尖利的怪声突然大叫起来。
我吃了一吓,赶忙抬起头,却见一个凸颧骨、薄嘴唇、五十岁上下的女人站在我面前,两手搭在髀间,没有系裙,张着两脚,正像一个画图仪器里细脚伶仃的圆规。
我愕然了。
“不认识了么?我还抱过你咧!”
我愈加愕然了。幸而我的母亲也就进来,从旁说:
“他多年出门,统忘却了。你该记得罢,”便向着我说,“这是斜对门的杨二嫂,……开豆腐店的。”
哦,我记得了。我孩子时候,在斜对门的豆腐店里确乎终日坐着一个杨二嫂,人都叫伊“豆腐西施”。但是擦着白粉,颧骨没有这么高,嘴唇也没有这么薄,而且终日坐着,我也从没有见过这圆规式的姿势。那时人说:因为伊,这豆腐店的买卖非常好。但这大约因为年龄的关系,我却并未蒙着一毫感化,所以竟完全忘却了。然而圆规很不平,显出鄙夷的神色,仿佛嗤笑法国人不知道拿破仑,美国人不知道华盛顿似的,冷笑说:
“忘了?这真是贵人眼高……”
“那有这事……我……”我惶恐着,站起来说。
“那么,我对你说。迅哥儿,你阔了,搬动又笨重,你还要什么这些破烂木器,让我拿去罢。我们小户人家,用得着。”
“我并没有阔哩。我须卖了这些,再去……”
“阿呀呀,你放了道台了,还说不阔?你现在有三房姨太太;出门便是八抬的大轿,还说不阔?吓,什么都瞒不过我。”
我知道无话可说了,便闭了口,默默的站着。
“阿呀阿呀,真是愈有钱,便愈是一毫不肯放松,愈是一毫不肯放松,便愈有钱……”圆规一面愤愤的回转身,一面絮絮的说,慢慢向外走,顺便将我母亲的一副手套塞在裤腰里,出去了。
此后又有近处的本家和亲戚来访问我。我一面应酬,偷空便收拾些行李,这样的过了三四天。
一日是天气很冷的午后,我吃过午饭,坐着喝茶,觉得外面有人进来了,便回头去看。我看时,不由的非常出惊,慌忙站起身,迎着走去。
这来的便是闰土。虽然我一见便知道是闰土,但又不是我这记忆上的闰土了。他身材增加了一倍;先前的紫色的圆脸,已经变作灰黄,而且加上了很深的皱纹;眼睛也像他父亲一样,周围都肿得通红,这我知道,在海边种地的人,终日吹着海风,大抵是这样的。他头上是一顶破毡帽,身上只一件极薄的棉衣,浑身瑟索着;手里提着一个纸包和一支长烟管,那手也不是我所记得的红活圆实的手,却又粗又笨而且开裂,像是松树皮了。
我这时很兴奋,但不知道怎么说才好,只是说:
“阿!闰土哥,——你来了?……”
我接着便有许多话,想要连珠一般涌出:角鸡,跳鱼儿,贝壳,猹,……但又总觉得被什么挡着似的,单在脑里面回旋,吐不出口外去。
他站住了,脸上现出欢喜和凄凉的神情;动着嘴唇,却没有作声。他的态度终于恭敬起来了,分明的叫道:
“老爷!……”
我似乎打了一个寒噤;我就知道,我们之间已经隔了一层可悲的厚障壁了。我也说不出话。
他回过头去说,“水生,给老爷磕头。”便拖出躲在背后的孩子来,这正是一个廿年前的闰土,只是黄瘦些,颈子上没有银圈罢了。 “这是第五个孩子,没有见过世面,躲躲闪闪……”
母亲和宏儿下楼来了,他们大约也听到了声音。
“老太太。信是早收到了。我实在喜欢的不得了,知道老爷回来……”闰土说。
“阿,你怎的这样客气起来。你们先前不是哥弟称呼么?还是照旧:迅哥儿。”母亲高兴的说。
“阿呀,老太太真是……这成什么规矩。那时是孩子,不懂事……”闰土说着,又叫水生上来打拱,那孩子却害羞,紧紧的只贴在他背后。
“他就是水生?第五个?都是生人,怕生也难怪的;还是宏儿和他去走走。”母亲说。
宏儿听得这话,便来招水生,水生却松松爽爽同他一路出去了。母亲叫闰土坐,他迟疑了一回,终于就了坐,将长烟管靠在桌旁,递过纸包来,说:
“冬天没有什么东西了。这一点干青豆倒是自家晒在那里的,请老爷……”
我问问他的景况。他只是摇头。
“非常难。第六个孩子也会帮忙了,却总是吃不够……又不太平……什么地方都要钱,没有规定……收成又坏。种出东西来,挑去卖,总要捐几回钱,折了本;不去卖,又只能烂掉……”
他只是摇头;脸上虽然刻着许多皱纹,却全然不动,仿佛石像一般。他大约只是觉得苦,却又形容不出,沉默了片时,便拿起烟管来默默的吸烟了。
母亲问他,知道他的家里事务忙,明天便得回去;又没有吃过午饭,便叫他自己到厨下炒饭吃去。
他出去了;母亲和我都叹息他的景况:多子,饥荒,苛税,兵,匪,官,绅,都苦得他像一个木偶人了。母亲对我说,凡是不必搬走的东西,尽可以送他,可以听他自己去拣择。
下午,他拣好了几件东西:两条长桌,四个椅子,一副香炉和烛台,一杆抬秤。他又要所有的草灰(我们这里煮饭是烧稻草的,那灰,可以做沙地的肥料),待我们启程的时候,他用船来载去。
夜间,我们又谈些闲天,都是无关紧要的话;第二天早晨,他就领了水生回去了。
又过了九日,是我们启程的日期。闰土早晨便到了,水生没有同来,却只带着一个五岁的女儿管船只。我们终日很忙碌,再没有谈天的工夫。来客也不少,有送行的,有拿东西的,有送行兼拿东西的。待到傍晚我们上船的时候,这老屋里的所有破旧大小粗细东西,已经一扫而空了。
我们的船向前走,两岸的青山在黄昏中,都装成了深黛颜色,连着退向船后梢去。
宏儿和我靠着船窗,同看外面模糊的风景,他忽然问道:
“大伯!我们什么时候回来?”
“回来?你怎么还没有走就想回来了。”
“可是,水生约我到他家玩去咧……”他睁着大的黑眼睛,痴痴的想。
我和母亲也都有些惘然,于是又提起闰土来。母亲说,那豆腐西施的杨二嫂,自从我家收拾行李以来,本是每日必到的,前天伊在灰堆里,掏出十多个碗碟来,议论之后,便定说是闰土埋着的,他可以在运灰的时候,一齐搬回家里去;杨二嫂发见了这件事,自己很以为功,便拿了那狗气杀(这是我们这里养鸡的器具,木盘上面有着栅栏,内盛食料,鸡可以伸进颈子去啄,狗却不能,只能看着气死),飞也似的跑了,亏伊装着这么高低的小脚,竟跑得这样快。
老屋离我愈远了;故乡的山水也都渐渐远离了我,但我却并不感到怎样的留恋。我只觉得我四面有看不见的高墙,将我隔成孤身,使我非常气闷;那西瓜地上的银项圈的小英雄的影像,我本来十分清楚,现在却忽地模糊了,又使我非常的悲哀。
母亲和宏儿都睡着了。
我躺着,听船底潺潺的水声,知道我在走我的路。我想:我竟与闰土隔绝到这地步了,但我们的后辈还是一气,宏儿不是正在想念水生么。我希望他们不再像我,又大家隔膜起来……然而我又不愿意他们因为要一气,都如我的辛苦展转而生活,也不愿意他们都如闰土的辛苦麻木而生活,也不愿意都如别人的辛苦恣睢而生活。他们应该有新的生活,为我们所未经生活过的。
我想到希望,忽然害怕起来了。闰土要香炉和烛台的时候,我还暗地里笑他,以为他总是崇拜偶像,什么时候都不忘却。现在我所谓希望,不也是我自己手制的偶像么?只是他的愿望切近,我的愿望茫远罢了。
我在朦胧中,眼前展开一片海边碧绿的沙地来,上面深蓝的天空中挂着一轮金黄的圆月。我想:希望本是无所谓有,无所谓无的。这正如地上的路;其实地上本没有路,走的人多了,也便成了路。
一九二一年一月

View file

@ -1,6 +1,6 @@
--- ---
title: 故乡 title: 一次意外的森林徒步
published: 1977-07-18 published: 2025-08-18
tags: ["鲁迅"] tags: ["鲁迅"]
--- ---

View file

@ -1,6 +1,6 @@
--- ---
title: 罗生门 title: 罗生门
published: 1984-09-17 published: 2024-09-17
tags: ["黑泽明"] tags: ["黑泽明"]
--- ---

View file

@ -1,7 +1,7 @@
--- ---
import Footer from '@/components/Footer.astro' import Footer from '@/components/Footer.astro'
import Header from '@/components/Header.astro' import Header from '@/components/Header.astro'
import LanguageSwitcher from '@/components/LangSwitch.astro' import LanguageSwitcher from '@/components/LanguageSwitcher.astro'
import Navigation from '@/components/Navbar.astro' import Navigation from '@/components/Navbar.astro'
import PhotoSwipe from '@/components/PhotoSwipe.astro' import PhotoSwipe from '@/components/PhotoSwipe.astro'
import Scrollbar from '@/components/Scrollbar.astro' import Scrollbar from '@/components/Scrollbar.astro'

View file

@ -1,15 +1,17 @@
--- ---
import { themeConfig } from '@/config'
import Layout from '@/layouts/Layout.astro' import Layout from '@/layouts/Layout.astro'
import { getPinnedPosts, getPosts } from '@/utils/content' import { getPinnedPosts, getPostsByYear } from '@/utils/content'
import { generateLanguagePaths } from '@/utils/i18n'
export function getStaticPaths() { export function getStaticPaths() {
return generateLanguagePaths() return themeConfig.global.moreLocale.map(lang => ({
params: { lang },
}))
} }
const { lang } = Astro.params const { lang } = Astro.params
const pinnedPosts = await getPinnedPosts() const pinnedPosts = await getPinnedPosts(lang)
const posts = await getPosts() const postsByYear = await getPostsByYear(lang)
--- ---
<Layout> <Layout>
@ -21,7 +23,10 @@ const posts = await getPosts()
<li> <li>
<a href={`/${lang}/posts/${post.data.slug || post.slug}/`}> <a href={`/${lang}/posts/${post.data.slug || post.slug}/`}>
{post.data.title} {post.data.title}
<time>({post.data.published.toISOString().split('T')[0]})</time> <time class="block lg:inline lg:before:content-['_']">
{post.data.published.toLocaleDateString('en-US', { month: '2-digit', day: '2-digit' }).replace('/', '-')}
{post.remarkPluginFrontmatter?.minutes && <span> · {post.remarkPluginFrontmatter.minutes} min</span>}
</time>
</a> </a>
</li> </li>
))} ))}
@ -29,17 +34,27 @@ const posts = await getPosts()
</section> </section>
)} )}
<section> {[...postsByYear.entries()].map(([year, posts]) => (
// Year Group
<section class="mt-8">
{/* Year */}
<time class="text-8 font-date">{year}</time>
{/* Posts List */}
<ul> <ul>
{posts.map(post => ( {posts.map(post => (
<li> // Single Post
<a href={`/${lang}/posts/${post.data.slug || post.slug}/`}> <li class="mt-6">
{post.data.title} {/* Post Title */}
<time>({post.data.published.toISOString().split('T')[0]})</time> <a href={`/posts/${post.data.slug || post.slug}/`}>{post.data.title}</a>
</a> {/* Post Date */}
<time class="text-5.6 leading-7 font-date block lg:inline lg:before:content-['_'] opacity-30" aria-hidden="true">
{post.data.published.toLocaleDateString('en-US', { month: '2-digit', day: '2-digit' }).replace('/', '-')}
{post.remarkPluginFrontmatter?.minutes && <span class="ml-2"> {post.remarkPluginFrontmatter.minutes} min</span>}
</time>
</li> </li>
))} ))}
</ul> </ul>
</section> </section>
))}
</main> </main>
</Layout> </Layout>

View file

@ -16,13 +16,16 @@ export async function getStaticPaths() {
} }
const { post } = Astro.props const { post } = Astro.props
const { Content } = await post.render() const { Content, remarkPluginFrontmatter } = await post.render()
--- ---
<Layout> <Layout>
<article> <article>
<h1>{post.data.title}</h1> <h1>{post.data.title}</h1>
<time>{post.data.published.toISOString().split('T')[0]}</time> <time>
{post.data.published.toLocaleDateString('en-US', { month: '2-digit', day: '2-digit' }).replace('/', '-')}
{remarkPluginFrontmatter.minutes && <span> · {remarkPluginFrontmatter.minutes} min</span>}
</time>
<Content /> <Content />
</article> </article>
</Layout> </Layout>

View file

@ -1,9 +1,9 @@
--- ---
import Layout from '@/layouts/Layout.astro' import Layout from '@/layouts/Layout.astro'
import { getPinnedPosts, getPosts } from '@/utils/content' import { getPinnedPosts, getPostsByYear } from '@/utils/content'
const posts = await getPosts()
const pinnedPosts = await getPinnedPosts() const pinnedPosts = await getPinnedPosts()
const postsByYear = await getPostsByYear()
--- ---
<Layout> <Layout>
@ -15,7 +15,10 @@ const pinnedPosts = await getPinnedPosts()
<li> <li>
<a href={`/posts/${post.data.slug || post.slug}/`}> <a href={`/posts/${post.data.slug || post.slug}/`}>
{post.data.title} {post.data.title}
<time>({post.data.published.toISOString().split('T')[0]})</time> <time class="block lg:inline lg:before:content-['_']">
{post.data.published.toLocaleDateString('en-US', { month: '2-digit', day: '2-digit' }).replace('/', '-')}
{post.remarkPluginFrontmatter?.minutes && <span> {post.remarkPluginFrontmatter.minutes} min</span>}
</time>
</a> </a>
</li> </li>
))} ))}
@ -23,17 +26,27 @@ const pinnedPosts = await getPinnedPosts()
</section> </section>
)} )}
<section> {[...postsByYear.entries()].map(([year, posts]) => (
// Year Group
<section class="mt-8">
{/* Year */}
<time class="text-8 font-date">{year}</time>
{/* Posts List */}
<ul> <ul>
{posts.map(post => ( {posts.map(post => (
<li> // Single Post
<a href={`/posts/${post.data.slug || post.slug}/`}> <li class="mt-6">
{post.data.title} {/* Post Title */}
<time>({post.data.published.toISOString().split('T')[0]})</time> <a class="hover:text-secondary" href={`/posts/${post.data.slug || post.slug}/`}>{post.data.title}</a>
</a> {/* Post Date */}
<time class="text-5.6 leading-7 font-date block lg:inline lg:before:content-['_'] opacity-30" aria-hidden="true">
{post.data.published.toLocaleDateString('en-US', { month: '2-digit', day: '2-digit' }).replace('/', '-')}
{post.remarkPluginFrontmatter?.minutes && <span class="ml-2"> {post.remarkPluginFrontmatter.minutes} min</span>}
</time>
</li> </li>
))} ))}
</ul> </ul>
</section> </section>
))}
</main> </main>
</Layout> </Layout>

View file

@ -16,7 +16,7 @@ export async function getStaticPaths() {
} }
const { post } = Astro.props const { post } = Astro.props
const { Content } = await post.render() const { Content, remarkPluginFrontmatter } = await post.render()
--- ---
<Layout <Layout
@ -26,7 +26,10 @@ const { Content } = await post.render()
> >
<article> <article>
<h1>{post.data.title}</h1> <h1>{post.data.title}</h1>
<time>{post.data.published.toISOString().split('T')[0]}</time> <time>
{post.data.published.toLocaleDateString('en-US', { month: '2-digit', day: '2-digit' }).replace('/', '-')}
{remarkPluginFrontmatter.minutes && <span> · {remarkPluginFrontmatter.minutes} min</span>}
</time>
<Content /> <Content />
</article> </article>
</Layout> </Layout>

View file

@ -4,17 +4,17 @@
--uno-colors-background: theme('colors.background'); --uno-colors-background: theme('colors.background');
} }
html { html {
--at-apply: 'scroll-smooth antialiased text-62.5%'; --at-apply: 'scroll-smooth antialiased text-62.5% bg-background c-secondary ';
} }
body { body {
--at-apply: 'bg-background c-secondary text-1.6rem'; --at-apply: 'text-1.6rem';
} }
* { * {
scrollbar-width: none; scrollbar-width: none;
-ms-overflow-style: none; -ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
} }
::-webkit-scrollbar {
display: none;
} }
h1, h2, h3 { h1, h2, h3 {
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
@ -53,18 +53,27 @@ html:not(.dark) {
--from: 100% 0 0 0; --from: 100% 0 0 0;
} }
::view-transition-new(theme-transition) { ::view-transition-new(theme-transition) {
transition: none;
animation: reveal 1s cubic-bezier(0.4, 0, 0.2, 1); animation: reveal 1s cubic-bezier(0.4, 0, 0.2, 1);
clip-path: inset(0 0 0 0); clip-path: inset(0 0 0 0);
z-index: 2; z-index: 2;
} }
::view-transition-old(theme-transition) { ::view-transition-old(theme-transition) {
transition: none;
animation: none; animation: none;
z-index: -1; z-index: 1;
} }
@supports not (view-transition-name: none) { @supports not (view-transition-name: none) {
body:not([data-restore-theme]) { body:not([data-restore-theme]) {
--at-apply: 'transition-all duration-500 ease-in-out'; --at-apply: 'transition-colors duration-500 ease-in-out';
} }
} }
/* iOS browsers flash issue fix */
/* @media (prefers-color-scheme: dark) {
html {
background: #111;
}
}
@media (prefers-color-scheme: light) {
html {
background: #f0f0f0;
}
} */

10
src/types/global.d.ts vendored
View file

@ -4,11 +4,15 @@ declare global {
namespace astroHTML.JSX { namespace astroHTML.JSX {
interface HTMLAttributes extends AttributifyAttributes {} interface HTMLAttributes extends AttributifyAttributes {}
} }
interface Document {
startViewTransition: (updateCallback: () => void) => ViewTransition
} }
declare global { interface ViewTransition {
interface Document { finished: Promise<void>
startViewTransition: (callback: () => void) => void ready: Promise<void>
updateCallbackDone: Promise<void>
} }
} }

View file

@ -3,13 +3,24 @@ import themeConfig from '@/config'
import { langPath } from '@/utils/ui' import { langPath } from '@/utils/ui'
import { getCollection } from 'astro:content' import { getCollection } from 'astro:content'
export type Post = CollectionEntry<'posts'> // Type definitions
export type Post = CollectionEntry<'posts'> & {
remarkPluginFrontmatter?: {
minutes?: number
}
}
export type PostData = Post['data'] export type PostData = Post['data']
export type PostsGroupByYear = Map<number, Post[]> export type PostsGroupByYear = Map<number, Post[]>
// Check if the slug is duplicated under the same language. // Get post metadata including reading time
async function getPostMeta(post: CollectionEntry<'posts'>): Promise<Post> {
const { remarkPluginFrontmatter } = await post.render()
return { ...post, remarkPluginFrontmatter }
}
// Check if the slug is duplicated under the same language
export async function checkSlugDuplication(posts: Post[]): Promise<string[]> { export async function checkSlugDuplication(posts: Post[]): Promise<string[]> {
const slugMap = new Map<string, Set<string>>() // Map<lang, Set<slug>> const slugMap = new Map<string, Set<string>>()
const duplicates: string[] = [] const duplicates: string[] = []
posts.forEach((post) => { posts.forEach((post) => {
@ -48,7 +59,9 @@ export async function getPosts(lang?: string) {
}, },
) )
return posts.sort((a: Post, b: Post) => const postsWithMeta = await Promise.all(posts.map(getPostMeta))
return postsWithMeta.sort((a: Post, b: Post) =>
b.data.published.valueOf() - a.data.published.valueOf(), b.data.published.valueOf() - a.data.published.valueOf(),
) )
} }
@ -65,7 +78,7 @@ export async function getPinnedPosts(lang?: string) {
return posts.filter(post => post.data.pin) return posts.filter(post => post.data.pin)
} }
// Get posts group by year (not pinned) // Get posts grouped by year (not pinned)
export async function getPostsByYear(lang?: string): Promise<PostsGroupByYear> { export async function getPostsByYear(lang?: string): Promise<PostsGroupByYear> {
const posts = await getRegularPosts(lang) const posts = await getRegularPosts(lang)
const yearMap = new Map<number, Post[]>() const yearMap = new Map<number, Post[]>()
@ -108,7 +121,7 @@ export async function getAllTags(lang?: string) {
return Array.from(tags) return Array.from(tags)
} }
// Get posts group by each tag // Get posts grouped by tags
export async function getPostsGroupByTags(lang?: string) { export async function getPostsGroupByTags(lang?: string) {
const posts = await getRegularPosts(lang) const posts = await getRegularPosts(lang)
const tagMap = new Map<string, Post[]>() const tagMap = new Map<string, Post[]>()
@ -127,7 +140,7 @@ export async function getPostsGroupByTags(lang?: string) {
return tagMap return tagMap
} }
// Get all posts by one tag // Get all posts by specific tag
export async function getPostsByTag(tag: string, lang?: string) { export async function getPostsByTag(tag: string, lang?: string) {
const posts = await getRegularPosts(lang) const posts = await getRegularPosts(lang)
return posts.filter((post: Post) => return posts.filter((post: Post) =>

View file

@ -34,8 +34,8 @@ export const ui = {
about: 'About', about: 'About',
}, },
'es': { 'es': {
posts: 'Posts', posts: 'Artículos',
tags: 'Tags', tags: 'Etiquetas',
about: 'Sobre', about: 'Sobre',
}, },
'ru': { 'ru': {