41 | )
42 | );
43 | }
44 |
45 | export default Toc;
46 |
--------------------------------------------------------------------------------
/src/content/config.ts:
--------------------------------------------------------------------------------
1 | import { defineCollection, z } from 'astro:content';
2 |
3 | const postCollection = defineCollection({
4 | type: 'content',
5 | schema: z
6 | .object({
7 | /** Title */
8 | title: z.string(),
9 | /** Description */
10 | description: z.string().optional(),
11 | /** Tags */
12 | tags: z.array(z.string()).optional(),
13 | /** Whether it's a draft */
14 | draft: z.boolean().optional(),
15 | /** Publish date (required when not draft) */
16 | pubDate: z.coerce.date().optional(),
17 | })
18 | .refine(
19 | (data) => {
20 | // If it is a draft, then pubDate is not required; otherwise, it is mandatory.
21 | if (data.draft === true) {
22 | return true;
23 | }
24 | return data.pubDate !== undefined;
25 | },
26 | {
27 | message: 'When draft is false, publicDate is required',
28 | path: ['publicDate'],
29 | },
30 | ),
31 | });
32 |
33 | export const collections = { post: postCollection };
34 |
--------------------------------------------------------------------------------
/src/content/post/40-questions.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 40 questions
3 | description: This repo maintains revisons and translations to the list of 40 questions I ask myself each year and each decade.
4 | tags:
5 | - Life
6 | - Thinking
7 | - Writing
8 | pubDate: 2022-01-14
9 | ---
10 |
11 | ## About
12 |
13 | This repo maintains revisions and translations to the list of 40 questions I ask myself each year and each decade.
14 |
15 | See related blog posts:
16 |
17 | - [40 questions to ask yourself every year](http://stephanango.com/40-questions) (October 2016)
18 | - [40 questions to ask yourself every decade](http://stephanango.com/40-questions-decade) (January 2022)
19 |
20 | ## Translations
21 |
22 | If you'd like to help translate the questions, you can submit your translation via pull request. Place the translated files into a folder named using the [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) two letter language code. See the [/translations](/translations) folder.
23 |
24 | | ISO 639-1 | Language | Year | Decade |
25 | | :-------- | :-------------------- | ------------------------------------- | --------------------------------------- |
26 | | en | English (original) | [link](year.md) | [link](decade.md) |
27 | | ar | Arabic | [link](/translations/ar/year.md) | [link](/translations/ar/decade.md) |
28 | | bg | Bulgarian | [link](/translations/bg/year.md) | [link](/translations/bg/decade.md) |
29 | | ca | Catalan | [link](/translations/ca/year.md) | [link](/translations/ca/decade.md) |
30 | | da | Danish | [link](/translations/da/year.md) | [link](/translations/da/decade.md) |
31 | | de | German | [link](/translations/de/year.md) | [link](/translations/de/decade.md) |
32 | | el | Greek | [link](/translations/el/year.md) | [link](/translations/el/decade.md) |
33 | | es | Spanish | [link](/translations/es/year.md) | [link](/translations/es/decade.md) |
34 | | fa | Persian | [link](/translations/fa/year.md) | [link](/translations/fa/decade.md) |
35 | | fi | Finnish | [link](/translations/fi/year.md) | [link](/translations/fi/decade.md) |
36 | | fr | French | [link](/translations/fr/year.md) | [link](/translations/fr/decade.md) |
37 | | hi | Hindi | [link](/translations/hi/year.md) | [link](/translations/hi/decade.md) |
38 | | hu | Hungarian (Magyar) | [link](/translations/hu/year.md) | [link](/translations/hu/decade.md) |
39 | | id | Indonesian | [link](/translations/id/year.md) | [link](/translations/id/decade.md) |
40 | | dv | Dhivehi | [link](/translations/dv/year.md) | [link](/translations/dv/decade.md) |
41 | | it | Italian | [link](/translations/it/year.md) | [link](/translations/it/decade.md) |
42 | | ja | Japanese | [link](/translations/ja/year.md) | [link](/translations/ja/decade.md) |
43 | | ko | Korean | [link](/translations/ko/year.md) | [link](/translations/ko/decade.md) |
44 | | lt | Lithuanian | [link](/translations/lt/year.md) | |
45 | | lv | Latvian | [link](/translations/lv/year.md) | [link](/translations/lv/decade.md) |
46 | | ml | Malayalam | [link](/translations/ml/year.md) | [link](/translations/ml/decade.md) |
47 | | nl | Dutch | [link](/translations/nl/year.md) | [link](/translations/nl/decade.md) |
48 | | no | Norwegian | [link](/translations/no/year.md) | [link](/translations/no/decade.md) |
49 | | pl | Polish | [link](/translations/pl/year.md) | [link](/translations/pl/decade.md) |
50 | | pt | Portuguese | [link](/translations/pt/year.md) | [link](/translations/pt/decade.md) |
51 | | ru | Russian | [link](/translations/ru/year.md) | [link](/translations/ru/decade.md) |
52 | | sv | Swedish | [link](/translations/sv/year.md) | [link](/translations/sv/decade.md) |
53 | | ta | Tamil | [link](/translations/ta/year.md) | [link](/translations/ta/decade.md) |
54 | | th | Thai | [link](/translations/th/year.md) | |
55 | | tl | Filipino | [link](/translations/tl/year.md) | |
56 | | tr | Turkish | [link](/translations/tr/year.md) | [link](/translations/tr/decade.md) |
57 | | uk | Ukrainian | [link](/translations/uk/year.md) | [link](/translations/uk/decade.md) |
58 | | vi | Vietnamese | [link](/translations/vi/year.md) | [link](/translations/vi/decade.md) |
59 | | zh-hans | Chinese (simplified) | [link](/translations/zh-hans/year.md) | [link](/translations/zh-hans/decade.md) |
60 | | zh-hant | Chinese (traditional) | [link](/translations/zh-hant/year.md) | [link](/translations/zh-hant/decade.md) |
61 |
62 | ## 40 questions to ask yourself each year
63 |
64 | One of my rituals at the end of each year is asking myself these forty questions. It usually takes me about a week to work my way through all of them. I find it to be one of the most valuable exercises to reflect on what happened, good and bad, and how I hope the year ahead will shape up.
65 |
66 | What is more interesting than each individual answer are the trends that emerge after years of answering the same questions. I’ve shared this list with my family and closest friends, and always enjoy discussing answers as we reflect on the year.
67 |
68 | Feel free to add or remove questions, and [share your edits with me](https://twitter.com/kepano). This is first and foremost a personal exercise, so make it a tradition you can enjoy for years to come.
69 |
70 | ## 40 questions to ask yourself each decade
71 |
72 | Some time ago I had answered [Proust's famous questionnaire](https://en.wikipedia.org/wiki/Proust_Questionnaire), and thought I would try answering it again. While the yearly questions help me reflect on what happened, Proust's questions are more about personal philosophies and traits, and thus change less frequently over time.
73 |
74 | Going through my answers to the Proust questionnaire, I was inspired to work on a new questionnaire that I could use for the next few decades. I tried create a set of questions that I would enjoy reflecting on in ten years. This list combines questions from Proust's questionnaire, and others I've been collecting ad hoc.
75 |
76 | It will be ten years before I can tell you whether this worked well or not, but join me on this journey if you'd like! Again, consider editing this list with questions you would like to know your own answers to in ten years.
77 |
78 | :::info
79 | From [40-questions](https://github.com/kepano/40-questions)
80 | :::
--------------------------------------------------------------------------------
/src/content/post/about-slate-blog.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: About Slate Blog
3 | description: I use Obsidian to think, take notes, write essays, and publish this site. This is my bottom-up approach to note-taking and organizing things I am interested in. It embraces chaos and laziness to create emergent structure.
4 | tags:
5 | - Dev
6 | - Tailwind
7 | - Astro
8 | - Design
9 | pubDate: 2025-01-21
10 | ---
11 |
12 | ## Why We build it?
13 | We love writing and sharing, and we also appreciate great internet products. So we created this minimalist blogging product, focusing on content itself, providing a smooth and pure writing and reading experience, and built on the latest technology framework to make it faster and lighter.
14 |
15 | It also works seamlessly with [Obsidian](https://obsidian.md/), helping you turn your notes into published posts effortlessly.
16 |
17 | ## ✨ Features
18 |
19 | - Minimalist design theme
20 | - Mobile-first responsive layout
21 | - Light and dark mode support
22 | - Quick setup with zero configuration required
23 | - Draft mode with local preview and automatic production filtering
24 | - Built-in RSS feed with Follow authentication
25 | - Integrated Algolia search functionality
26 | - Comprehensive SEO optimization for better search rankings
27 |
28 | ## 🪜 Framework
29 |
30 | - Astro + React + Typescript
31 | - Tailwindcss + @radix-ui/colors
32 | - Updated to [Tailwind CSS v4.0](https://tailwindcss.com/blog/tailwindcss-v4) (Jan 10, 2025)
33 | - Docsearch
34 |
35 | ## 🔨 Usage
36 |
37 | ```bash
38 | # Start local server
39 | npm run dev
40 | # or
41 | yarn dev
42 | # or
43 | pnpm dev
44 |
45 | # Build
46 | npm run build
47 | # or
48 | yarn build
49 | # or
50 | pnpm build
51 | ```
52 |
53 | ## 🗂 Directory Structure
54 |
55 | ```
56 | - plugins/ # Custom plugins
57 | - src/
58 | ├── assets/ # Asset files
59 | ├── components/ # Components
60 | ├── content/ # Content collections
61 | ├── helpers/ # Business logic
62 | ├── pages/ # Pages
63 | └── typings/ # Common types
64 | ```
65 |
66 | > Articles are stored in the `src/content/post` directory, supporting markdown and mdx formats. The filename is the path name. For example, `src/content/post/my-first-post.md` => `https://your-blog.com/blog/my-first-post`.
67 |
68 | ## Configuration
69 |
70 | Theme configuration is done through `slate.config.ts` in the root directory.
71 |
72 | | Option | Description | Type | Default |
73 | | --- | --- | --- | --- |
74 | | site | Final deployment link | `string` | - |
75 | | title | Website title | `string` | - |
76 | | description | Website description | `string` | - |
77 | | lang | Language | `string` | `zh-CN` |
78 | | theme | Theme | `{ mode: 'auto' | 'light' | 'dark', enableUserChange: boolean }` | `{ mode: 'auto', enableUserChange: true }` |
79 | | avatar | Avatar | `string` | - |
80 | | sitemap | Website sitemap configuration | [SitemapOptions](https://docs.astro.build/en/guides/integrations-guide/sitemap/) | - |
81 | | readTime | Show reading time | `boolean` | `false` |
82 | | lastModified | Show last modified time | `boolean` | `false` |
83 | | algolia | Docsearch configuration | `{ appId: string, apiKey: string, indexName: string }` | - |
84 | | follow | Follow subscription authentication configuration | `{ feedId: string, userId: string }` | - |
85 | | footer | Website footer configuration | `{ copyright: string }` | - |
86 |
87 | ### Algolia Application
88 |
89 | 1. Deploy your site first
90 | 2. Apply for an `apiKey` at [algolia](https://docsearch.algolia.com/apply/)
91 | 3. After successful application, configure `algolia` in `slate.config.ts`
92 | 4. Redeploy
93 |
94 | ### Follow Subscription Authentication
95 |
96 | 1. Register a [follow](https://follow.is/) account
97 | 2. Deploy your site
98 | 3. Click the `+` button on `follow`, select `RSS` subscription, and enter the `rss` link (usually `[site]/rss.xml`, where `site` is the value of `site` in `slate.config.ts`)
99 | 4. Redeploy
100 |
101 | ## Article Frontmatter Description
102 |
103 | | Option | Description | Type | Required |
104 | | --- | --- | --- | --- |
105 | | title | Article title | `string` | Yes |
106 | | description | Article description | `string` | No |
107 | | tags | Article tags | `string[]` | No |
108 | | draft | Whether it's a draft. When not provided or `false`, `pubDate` must be provided; drafts are only visible in local preview | `boolean` | No |
109 | | pubDate | Article publication date | `date` | No, required when `draft` is `false` |
110 |
111 | **For more details, check the `src/content/config.ts` file**
112 |
113 | ### Example
114 |
115 | ```md
116 | ---
117 | title: 40 questions
118 | description: This repo maintains revisons and translations to the list of 40 questions I ask myself each year and each decade.
119 | tags:
120 | - Life
121 | - Thinking
122 | - Writing
123 | pubDate: 2025-01-06
124 | ---
125 | ```
126 |
127 | ## Markdown Syntax Support
128 |
129 | In addition to standard Markdown syntax, the following extended syntax is supported:
130 |
131 | ### Basic Syntax
132 | - Headers, lists, blockquotes, code blocks and other basic syntax
133 | - Tables
134 | - Links and images
135 | - **Bold**, *italic*, and ~strikethrough~ text
136 |
137 | ### Extended Syntax
138 | #### Container syntax
139 | Using `:::` markers
140 | ```md
141 | :::info
142 | This is an information prompt
143 | :::
144 | ```
145 | The result will be displayed as:
146 |
147 | :::info
148 | This is an information prompt
149 | :::
150 |
151 | #### LaTeX Mathematical Formulas
152 | - Inline formula: $E = mc^2$
153 | - Block formula: $$ E = mc^2 $$
154 |
155 | #### Support for image captions
156 | ```md
157 | 
158 | ```
159 | The result will be displayed as:
160 |
161 | 
162 |
163 | ## Updates
164 |
165 | ### Version 1.3.0
166 | - Support Social Links
167 | - Optimize RSS article detail generation.
168 | - Add a script to synchronize the latest slate-blog version
169 |
170 | ### Version 1.2.0
171 | - Support i18n (English and Chinese)
172 | - Fixed known issues
173 |
174 | ### Version 1.1.1
175 | - Fixed known issues
176 |
177 | ### Version 1.1.0
178 | - Upgraded to support [Tailwind CSS v4.0](https://tailwindcss.com/blog/tailwindcss-v4)
179 | - Added dark mode support
180 | - Fixed known issues
181 |
182 | :::info
183 | From [Slate Blog](https://github.com/SlateDesign/slate-blog)
184 | :::
185 |
--------------------------------------------------------------------------------
/src/content/post/life-is-short.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: LIFE IS SHORT
3 | description: Life is short, as everyone knows. When I was a kid I used to wonder about this. Is life actually short, or are we really complaining about its finiteness? Would we be just as likely to feel life was short if we lived 10 times as long?
4 | tags:
5 | - Thinking
6 | - Life
7 | pubDate: 2016-01-01
8 | ---
9 |
10 | Life is short, as everyone knows. When I was a kid I used to wonder about this. Is life actually short, or are we really complaining about its finiteness? Would we be just as likely to feel life was short if we lived 10 times as long?
11 |
12 | Since there didn't seem any way to answer this question, I stopped wondering about it. Then I had kids. That gave me a way to answer the question, and the answer is that life actually is short.
13 |
14 | Having kids showed me how to convert a continuous quantity, time, into discrete quantities. You only get 52 weekends with your 2 year old. If Christmas-as-magic lasts from say ages 3 to 10, you only get to watch your child experience it 8 times. And while it's impossible to say what is a lot or a little of a continuous quantity like time, 8 is not a lot of something. If you had a handful of 8 peanuts, or a shelf of 8 books to choose from, the quantity would definitely seem limited, no matter what your lifespan was.
15 |
16 | Ok, so life actually is short. Does it make any difference to know that?
17 |
18 | It has for me. It means arguments of the form "Life is too short for x" have great force. It's not just a figure of speech to say that life is too short for something. It's not just a synonym for annoying. If you find yourself thinking that life is too short for something, you should try to eliminate it if you can.
19 |
20 | When I ask myself what I've found life is too short for, the word that pops into my head is "bullshit." I realize that answer is somewhat tautological. It's almost the definition of bullshit that it's the stuff that life is too short for. And yet bullshit does have a distinctive character. There's something fake about it. It's the junk food of experience. [1]
21 |
22 | If you ask yourself what you spend your time on that's bullshit, you probably already know the answer. Unnecessary meetings, pointless disputes, bureaucracy, posturing, dealing with other people's mistakes, traffic jams, addictive but unrewarding pastimes.
23 |
24 | There are two ways this kind of thing gets into your life: it's either forced on you, or it tricks you. To some extent you have to put up with the bullshit forced on you by circumstances. You need to make money, and making money consists mostly of errands. Indeed, the law of supply and demand ensures that: the more rewarding some kind of work is, the cheaper people will do it. It may be that less bullshit is forced on you than you think, though. There has always been a stream of people who opt out of the default grind and go live somewhere where opportunities are fewer in the conventional sense, but life feels more authentic. This could become more common.
25 |
26 | You can do it on a smaller scale without moving. The amount of time you have to spend on bullshit varies between employers. Most large organizations (and many small ones) are steeped in it. But if you consciously prioritize bullshit avoidance over other factors like money and prestige, you can probably find employers that will waste less of your time.
27 |
28 | If you're a freelancer or a small company, you can do this at the level of individual customers. If you fire or avoid toxic customers, you can decrease the amount of bullshit in your life by more than you decrease your income.
29 |
30 | But while some amount of bullshit is inevitably forced on you, the bullshit that sneaks into your life by tricking you is no one's fault but your own. And yet the bullshit you choose may be harder to eliminate than the bullshit that's forced on you. Things that lure you into wasting your time have to be really good at tricking you. An example that will be familiar to a lot of people is arguing online. When someone contradicts you, they're in a sense attacking you. Sometimes pretty overtly. Your instinct when attacked is to defend yourself. But like a lot of instincts, this one wasn't designed for the world we now live in. Counterintuitive as it feels, it's better most of the time not to defend yourself. Otherwise these people are literally taking your life. [2]
31 |
32 | Arguing online is only incidentally addictive. There are more dangerous things than that. As I've written before, one byproduct of technical progress is that things we like tend to become more addictive. Which means we will increasingly have to make a conscious effort to avoid addictions — to stand outside ourselves and ask "is this how I want to be spending my time?"
33 |
34 | As well as avoiding bullshit, one should actively seek out things that matter. But different things matter to different people, and most have to learn what matters to them. A few are lucky and realize early on that they love math or taking care of animals or writing, and then figure out a way to spend a lot of time doing it. But most people start out with a life that's a mix of things that matter and things that don't, and only gradually learn to distinguish between them.
35 |
36 | For the young especially, much of this confusion is induced by the artificial situations they find themselves in. In middle school and high school, what the other kids think of you seems the most important thing in the world. But when you ask adults what they got wrong at that age, nearly all say they cared too much what other kids thought of them.
37 |
38 | One heuristic for distinguishing stuff that matters is to ask yourself whether you'll care about it in the future. Fake stuff that matters usually has a sharp peak of seeming to matter. That's how it tricks you. The area under the curve is small, but its shape jabs into your consciousness like a pin.
39 |
40 | The things that matter aren't necessarily the ones people would call "important." Having coffee with a friend matters. You won't feel later like that was a waste of time.
41 |
42 | One great thing about having small children is that they make you spend time on things that matter: them. They grab your sleeve as you're staring at your phone and say "will you play with me?" And odds are that is in fact the bullshit-minimizing option.
43 |
44 | If life is short, we should expect its shortness to take us by surprise. And that is just what tends to happen. You take things for granted, and then they're gone. You think you can always write that book, or climb that mountain, or whatever, and then you realize the window has closed. The saddest windows close when other people die. Their lives are short too. After my mother died, I wished I'd spent more time with her. I lived as if she'd always be there. And in her typical quiet way she encouraged that illusion. But an illusion it was. I think a lot of people make the same mistake I did.
45 |
46 | The usual way to avoid being taken by surprise by something is to be consciously aware of it. Back when life was more precarious, people used to be aware of death to a degree that would now seem a bit morbid. I'm not sure why, but it doesn't seem the right answer to be constantly reminding oneself of the grim reaper hovering at everyone's shoulder. Perhaps a better solution is to look at the problem from the other end. Cultivate a habit of impatience about the things you most want to do. Don't wait before climbing that mountain or writing that book or visiting your mother. You don't need to be constantly reminding yourself why you shouldn't wait. Just don't wait.
47 |
48 | I can think of two more things one does when one doesn't have much of something: try to get more of it, and savor what one has. Both make sense here.
49 |
50 | How you live affects how long you live. Most people could do better. Me among them.
51 |
52 | But you can probably get even more effect by paying closer attention to the time you have. It's easy to let the days rush by. The "flow" that imaginative people love so much has a darker cousin that prevents you from pausing to savor life amid the daily slurry of errands and alarms. One of the most striking things I've read was not in a book, but the title of one: James Salter's Burning the Days.
53 |
54 | It is possible to slow time somewhat. I've gotten better at it. Kids help. When you have small children, there are a lot of moments so perfect that you can't help noticing.
55 |
56 | It does help too to feel that you've squeezed everything out of some experience. The reason I'm sad about my mother is not just that I miss her but that I think of all the things we could have done that we didn't. My oldest son will be 7 soon. And while I miss the 3 year old version of him, I at least don't have any regrets over what might have been. We had the best time a daddy and a 3 year old ever had.
57 |
58 | Relentlessly prune bullshit, don't wait to do things that matter, and savor the time you have. That's what you do when life is short.
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | Notes
67 |
68 | [1] At first I didn't like it that the word that came to mind was one that had other meanings. But then I realized the other meanings are fairly closely related. Bullshit in the sense of things you waste your time on is a lot like intellectual bullshit.
69 |
70 | [2] I chose this example deliberately as a note to self. I get attacked a lot online. People tell the craziest lies about me. And I have so far done a pretty mediocre job of suppressing the natural human inclination to say "Hey, that's not true!"
71 |
72 | **Thanks** to Jessica Livingston and Geoff Ralston for reading drafts of this.
73 |
74 | - [Korean Translation](https://blog.naver.com/happy_alpaca/221346692172)
75 | - [Japanese Translation](https://note.com/tokyojack/n/ne4c25e990634)
76 | - [Chinese Translation](https://www.jianshu.com/p/682429f8ac3f)
77 |
78 |
79 | :::info
80 | From [LIFE IS SHORT](https://www.paulgraham.com/vb.html)
81 | :::
--------------------------------------------------------------------------------
/src/content/post/obsidian-vault-template.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Obsidian Vault Template
3 | description: I use Obsidian to think, take notes, write essays, and publish this site. This is my bottom-up approach to note-taking and organizing things I am interested in. It embraces chaos and laziness to create emergent structure.
4 | tags:
5 | - Obsidian
6 | - Writing
7 | - Dev
8 | pubDate: 2024-04-16
9 | ---
10 |
11 | I use [Obsidian](https://stephango.com/obsidian) to think, take notes, write essays, and publish this site. This is my bottom-up approach to note-taking and organizing things I am interested in. It embraces chaos and laziness to create emergent structure.
12 |
13 | In Obsidian, a “vault” is simply a folder of files. This is important because it adheres to my [File over app](https://stephango.com/file-over-app) philosophy. If you want to create digital artifacts that last, they must be files you can control, in formats that are easy to retrieve and read. Obsidian gives you that freedom.
14 |
15 | The following is in no way dogmatic, just one example of how you can use Obsidian. Take the parts you like.
16 |
17 | ## Get started
18 | 1. [Download the vault](https://github.com/kepano/kepano-obsidian/archive/refs/heads/main.zip) or clone it from [the Github repo](https://github.com/kepano/kepano-obsidian)
19 | 2. Unzip the `.zip` file to a folder of your choosing
20 | 3. In Obsidian open the folder as a vault
21 |
22 | ## Theme and related tools
23 |
24 | - My theme [Minimal](https://stephango.com/minimal)
25 | - My [web clipper](https://stephango.com/obsidian-web-clipper) to save articles and pages from the web
26 | - [Obsidian Sync](https://obsidian.md/sync) to sync notes between my desktop, phone and tablet
27 |
28 | ## Plugins
29 | Some of my templates depend on plugins:
30 |
31 | - [Dataview](https://github.com/blacksmithgu/obsidian-dataview) for overview notes
32 | - [Leaflet](https://github.com/javalent/obsidian-leaflet) for maps
33 |
34 | ## Folders
35 | I use very few folders. I avoid folders because many of my entries belong to more than one area of thought. My system is oriented towards speed and laziness. I don’t want the overhead of having to consider where something should go.
36 |
37 | My personal notes are in the root of my vault. These are my journal entries, essays, [evergreen](https://stephango.com/evergreen-notes) notes, and personal ideas. If a note is in the root I know it’s something I came up with. I do not use the file explorer much for navigation, instead I navigate mostly using the quick switcher or clicking links.
38 |
39 | If you want to use this vault as a starting point the Categories and Templates folders contain everything you need.
40 |
41 | The folders I use:
42 |
43 | - **Attachments** for images, audio, videos, PDFs, etc.
44 | - **Clippings** for articles and web pages captured with my web clipper written by other people.
45 | - **Daily** for my daily notes, all named YYYY-MM-DD.md.
46 | - **References** for anything that refers to something that exists outside of my vault, e.g. books, movies, places, people, podcasts, etc.
47 | - **Templates** for templates. In my real personal vault the “Templates” folder is nested under “Meta” which also contains my personal style guide and other random notes about the vault.
48 |
49 | The folders I don’t use, but have created here for the sake of clarity. The notes in these folders would be in the root of my personal vault:
50 | - **Categories** contains top-level overviews of notes per category (e.g. books, movies, podcasts, etc).
51 | - **Notes** contains example notes.
52 |
53 |
54 | ## Structure, categories, and tags
55 | My notes are primarily organized using the category property. Categories are always links which makes it easy to get back to my top-level overviews. Some other rules I follow in my vault:
56 |
57 | - Avoid splitting content into multiple vaults.
58 | - Avoid folders for organization.
59 | - Avoid non-standard Markdown.
60 | - Always pluralize categories and tags.
61 | - Use `YYYY-MM-DD` dates everywhere.
62 |
63 | Having a [consistent style](https://stephango.com/style) collapses hundreds of future decisions into one, and gives me focus. I always pluralize tags so I never have to wonder what to name new tags. Choose the rules that feel comfortable to you.
64 |
65 | ## Templates and properties
66 |
67 | Almost every note I create starts from a [template](https://github.com/kepano/kepano-obsidian/tree/main/Templates). I use templates heavily because they allow me to lazily add information that will help me find the note later. I have a template for every category with properties at the top, to capture data such as:
68 |
69 | - Dates — created, start, end, published
70 | - People — author, director, artist, cast, host, guests
71 | - Themes — grouping by genre, type, topic, related notes
72 | - Locations — neighborhood, city, coordinates
73 | - Ratings — more on this below
74 |
75 | A few rules I follow for properties:
76 | - Property names and values should aim to be reusable across categories. This allows me to find things across categories, e.g. genre is shared across all media types, which means I can see an archive of Sci-fi books, movies and shows in one place.
77 | - Templates should aim to be composable, e.g. Person and Author are two different templates that can be added to the same note.
78 | - Short property names are faster to type, e.g. start instead of startdate.
79 | - Default to list type properties instead of text if there is any chance it might contain more than one link or value in the future.
80 |
81 | The [.obsidian/types.json](https://github.com/kepano/kepano-obsidian/blob/main/.obsidian/types.json) file lists which properties are assigned to which types (i.e. date, number, text, etc).
82 |
83 | ## Rating system
84 | Anything with a rating uses an integer from 1 to 7:
85 |
86 | - 7 — Perfect, must try, life-changing, go out of your way to seek this out
87 | - 6 — Excellent, worth repeating
88 | - 5 — Good, don’t go out of your way, but enjoyable
89 | - 4 — Passable, works in a pinch
90 | - 3 — Bad, don’t do this if you can
91 | - 2 — Atrocious, actively avoid, repulsive
92 | - 1 — Evil, life-changing in a bad way
93 |
94 | Why this scale? I like rating out of 7 better than 4 or 5 because I need more granularity at the top, for the good experiences, and 10 is too granular.
95 |
96 | ## Publishing to the web
97 | This site is written, edited, and published directly from Obsidian. To do this, I break one of my rules listed above — I have a separate vault for my site. I use a static site generator called [Jekyll](https://jekyllrb.com/) to automatically compile my notes into a website and convert them from Markdown to HTML.
98 |
99 | My publishing flow is easy to use, but a bit technical to set up. This is because I like to have full control over every aspect of my site’s layout. If you don’t need full control you might consider [Obsidian Publish](https://obsidian.md/publish) which is more user-friendly, and what I use for my [Minimal documentation site](https://minimal.guide/publish/download).
100 |
101 | For this site, I push notes from Obsidian to a GitHub repo using the [Obsidian Git](https://obsidian.md/plugins?id=obsidian-git) plugin. The notes are then automatically compiled using [Jekyll ](https://jekyllrb.com/)with my web host [Netlify](https://www.netlify.com/). I also use my [Permalink Opener](https://stephango.com/permalink-opener) plugin to quickly open notes in the browser so I can compare the draft and live versions.
102 |
103 | The color palette is [Flexoki](https://stephango.com/flexoki), which I created for this site. My Jekyll template is not public, but you can get similar results from [this template](https://github.com/maximevaillancourt/digital-garden-jekyll-template) by Maxime Vaillancourt. There are also many alternatives to Jekyll you can use to compile your site such as [Quartz](https://quartz.jzhao.xyz/), [Astro](https://astro.build/), [Eleventy](https://www.11ty.dev/), and [Hugo](https://gohugo.io/).
104 |
105 | :::info
106 | From [Obsidian Vault Template](https://stephango.com/vault)
107 | :::
--------------------------------------------------------------------------------
/src/content/post/sonner-getting-started.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Building a toast component
3 | description: Earlier this year, I built a Toast library for React called Sonner. In this article, I'll show you some of the lessons I've learned and mistakes I made while building it.
4 | tags:
5 | - Dev
6 | - Web
7 | - Animation
8 | pubDate: 2023-09-05
9 | ---
10 | Earlier this year, I built a Toast library for React called [Sonner](https://sonner.emilkowal.ski/). In this article, I'll show you some of the lessons I've learned and mistakes I made while building it.
11 |
12 | ## Animations
13 |
14 | Initially, I used CSS keyframes for enter and exit animations, but they aren't interruptible. A CSS transition can be interrupted and smoothly transition to a new value, even before the first transition has finished. You can see the difference below.
15 |
16 | To transition the toast when it enters the screen, essentially to mimic the enter animation, we used `useEffect` to set the mounted state to true after the first render. This way, the toast is rendered with transform: `translateY(100%)` and then transitions to `transform: translateY(0)`. The style is based on a data attribute.
17 |
18 | ```tsx
19 | React.useEffect(() => { setMounted(true);}, []);
20 |
21 | //...
22 |
23 |
24 | ```
25 |
26 | ## Stacking toasts
27 |
28 | To create the stacking effect, we multiply the gap between toasts by the index of the toast to get the y position. It's worth noting that every toast has `position: absolute` to make stacking easier. We also scale them down by 0.05 * index to create the illusion of depth. Here's the simplified CSS for it:
29 |
30 | ```tsx
31 | [data-sonner-toast][data-expanded="false"][data-front="false"] {
32 | --scale: var(--toasts-before) * 0.05 + 1;
33 | --y: translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(calc(-1 * var(--scale))
34 | );
35 | }
36 | ```
37 |
38 | This works great until you have toasts with different heights, they won't stick out evenly. We fix it by simply making all the toasts the height of the toast in front when in stacked mode. Here's how the toasts would look with different heights:
39 |
40 | Add toast
41 |
42 |
43 | ## Swiping
44 |
45 | The toasts can be swiped down to dismiss. That's just a simple event listener on the toast which updates a variable responsible for the `translateY` value.
46 |
47 | ```tsx
48 | // This is a simplified version of the code
49 | const onMove = (event) => {
50 | const yPosition = event.clientY - pointerStartRef.current.y;
51 |
52 | toastRef.current?.style.setProperty("--swipe-amount", `${yPosition}px`);
53 | };
54 | ```
55 |
56 | The swipe is momentum-based, meaning you don't have to swipe until a specific threshold is met to remove the toast. If the swipe movement is fast enough, the toast will still be dismissed because the velocity is high enough.
57 |
58 | ```tsx
59 | const timeTaken = new Date().getTime() - dragStartTime.current.getTime();
60 | const velocity = Math.abs(swipeAmount) / timeTaken;
61 |
62 | // Remove if the threshold is met or velocity is high enough
63 | if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) {
64 | deleteToast();
65 | }
66 | ```
67 |
68 |
69 |
70 | ## Spatial consistency
71 |
72 | Initially, the toast was entering from the bottom, and you could swipe it to the right, as shown in [this tweet](https://twitter.com/emilkowalski_/status/1503372086038962178). However, that didn't feel natural since the toast didn't follow a symmetric path. If something enters from the bottom, it should also exit in the same direction. I learned this principle from the [Designing Fluid Interfaces](https://developer.apple.com/videos/play/wwdc2018/803/) talk from Apple. It's amazing and I highly recommend watching it.
73 |
74 | If you are interested in this type of stuff, then you might enjoy my course on animations, where I cover this and much more — [animations.dev](https://animations.dev/).
75 |
76 |
77 |
78 | ## Expanding toasts
79 |
80 |
81 | We calculate the expanded position of each toast by adding the heights of all preceding toasts and the gap between them. This value will become the new `translateY` when the user hovers over the toast area.
82 |
83 | ```tsx
84 | const toastsHeightBefore = React.useMemo(() => {
85 | return heights.reduce((prev, curr, reducerIndex) => {
86 | // Calculate offset up until current toast
87 | if (reducerIndex >= heightIndex) {
88 | return prev;
89 | }
90 |
91 | return prev + curr.height;
92 | }, 0);
93 | }, [heights, heightIndex]);
94 |
95 | // ...
96 |
97 | const offset = React.useMemo(
98 | () => heightIndex * GAP + toastsHeightBefore,
99 | [heightIndex, toastsHeightBefore]
100 | );
101 | ```
102 |
103 | ## State management
104 |
105 | To avoid using [context](https://react.dev/reference/react/createContext), we manage the state via the [Observer Pattern](https://javascriptpatterns.vercel.app/patterns/design-patterns/observer-pattern). We subscribe to the observable object in the `` component. Whenever the `toast()` function is called, the `` component (as the subscriber) is notified and updates its state. We can then render all the toasts using `Array.map()`.
106 |
107 | ```tsx
108 | function Toaster() {
109 | React.useEffect(() => {
110 | return ToastState.subscribe((toast) => {
111 | setToasts((toasts) => [...toasts, toast]);
112 | });
113 | }, []);
114 |
115 | // ...
116 |
117 | return (
118 |
119 | {toasts.map((toast, index) => (
120 |
121 | ))}
122 |
123 | );
124 | }
125 | ```
126 |
127 | To create a new toast, we simply import `toast` and call it. There's no need for hooks or context, just a straightforward function call.
128 |
129 | ```tsx
130 | import { toast } from "sonner";
131 |
132 | // ...
133 |
134 | toast("My toast");
135 | ```
136 |
137 |
138 | ## Hover state
139 |
140 | The hover state depends on whether we are hovering over one of the toasts. However, there are also gaps between the toasts. To address this, we add an `:after` pseudo-element to fill in these gaps, ensuring a consistent hover state. You will see these filled gaps depicted below.
141 |
142 | Add toast
143 |
144 | ## Pointer capture
145 |
146 | Once we start dragging, we set the toast to capture all future pointer events. This ensures that even if the mouse or our thumb moves outside the toast while dragging, the toast remains the target of the pointer events. As a result, dragging remains possible, even if we are outside of the toast, leading to a better user experience.
147 |
148 | ```tsx
149 | function onPointerDown(event) {
150 | event.target.setPointerCapture(event.pointerId);
151 | }
152 | ```
153 |
154 |
155 | :::info
156 | From [Building a toast component](https://emilkowal.ski/ui/building-a-toast-component)
157 | :::
--------------------------------------------------------------------------------
/src/content/post/why-astro.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Why Astro?
3 | description: Astro is the web framework for building content-driven websites like blogs, marketing, and e-commerce. Astro is best-known for pioneering a new frontend architecture to reduce JavaScript overhead and complexity compared to other frameworks. If you need a website that loads fast and has great SEO, then Astro is for you.
4 | tags:
5 | - Dev
6 | - Astro
7 | - Web
8 | pubDate: 2024-12-03
9 | ---
10 |
11 | Astro is the web framework for building content-driven websites like blogs, marketing, and e-commerce. Astro is best-known for pioneering a new [frontend architecture](https://docs.astro.build/en/concepts/islands/) to reduce JavaScript overhead and complexity compared to other frameworks. If you need a website that loads fast and has great SEO, then Astro is for you.
12 |
13 | ## Features
14 | Astro is an all-in-one web framework. It includes everything you need to create a website, built-in. There are also hundreds of different [integrations](https://astro.build/integrations/) and [API hooks](https://docs.astro.build/en/reference/integrations-reference/) available to customize a project to your exact use case and needs.
15 |
16 | Some highlights include:
17 |
18 | - [Islands](https://docs.astro.build/en/concepts/islands/): A component-based web architecture optimized for content-driven websites.
19 | - [UI-agnostic](https://docs.astro.build/en/guides/framework-components/): Supports React, Preact, Svelte, Vue, Solid, HTMX, web components, and more.
20 | - [Server-first](https://docs.astro.build/en/guides/on-demand-rendering/): Moves expensive rendering off of your visitors’ devices.
21 | - [Zero JS, by default](https://docs.astro.build/en/basics/astro-components/): Less client-side JavaScript to slow your site down.
22 | - [Content collections](https://docs.astro.build/en/guides/content-collections/): Organize, validate, and provide TypeScript type-safety for your Markdown content.
23 | - [Customizable](https://docs.astro.build/en/guides/integrations-guide/): Tailwind, MDX, and hundreds of integrations to choose from.
24 |
25 | ## Design Principles
26 |
27 | Here are five core design principles to help explain why we built Astro, the problems that it exists to solve, and why Astro may be the best choice for your project or team.
28 |
29 | Astro is…
30 |
31 | 1. [Content-driven](https://docs.astro.build/en/concepts/why-astro/#content-driven): Astro was designed to showcase your content.
32 | 2. [Server-first](https://docs.astro.build/en/concepts/why-astro/#server-first): Websites run faster when they render HTML on the server.
33 | 3. [Fast by default](https://docs.astro.build/en/concepts/why-astro/#fast-by-default): It should be impossible to build a slow website in Astro.
34 | 4. [Easy to use](https://docs.astro.build/en/concepts/why-astro/#easy-to-use): You don’t need to be an expert to build something with Astro.
35 | 5. [Developer-focused](https://docs.astro.build/en/concepts/why-astro/#developer-focused): You should have the resources you need to be successful.
36 |
37 | ### Content-driven
38 | Astro was designed for building content-rich websites. This includes marketing sites, publishing sites, documentation sites, blogs, portfolios, landing pages, community sites, and e-commerce sites. If you have content to show, it needs to reach your reader quickly.
39 |
40 | By contrast, most modern web frameworks were designed for building web applications. These frameworks excel at building more complex, application-like experiences in the browser: logged-in admin dashboards, inboxes, social networks, todo lists, and even native-like applications like [Figma](https://figma.com/) and [Ping](https://ping.gg/). However with that complexity, they can struggle to provide great performance when delivering your content.
41 |
42 | Astro’s focus on content from its beginnings as a static site builder have allowed Astro to **sensibly scale up to performant, powerful, dynamic web applications** that still respect your content and your audience. Astro’s unique focus on content lets Astro make tradeoffs and deliver unmatched performance features that wouldn’t make sense for more application-focused web frameworks to implement.
43 |
44 | ### Server-first
45 | Astro leverages server rendering over client-side rendering in the browser as much as possible. This is the same approach that traditional server-side frameworks -- PHP, WordPress, Laravel, Ruby on Rails, etc. -- have been using for decades. But you don’t need to learn a second server-side language to unlock it. With Astro, everything is still just HTML, CSS, and JavaScript (or TypeScript, if you prefer).
46 |
47 | This approach stands in contrast to other modern JavaScript web frameworks like Next.js, SvelteKit, Nuxt, Remix, and others. These frameworks were built for client-side rendering of your entire website and include server-side rendering mainly to address performance concerns. This approach has been dubbed the **Single-Page App (SPA)**, in contrast with Astro’s **Multi-Page App (MPA)** approach.
48 |
49 | The SPA model has its benefits. However, these come at the expense of additional complexity and performance tradeoffs. These tradeoffs harm page performance -- critical metrics like [Time to Interactive (TTI)](https://web.dev/interactive/) -- which doesn’t make much sense for content-focused websites where first-load performance is essential.
50 |
51 | Astro’s server-first approach allows you to opt in to client-side rendering only if, and exactly as, necessary. You can choose to add UI framework components that run on the client. You can take advantage of Astro’s view transitions router for finer control over select page transitions and animations. Astro’s server-first rendering, either pre-rendered or on-demand, provides performant defaults that you can enhance and extend.
52 |
53 | ### Fast by default
54 | Good performance is always important, but it is especially critical for websites whose success depends on displaying your content. It has been well-proven that poor performance loses you engagement, conversions, and money. For example:
55 |
56 | - Every 100ms faster → 1% more conversions ([Mobify](https://web.dev/why-speed-matters/), earning +$380,000/yr)
57 | - 50% faster → 12% more sales ([AutoAnything](https://www.digitalcommerce360.com/2010/08/19/web-accelerator-revs-conversion-and-sales-autoanything/))
58 | - 20% faster → 10% more conversions ([Furniture Village](https://www.thinkwithgoogle.com/intl/en-gb/marketing-strategies/app-and-mobile/furniture-village-and-greenlight-slash-page-load-times-boosting-user-experience/))
59 | - 40% faster → 15% more sign-ups ([Pinterest](https://medium.com/pinterest-engineering/driving-user-growth-with-performance-improvements-cfc50dafadd7))
60 | - 850ms faster → 7% more conversions ([COOK](https://web.dev/why-speed-matters/))
61 | - Every 1 second slower → 10% fewer users ([BBC](https://www.creativebloq.com/features/how-the-bbc-builds-websites-that-scale))
62 |
63 | In many web frameworks, it is easy to build a website that looks great during development only to load painfully slow once deployed. JavaScript is often the culprit, since many phones and lower-powered devices rarely match the speed of a developer’s laptop.
64 |
65 | Astro’s magic is in how it combines the two values explained above -- a content focus with a server-first architecture -- to make tradeoffs and deliver features that other frameworks cannot. The result is amazing web performance for every website, out of the box. Our goal: It should be nearly impossible to build a slow website with Astro.
66 |
67 | An Astro website can [load 40% faster with 90%](https://twitter.com/t3dotgg/status/1437195415439360003) less JavaScript than the same site built with the most popular React web framework. But don’t take our word for it: watch Astro’s performance leave Ryan Carniato (creator of Solid.js and Marko) [speechless](https://youtu.be/2ZEMb_H-LYE?t=8163).
68 |
69 | ### Easy to use
70 | **Astro’s goal is to be accessible to every web developer.** Astro was designed to feel familiar and approachable regardless of skill level or past experience with web development.
71 |
72 | The `.astro` UI language is a superset of HTML: any valid HTML is valid Astro templating syntax! So, if you can write HTML, you can write Astro components! But, it also combines some of our favorite features borrowed from other component languages like JSX expressions (React) and CSS scoping by default (Svelte and Vue). This closeness to HTML also makes it easier to use progressive enhancement and common accessibility patterns without any overhead.
73 |
74 | We then made sure that you could also use your favorite UI component languages that you already know, and even reuse components you might already have. React, Preact, Svelte, Vue, Solid, and others, including web components, are all supported for authoring UI components in an Astro project.
75 |
76 | Astro was designed to be less complex than other UI frameworks and languages. One big reason for this is that Astro was designed to render on the server, not in the browser. That means that you don’t need to worry about: hooks (React), stale closures (also React), refs (Vue), observables (Svelte), atoms, selectors, reactions, or derivations. There is no reactivity on the server, so all of that complexity melts away.
77 |
78 | One of our favorite sayings is: **opt in to complexity**. We designed Astro to remove as much “required complexity” as possible from the developer experience, especially as you onboard for the first time. You can build a “Hello World” example website in Astro with just HTML and CSS. Then, when you need to build something more powerful, you can incrementally reach for new features and APIs as you go.
79 |
80 | ### Developer-focused
81 | We strongly believe that Astro is only a successful project if people love using it. Astro has everything you need to support you as you build with Astro.
82 |
83 | Astro invests in developer tools like a great CLI experience from the moment you open your terminal, an official VS Code extension for syntax highlighting, TypeScript and Intellisense, and documentation actively maintained by hundreds of community contributors and available in 14 languages.
84 |
85 | Our welcoming, respectful, inclusive community on Discord is ready to provide support, motivation, and encouragement. Open a `#support` thread to get help with your project. Visit our dedicated `#showcase`channel for sharing your Astro sites, blog posts, videos, and even work-in-progress for safe feedback and constructive criticism. Participate in regular live events such as our weekly community call, “Talking and Doc’ing,” and API/bug bashes.
86 |
87 | As an open-source project, we welcome contributions of all types and sizes from community members of all experience levels. You are invited to join in roadmap discussions to shape the future of Astro, and we hope you’ll contribute fixes and features to the core codebase, compiler, docs, language tools, websites, and other projects.
88 |
89 | ::: info
90 | From [Why Astro?](https://docs.astro.build/en/concepts/why-astro/)
91 | :::
--------------------------------------------------------------------------------
/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 | declare var Heti: any;
6 |
--------------------------------------------------------------------------------
/src/helpers/config-helper.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @file: Configuration handler
3 | */
4 | import type { SlateConfig, ThemeOptions } from '@/typings/config';
5 |
6 | /** Default configuration */
7 | const defaultConfig: Partial = {
8 | lang: 'zh-CN',
9 | theme: {
10 | mode: 'auto',
11 | enableUserChange: true,
12 | },
13 | readTime: false,
14 | lastModified: false,
15 | };
16 |
17 | export function defineConfig(config: SlateConfig): SlateConfig {
18 | const mergedConfig: Partial = {};
19 |
20 | if (typeof config.theme === 'string') {
21 | mergedConfig.theme = {
22 | ...(defaultConfig.theme as ThemeOptions),
23 | mode: config.theme,
24 | };
25 | } else {
26 | mergedConfig.theme = {
27 | ...(defaultConfig.theme as ThemeOptions),
28 | ...config.theme,
29 | };
30 | }
31 |
32 | return Object.assign({}, defaultConfig, config, mergedConfig);
33 | }
34 |
--------------------------------------------------------------------------------
/src/helpers/utils.ts:
--------------------------------------------------------------------------------
1 | import slateConfig from '~@/slate.config';
2 | import type { ThemeMode } from '@/typings/config';
3 |
4 | /**
5 | * @description: Get full title
6 | * @param title
7 | */
8 | export function getFullTitle(title: string) {
9 | return `${title}${!!title && slateConfig.title ? ' | ' : ''}${slateConfig.title}`;
10 | }
11 |
12 | /**
13 | * @description: Set theme mode
14 | * @param mode
15 | */
16 | export function setThemeMode(mode: ThemeMode) {
17 | document.documentElement.className = mode;
18 | document.documentElement.dataset.theme = mode;
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/src/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import i18next from 'i18next'
2 | import zhCn from './lang/zh-cn';
3 | import enUS from './lang/en-us'
4 | import slateConfig from '~@/slate.config';
5 |
6 | await i18next.init({
7 | lng: slateConfig.lang,
8 | fallbackLng: 'es-US',
9 | resources: {
10 | 'zh-CN': {
11 | translation: zhCn
12 | },
13 | 'en-US': {
14 | translation: enUS
15 | }
16 | }
17 | })
18 |
19 | export default i18next;
--------------------------------------------------------------------------------
/src/i18n/lang/en-us.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | common: {
3 | /** all tags */
4 | all: 'All',
5 | },
6 | /** blog page */
7 | blog: {
8 | /** last modified */
9 | lastModified: 'Last edited',
10 | /** reading time */
11 | readingTime: '{{minutes}} Min Read'
12 | },
13 | /** 404 page */
14 | 404: {
15 | /** page text */
16 | pageText: 'Page Not Found',
17 | /** back button text */
18 | backBtnText: 'Back to Home'
19 | }
20 | }
--------------------------------------------------------------------------------
/src/i18n/lang/zh-cn.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | common: {
3 | /** all tags */
4 | all: '全部',
5 | },
6 | blog: {
7 | lastModified: '编辑于',
8 | readingTime: '{{minutes}} 分钟阅读'
9 | },
10 | 404: {
11 | pageText: '你访问的页面不存在',
12 | backBtnText: '返回首页'
13 | }
14 | }
--------------------------------------------------------------------------------
/src/pages/404.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import i18next from '@/i18n';
3 | import PageLayout from '@/components/layouts/PageLayout.astro';
4 | import Button from '@/components/button';
5 | ---
6 |
7 |
8 |