From e7583875584a9634afd7a583fc63eecdd0ae7bcf Mon Sep 17 00:00:00 2001 From: Oleksandr <82455382+OleksandrMrn@users.noreply.github.com> Date: Sat, 18 Jul 2026 13:11:09 +0300 Subject: [PATCH 1/3] fix(homepage): guard blog category load (#452) - prevent optional blog category navigation data from crashing the localized homepage layout when database access fails - normalize the homepage site URL before building canonical and alternate metadata links --- frontend/app/[locale]/layout.tsx | 26 ++++++++++++++++++++++++-- frontend/app/[locale]/page.tsx | 4 +++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/frontend/app/[locale]/layout.tsx b/frontend/app/[locale]/layout.tsx index fbd4dbae..0b400830 100644 --- a/frontend/app/[locale]/layout.tsx +++ b/frontend/app/[locale]/layout.tsx @@ -10,11 +10,33 @@ import { CookieBanner } from '@/components/shared/CookieBanner'; import Footer from '@/components/shared/Footer'; import { ScrollWatcher } from '@/components/shared/ScrollWatcher'; import { ThemeProvider } from '@/components/theme/ThemeProvider'; -import { getCachedBlogCategories } from '@/db/queries/blog/blog-categories'; import { AuthProvider } from '@/hooks/useAuth'; import { locales } from '@/i18n/config'; import { readServerEnv } from '@/lib/env/server-env'; +type BlogCategory = { + id: string; + slug: string; + title: string; +}; + +async function getLayoutBlogCategories(locale: string): Promise { + try { + const { getCachedBlogCategories } = await import( + '@/db/queries/blog/blog-categories' + ); + + return await getCachedBlogCategories(locale); + } catch (error) { + console.error( + `[layout] failed to load blog categories for locale "${locale}"`, + error + ); + + return []; + } +} + export default async function LocaleLayout({ children, params, @@ -28,7 +50,7 @@ export default async function LocaleLayout({ const [messages, blogCategories] = await Promise.all([ getMessages({ locale }), - getCachedBlogCategories(locale), + getLayoutBlogCategories(locale), ]); const enableAdmin = diff --git a/frontend/app/[locale]/page.tsx b/frontend/app/[locale]/page.tsx index 6c9abbfa..99624b80 100644 --- a/frontend/app/[locale]/page.tsx +++ b/frontend/app/[locale]/page.tsx @@ -12,7 +12,9 @@ export async function generateMetadata({ }) { const { locale } = await params; const t = await getTranslations({ locale, namespace: 'homepage' }); - const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://devlovers.net'; + const siteUrl = ( + process.env.NEXT_PUBLIC_SITE_URL ?? 'https://devlovers.net' + ).replace(/\/$/, ''); const canonicalUrl = locale === 'en' ? `${siteUrl}/en` : `${siteUrl}/${locale}`; const localeMap: Record = { From 3ef98871957bf1c9ac8a858190b21c6357f56223 Mon Sep 17 00:00:00 2001 From: Viktor Svertoka Date: Sat, 18 Jul 2026 13:41:53 +0300 Subject: [PATCH 2/3] feat(categories): add Go, GraphQL, NestJS, and NuxtJS icons and styles (#454) --- frontend/data/categoryRegistry.ts | 36 +++++++++++++++++++++++++++++++ frontend/data/categoryStyles.ts | 10 ++++++--- frontend/public/icons/go.svg | 1 + frontend/public/icons/graphql.svg | 1 + frontend/public/icons/nestjs.svg | 1 + frontend/public/icons/nuxtjs.svg | 1 + 6 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 frontend/public/icons/go.svg create mode 100644 frontend/public/icons/graphql.svg create mode 100644 frontend/public/icons/nestjs.svg create mode 100644 frontend/public/icons/nuxtjs.svg diff --git a/frontend/data/categoryRegistry.ts b/frontend/data/categoryRegistry.ts index ccee5377..3d0bc586 100644 --- a/frontend/data/categoryRegistry.ts +++ b/frontend/data/categoryRegistry.ts @@ -309,6 +309,42 @@ export const categoryRegistry = [ 'group-hover:border-[#512BD4]/50 group-hover:bg-[#512BD4]/10 data-[state=active]:border-[#512BD4]/50 data-[state=active]:bg-[#512BD4]/10', 'bg-[#512BD4]' ), + createRegistryItem( + 'go', + 'Go', + 30, + '/icons/go.svg', + '#00ADD8', + 'group-hover:border-[#00ADD8]/50 group-hover:bg-[#00ADD8]/10 data-[state=active]:border-[#00ADD8]/50 data-[state=active]:bg-[#00ADD8]/10', + 'bg-[#00ADD8]' + ), + createRegistryItem( + 'graphql', + 'GraphQL', + 31, + '/icons/graphql.svg', + '#E434AA', + 'group-hover:border-[#E434AA]/50 group-hover:bg-[#E434AA]/10 data-[state=active]:border-[#E434AA]/50 data-[state=active]:bg-[#E434AA]/10', + 'bg-[#E434AA]' + ), + createRegistryItem( + 'nestjs', + 'NestJS', + 32, + '/icons/nestjs.svg', + '#DF234F', + 'group-hover:border-[#DF234F]/50 group-hover:bg-[#DF234F]/10 data-[state=active]:border-[#DF234F]/50 data-[state=active]:bg-[#DF234F]/10', + 'bg-[#DF234F]' + ), + createRegistryItem( + 'nuxtjs', + 'NuxtJS', + 33, + '/icons/nuxtjs.svg', + '#00C58E', + 'group-hover:border-[#00C58E]/50 group-hover:bg-[#00C58E]/10 data-[state=active]:border-[#00C58E]/50 data-[state=active]:bg-[#00C58E]/10', + 'bg-[#00C58E]' + ), ] as const satisfies readonly CategoryRegistryItem[]; export type CategoryRegistryEntry = (typeof categoryRegistry)[number]; diff --git a/frontend/data/categoryStyles.ts b/frontend/data/categoryStyles.ts index 0aea634f..d3ad5487 100644 --- a/frontend/data/categoryStyles.ts +++ b/frontend/data/categoryStyles.ts @@ -1,5 +1,9 @@ -import type { CategorySlug } from '@/components/q&a/types'; -import { categoryRegistry } from '@/data/categoryRegistry'; +import { + categoryRegistry, + type CategoryRegistryEntry, +} from '@/data/categoryRegistry'; + +type CategorySlug = CategoryRegistryEntry['slug']; export type CategoryTabStyle = { icon: string; @@ -28,7 +32,7 @@ export const categoryTabStyles = Object.fromEntries( iconClassName: item.iconClassName, } satisfies CategoryTabStyle, ]) -) as Partial>; +) as Record; export function getCategoryTabStyle(slug: string): CategoryTabStyle { return ( diff --git a/frontend/public/icons/go.svg b/frontend/public/icons/go.svg new file mode 100644 index 00000000..43377804 --- /dev/null +++ b/frontend/public/icons/go.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/icons/graphql.svg b/frontend/public/icons/graphql.svg new file mode 100644 index 00000000..3265b718 --- /dev/null +++ b/frontend/public/icons/graphql.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons/nestjs.svg b/frontend/public/icons/nestjs.svg new file mode 100644 index 00000000..4bec846a --- /dev/null +++ b/frontend/public/icons/nestjs.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/icons/nuxtjs.svg b/frontend/public/icons/nuxtjs.svg new file mode 100644 index 00000000..7f20b550 --- /dev/null +++ b/frontend/public/icons/nuxtjs.svg @@ -0,0 +1 @@ + From f8b1501f964e12ad3967dd16728c51748f641b48 Mon Sep 17 00:00:00 2001 From: Viktor Svertoka Date: Sat, 18 Jul 2026 14:16:21 +0300 Subject: [PATCH 3/3] chore(release): bump frontend to v1.0.13 and update changelog (#456) --- CHANGELOG.md | 23 +++++++++++++++++++++++ frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1a68603..827bdff3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1056,3 +1056,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/). - Preserved literal slug/title types in `categoryRegistry` factory to avoid widening to `string` - UI polish: - Brightened Django category accent color for better readability on dark theme + +## [1.0.13] - 2026-07-18 + +### Added + +- New category visual support: + - Added Go, GraphQL, NestJS, and NuxtJS to the centralized category registry + - Added SVG icons and category-specific accent styles for all four categories + +### Changed + +- Category styling architecture: + - Derived category slug typing directly from `categoryRegistry` + - Made generated category styles exhaustive for every registered category +- Layout runtime resilience: + - Changed blog category loading to a guarded dynamic query with an empty-state fallback +- Homepage SEO: + - Normalized the configured site URL before generating localized canonical URLs + +### Fixed + +- Prevented locale layout rendering from failing when blog category loading throws at runtime +- Prevented duplicate slashes in homepage canonical URLs when `NEXT_PUBLIC_SITE_URL` ends with `/` diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6592b923..a75383c0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "frontend", - "version": "1.0.12", + "version": "1.0.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frontend", - "version": "1.0.12", + "version": "1.0.13", "dependencies": { "@neondatabase/serverless": "^1.0.2", "@phosphor-icons/react": "^2.1.10", diff --git a/frontend/package.json b/frontend/package.json index 7775beb3..75235ce0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "1.0.12", + "version": "1.0.13", "private": true, "scripts": { "dev": "next dev",