Преизграждане на nullchefo.com с Astro
Защо замених React SPA за острови, content collections и нула JavaScript по подразбиране — и как реално изглеждаше миграцията.
Предишната версия на този сайт беше Next.js приложение. Работеше, но доставяше цял React runtime, за да рендира нещо, което честно казано е документ: малко текст за мен, списък с работни позиции, няколко линка. Новата версия е изградена с Astro и резултатът е чист HTML и CSS с няколкостотин байта vanilla JS за мобилното меню и scroll анимациите.
Защо Astro
Три причини направиха решението лесно:
- Нула JS по подразбиране. Компонентите се рендират до HTML по време на билда. JavaScript съществува само там, където изрично го добавя.
- Content collections. Публикациите са MDX файлове, валидирани от схема по време на билда — правописна грешка в дата е грешка при билда, а не тих бъг в продукция.
- Всичко е TypeScript. Файловете с данни, помощните функции и шаблоните споделят една типова система. Данните от CV-то ми са типизиран обект, използван и от английските, и от българските страници.
Съдържанието като файлове
Всяка публикация в този сайт живее в папка, кръстена на езика ѝ:
src/content/blog/
├── en/
│ └── rebuilding-nullchefo-with-astro.mdx
└── bg/
└── rebuilding-nullchefo-with-astro.mdx
Името на папката е локалът. Добавянето на немска версия на тази публикация би означавало създаване на de/rebuilding-nullchefo-with-astro.mdx — нищо друго. Схемата на колекцията държи всички честни:
const blog = defineCollection({
loader: glob({ pattern: '**/*.mdx', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
Тъй като преводите споделят едно име на файл, страницата на публикацията може сама да открие близнаците си и да направи линк към тях — редът “налична и на” по-горе се генерира от файловата система, а не се поддържа на ръка.
Какво запазих от стария сайт
Данните. Старото портфолио държеше CV съдържанието в TypeScript файлове, които се оказаха най-устойчивата част от кодовата база. Преместиха се почти без промяна, само с по-строги типове и ISO дати, така че всеки локал да форматира периодите по своя начин:
const monthYear = new Intl.DateTimeFormat(localeTag, {
month: 'short',
year: 'numeric',
});
Пренаписванията рядко се изплащат заради framework-а. Изплащат се, когато те принудят да отделиш това, което се променя често (съдържанието), от това, което не се променя (рендирането).
Числата
| Метрика | Next.js SPA | Astro |
|---|---|---|
| Доставен JS (начало) | ~140 KB | < 2 KB |
| Lighthouse perf | 80-и | 100 |
| Изход от билда | сървър | статичен |
Статичният изход означава, че целият сайт се деплойва на всеки хост, който може да сервира файлове — без Node сървър, без cold starts, без нищо за патчване в 2 през нощта.