fix/parameters-for-translations #15

Merged
raresj merged 4 commits from fix/parameters-for-translations into staging 2025-12-04 14:22:29 +00:00
41 changed files with 439 additions and 280 deletions

46
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,46 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: Full Stack",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev",
"serverReadyAction": {
"pattern": "started server on .+, url: (https?://.+)",
"uriFormat": "%s",
"action": "debugWithChrome"
},
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
{
"name": "Next.js: Frontend",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3030",
"webRoot": "${workspaceFolder}",
"sourceMapPathOverrides": {
"webpack://_N_E/*": "${webRoot}/*"
}
},
{
"name": "Next.js: Backend",
"type": "node",
"request": "attach",
"port": 9229,
"restart": true,
"sourceMaps": true,
"sourceMapPathOverrides": {
"webpack:///*": "${workspaceFolder}/*"
}
}
],
"compounds": [
{
"name": "Next.js: Debug Full Stack",
"configurations": ["Next.js: Backend", "Next.js: Frontend"],
"stopAll": true
}
]
}

View File

@@ -1,6 +1,6 @@
import { Metadata } from 'next'
import { Navbar } from '@/components/blog/navbar'
import {setRequestLocale} from 'next-intl/server'
import { setRequestLocale, getTranslations } from 'next-intl/server'
export const metadata: Metadata = {
title: 'About',
@@ -8,12 +8,13 @@ export const metadata: Metadata = {
}
type Props = {
params: Promise<{locale: string}>
params: Promise<{ locale: string }>
}
export default async function AboutPage({params}: Props) {
const {locale} = await params
export default async function AboutPage({ params }: Props) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('About')
return (
<>
<Navbar />
@@ -22,10 +23,10 @@ export default async function AboutPage({params}: Props) {
{/* Classification Header */}
<div className="border-2 border-[rgb(var(--border-primary))] p-8 mb-10">
<p className="text-[rgb(var(--text-muted))] font-mono text-xs uppercase tracking-widest mb-4">
&gt;&gt; _DOC://PUBLIC_ACCESS
{t('classificationHeader')}
</p>
<h1 className="text-4xl md:text-5xl font-mono font-bold uppercase text-[rgb(var(--text-primary))] tracking-tight">
ABOUT ME_
{t('mainTitle')}
</h1>
</div>
@@ -35,12 +36,10 @@ export default async function AboutPage({params}: Props) {
<section className="border-2 border-[rgb(var(--border-primary))] p-8">
<div className="border-l-4 border-[var(--neon-cyan)] pl-6">
<p className="font-mono text-base text-[rgb(var(--text-primary))] leading-relaxed mb-4">
Welcome to my corner of the internet! This is where I share my thoughts, opinions,
and experiences - from tech adventures to life as a family man. Yes, I love
technology, but there&apos;s so much more to life than just code and servers.
{t('introParagraph1')}
</p>
<p className="font-mono text-sm text-[rgb(var(--text-muted))] uppercase tracking-wider">
STATUS: ACTIVE // ROLE: DAD + DEV + LIFE ENTHUSIAST
{t('introLabel')}
</p>
</div>
</section>
@@ -48,46 +47,39 @@ export default async function AboutPage({params}: Props) {
{/* Life & Values Section */}
<section className="border-2 border-[rgb(var(--border-primary))] p-8">
<h2 className="text-2xl font-mono font-bold uppercase text-[rgb(var(--text-primary))] mb-6 pb-3 border-b-2 border-[rgb(var(--border-primary))]">
&gt; LIFE & VALUES
{t('lifeValuesTitle')}
</h2>
<div className="space-y-4">
<div className="border-l-4 border-[var(--neon-pink)] pl-6">
<h3 className="font-mono text-sm font-bold text-[var(--neon-pink)] uppercase mb-2">
[FAMILY FIRST]
{t('familyFirstTitle')}
</h3>
<p className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
Being a dad to an amazing toddler is my most important role. Family time is
sacred - whether it&apos;s building block towers, exploring parks, or just
enjoying the chaos of everyday life together. Tech can wait; these moments
can&apos;t.
{t('familyFirstText')}
</p>
</div>
<div className="border-l-4 border-[var(--neon-cyan)] pl-6">
<h3 className="font-mono text-sm font-bold text-[var(--neon-cyan)] uppercase mb-2">
[ACTIVE LIFESTYLE]
{t('activeLifestyleTitle')}
</h3>
<p className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
I believe in keeping the body active. Whether it&apos;s hitting the gym, playing
sports, or just staying on the move - physical activity keeps me sharp,
balanced, and ready for whatever life throws my way.
{t('activeLifestyleText')}
</p>
</div>
<div className="border-l-4 border-[var(--neon-green)] pl-6">
<h3 className="font-mono text-sm font-bold text-[var(--neon-green)] uppercase mb-2">
[ENJOYING THE SIMPLE THINGS]
{t('simpleThingsTitle')}
</h3>
<p className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
Life&apos;s too short not to enjoy it. A good drink, a relaxing
evening after a long day, or just not doing anything a blowing some steam off.
{t('simpleThingsText')}
</p>
</div>
<div className="border-l-4 border-[var(--neon-orange)] pl-6">
<h3 className="font-mono text-sm font-bold text-[var(--neon-orange)] uppercase mb-2">
[TECH WITH PURPOSE]
{t('techPurposeTitle')}
</h3>
<p className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
Yes, I love tech - self-hosting, privacy, tinkering with hardware. But it&apos;s
a tool, not a lifestyle. Tech should serve life, not the other way around.
{t('techPurposeText')}
</p>
</div>
</div>
@@ -96,51 +88,46 @@ export default async function AboutPage({params}: Props) {
{/* Content Section */}
<section className="border-2 border-[rgb(var(--border-primary))] p-8">
<h2 className="text-2xl font-mono font-bold uppercase text-[rgb(var(--text-primary))] mb-6 pb-3 border-b-2 border-[rgb(var(--border-primary))]">
&gt; WHAT YOU&apos;LL FIND HERE
{t('contentTitle')}
</h2>
<p className="font-mono text-sm text-[rgb(var(--text-muted))] uppercase tracking-wider mb-6">
CONTENT SCOPE // EVERYTHING FROM TECH TO LIFE
{t('contentSubtitle')}
</p>
<ul className="space-y-3">
<li className="flex items-start gap-3">
<span className="text-[var(--neon-pink)] font-mono font-bold">&gt;</span>
<span className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
<strong>Thoughts & Opinions</strong> - My take on life, work, and everything in
between
<strong>{t('contentThoughts')}</strong> - {t('contentThoughtsDesc')}
</span>
</li>
<li className="flex items-start gap-3">
<span className="text-[var(--neon-pink)] font-mono font-bold">&gt;</span>
<span className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
<strong>Life & Family</strong> - Adventures in parenting, sports, and enjoying
the simple things
<strong>{t('contentLifeFamily')}</strong> - {t('contentLifeFamilyDesc')}
</span>
</li>
<li className="flex items-start gap-3">
<span className="text-[var(--neon-cyan)] font-mono font-bold">&gt;</span>
<span className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
<strong>Tech Research</strong> - When I dive into interesting technologies and
experiments
<strong>{t('contentTechResearch')}</strong> - {t('contentTechResearchDesc')}
</span>
</li>
<li className="flex items-start gap-3">
<span className="text-[var(--neon-cyan)] font-mono font-bold">&gt;</span>
<span className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
<strong>System Administration</strong> - Self-hosting, infrastructure, and
DevOps adventures
<strong>{t('contentSysAdmin')}</strong> - {t('contentSysAdminDesc')}
</span>
</li>
<li className="flex items-start gap-3">
<span className="text-[var(--neon-cyan)] font-mono font-bold">&gt;</span>
<span className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
<strong>Development Insights</strong> - Lessons learned from building software
<strong>{t('contentDevelopment')}</strong> - {t('contentDevelopmentDesc')}
</span>
</li>
<li className="flex items-start gap-3">
<span className="text-[var(--neon-green)] font-mono font-bold">&gt;</span>
<span className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
<strong>Random Stuff</strong> - Because life doesn&apos;t fit into neat
categories!
<strong>{t('contentRandom')}</strong> - {t('contentRandomDesc')}
</span>
</li>
</ul>
@@ -149,40 +136,39 @@ export default async function AboutPage({params}: Props) {
{/* Areas of Focus Section */}
<section className="border-2 border-[rgb(var(--border-primary))] p-8">
<h2 className="text-2xl font-mono font-bold uppercase text-[rgb(var(--text-primary))] mb-6 pb-3 border-b-2 border-[rgb(var(--border-primary))]">
&gt; AREAS OF FOCUS
{t('focusTitle')}
</h2>
<div className="grid md:grid-cols-2 gap-4">
<div className="border border-[rgb(var(--border-primary))] p-4">
<h3 className="font-mono text-xs font-bold text-[var(--neon-pink)] uppercase mb-2">
[BEING A DAD]
{t('focusBeingDadTitle')}
</h3>
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
Playing with my boy, teaching moments, watching him grow, building memories
together
{t('focusBeingDadText')}
</p>
</div>
<div className="border border-[rgb(var(--border-primary))] p-4">
<h3 className="font-mono text-xs font-bold text-[var(--neon-cyan)] uppercase mb-2">
[STAYING ACTIVE]
{t('focusStayingActiveTitle')}
</h3>
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
Gym sessions, sports, keeping fit, maintaining energy for life&apos;s demands
{t('focusStayingActiveText')}
</p>
</div>
<div className="border border-[rgb(var(--border-primary))] p-4">
<h3 className="font-mono text-xs font-bold text-[var(--neon-green)] uppercase mb-2">
[TECHNOLOGY & SYSTEMS]
{t('focusTechnologyTitle')}
</h3>
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
Software development, infrastructure, DevOps, self-hosting adventures
{t('focusTechnologyText')}
</p>
</div>
<div className="border border-[rgb(var(--border-primary))] p-4">
<h3 className="font-mono text-xs font-bold text-[var(--neon-orange)] uppercase mb-2">
[LIFE BALANCE]
{t('focusLifeBalanceTitle')}
</h3>
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
Relaxing with good company, enjoying downtime, appreciating the simple moments
{t('focusLifeBalanceText')}
</p>
</div>
</div>
@@ -190,42 +176,42 @@ export default async function AboutPage({params}: Props) {
{/* Tech Stack Section */}
<section className="border-2 border-[rgb(var(--border-primary))] p-8">
<h2 className="text-2xl font-mono font-bold uppercase text-[rgb(var(--text-primary))] mb-6 pb-3 border-b-2 border-[rgb(var(--border-primary))]">
&gt; TECH STACK
{t('techStackTitle')}
</h2>
<p className="font-mono text-sm text-[rgb(var(--text-muted))] uppercase tracking-wider mb-6">
TOOLS I USE // WHEN NEEDED
{t('techStackSubtitle')}
</p>
<div className="grid md:grid-cols-2 gap-4">
<div className="border border-[rgb(var(--border-primary))] p-4">
<h3 className="font-mono text-xs font-bold text-[var(--neon-cyan)] uppercase mb-2">
[DEVELOPMENT]
{t('techStackDevelopmentTitle')}
</h3>
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
.NET, Golang, TypeScript, Next.js, React
{t('techStackDevelopmentText')}
</p>
</div>
<div className="border border-[rgb(var(--border-primary))] p-4">
<h3 className="font-mono text-xs font-bold text-[var(--neon-cyan)] uppercase mb-2">
[INFRASTRUCTURE]
{t('techStackInfrastructureTitle')}
</h3>
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
Windows Server, Linux, Docker, Hyper-V
{t('techStackInfrastructureText')}
</p>
</div>
<div className="border border-[rgb(var(--border-primary))] p-4">
<h3 className="font-mono text-xs font-bold text-[var(--neon-cyan)] uppercase mb-2">
[DESIGN]
{t('techStackDesignTitle')}
</h3>
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
Tailwind CSS, Markdown, Terminal aesthetics
{t('techStackDesignText')}
</p>
</div>
<div className="border border-[rgb(var(--border-primary))] p-4">
<h3 className="font-mono text-xs font-bold text-[var(--neon-cyan)] uppercase mb-2">
[SELF-HOSTING]
{t('techStackSelfHostingTitle')}
</h3>
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
Home lab, privacy-focused services, full control, Git server
{t('techStackSelfHostingText')}
</p>
</div>
</div>
@@ -233,7 +219,7 @@ export default async function AboutPage({params}: Props) {
{/* Contact Section */}
<section className="border-2 border-[rgb(var(--border-primary))] p-8">
<h2 className="text-2xl font-mono font-bold uppercase text-[rgb(var(--text-primary))] mb-6 pb-3 border-b-2 border-[rgb(var(--border-primary))]">
&gt; CONTACT
{t('contactTitle')}
</h2>
<div className="border-l-4 border-[var(--neon-pink)] pl-6">
{/* <p className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed mb-4">

View File

@@ -9,9 +9,7 @@ export default function NotFound() {
<div className="text-center">
<h1 className="text-6xl font-bold text-gray-300 dark:text-gray-700 mb-4">404</h1>
<h2 className="text-2xl font-semibold mb-4">{t('title')}</h2>
<p className="text-gray-600 dark:text-gray-400 mb-8">
{t('description')}
</p>
<p className="text-gray-600 dark:text-gray-400 mb-8">{t('description')}</p>
<div className="space-x-4">
<Link
href="/blog"

View File

@@ -2,13 +2,11 @@ import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { Link } from '@/src/i18n/navigation'
import { getAllPosts, getPostBySlug, getRelatedPosts } from '@/lib/markdown'
import { formatDate, formatRelativeDate } from '@/lib/utils'
import { formatDate } from '@/lib/utils'
import { TableOfContents } from '@/components/blog/table-of-contents'
import { ReadingProgress } from '@/components/blog/reading-progress'
import { StickyFooter } from '@/components/blog/sticky-footer'
import MarkdownRenderer from '@/components/blog/markdown-renderer'
import { setRequestLocale } from 'next-intl/server'
import { routing } from '@/src/i18n/routing'
export async function generateStaticParams() {
const locales = ['en', 'ro']

View File

@@ -7,7 +7,6 @@ import { BlogCard } from '@/components/blog/blog-card'
import { SearchBar } from '@/components/blog/search-bar'
import { SortDropdown } from '@/components/blog/sort-dropdown'
import { TagFilter } from '@/components/blog/tag-filter'
import { Navbar } from '@/components/blog/navbar'
interface BlogPageClientProps {
posts: Post[]
@@ -69,10 +68,10 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
{/* Header */}
<div className="border-l border-[var(--neon-cyan)] pl-6 mb-12">
<p className="font-mono text-xs text-[rgb(var(--text-muted))] uppercase tracking-widest mb-2">
{t("subtitle")}
{t('subtitle')}
</p>
<h1 className="text-4xl md:text-6xl font-mono font-bold text-[rgb(var(--text-primary))] uppercase tracking-tight">
&gt; {t("title")}_
&gt; {t('title')}_
</h1>
</div>
@@ -104,8 +103,7 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
{/* Results Count */}
<div className="mb-6">
<p className="font-mono text-sm text-[rgb(var(--text-muted))] uppercase">
{t("foundPosts", {count: filteredAndSortedPosts.length})}{' '}
{t('foundPosts', { count: filteredAndSortedPosts.length })}{' '}
</p>
</div>
@@ -128,7 +126,7 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
) : (
<div className="border border-[rgb(var(--border-primary))] bg-[rgb(var(--bg-secondary))] p-12 text-center">
<p className="font-mono text-lg text-[rgb(var(--text-muted))] uppercase">
{t("noPosts")}
{t('noPosts')}
</p>
</div>
)}
@@ -142,7 +140,7 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
disabled={currentPage === 1}
className="px-6 py-3 font-mono text-sm uppercase border border-[rgb(var(--border-primary))] text-[rgb(var(--text-primary))] disabled:opacity-30 disabled:cursor-not-allowed hover:border-[var(--neon-cyan)] hover:text-[var(--neon-cyan)] transition-colors cursor-pointer"
>
&lt; PREV
{t('prev')}
</button>
<div className="flex items-center gap-2">
{Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
@@ -164,7 +162,7 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
disabled={currentPage === totalPages}
className="px-6 py-3 font-mono text-sm uppercase border border-[rgb(var(--border-primary))] text-[rgb(var(--text-primary))] disabled:opacity-30 disabled:cursor-not-allowed hover:border-[var(--neon-cyan)] hover:text-[var(--neon-cyan)] transition-colors cursor-pointer"
>
NEXT &gt;
{t('next')}
</button>
</div>
</div>

View File

@@ -1,9 +1,11 @@
import { getAllPosts } from '@/lib/markdown'
import BlogPageClient from './blog-client'
import {setRequestLocale} from 'next-intl/server'
import { setRequestLocale } from 'next-intl/server'
export default async function BlogPage() {
const posts = await getAllPosts()
export default async function BlogPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
await setRequestLocale(locale)
const posts = await getAllPosts(locale)
const allTags = Array.from(new Set(posts.flatMap(post => post.frontmatter.tags))).sort()
return <BlogPageClient posts={posts} allTags={allTags} />

View File

@@ -1,24 +1,20 @@
import {notFound} from 'next/navigation'
import {setRequestLocale} from 'next-intl/server'
import {routing} from '@/src/i18n/routing'
import {ReactNode} from 'react'
import { notFound } from 'next/navigation'
import { setRequestLocale } from 'next-intl/server'
import { routing } from '@/src/i18n/routing'
import { ReactNode } from 'react'
type Props = {
children: ReactNode
breadcrumbs: ReactNode
params: Promise<{locale: string}>
params: Promise<{ locale: string }>
}
export function generateStaticParams() {
return routing.locales.map((locale) => ({locale}))
return routing.locales.map(locale => ({ locale }))
}
export default async function LocaleLayout({
children,
breadcrumbs,
params
}: Props) {
const {locale} = await params
export default async function LocaleLayout({ children, breadcrumbs, params }: Props) {
const { locale } = await params
if (!routing.locales.includes(locale as any)) {
notFound()

View File

@@ -1,11 +1,20 @@
import {Link} from '@/src/i18n/navigation'
import { Link } from '@/src/i18n/navigation'
import Image from 'next/image'
import { getAllPosts } from '@/lib/markdown'
import { formatDate } from '@/lib/utils'
import { ThemeToggle } from '@/components/theme-toggle'
import {setRequestLocale} from 'next-intl/server'
import { setRequestLocale, getTranslations } from 'next-intl/server'
type Props = {
params: Promise<{ locale: string }>
}
export default async function HomePage({ params }: Props) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('Home')
const tNav = await getTranslations('Navigation')
export default async function HomePage() {
const allPosts = await getAllPosts()
const featuredPosts = allPosts.slice(0, 6)
@@ -24,7 +33,7 @@ export default async function HomePage() {
<div className="flex items-center gap-3">
<Image src="/logo.png" alt="Logo" width={32} height={32} className="opacity-80" />
<span className="font-mono text-xs text-slate-500 uppercase tracking-widest">
TERMINAL:// V2.0
{t('terminalVersion')}
</span>
</div>
<div className="flex gap-4 items-center">
@@ -32,13 +41,13 @@ export default async function HomePage() {
href="/blog"
className="font-mono text-xs text-slate-600 dark:text-slate-400 uppercase tracking-wider hover:text-cyan-600 dark:hover:text-cyan-400"
>
[BLOG]
[{tNav('blog')}]
</Link>
<Link
href="/about"
className="font-mono text-xs text-slate-600 dark:text-slate-400 uppercase tracking-wider hover:text-cyan-600 dark:hover:text-cyan-400"
>
[ABOUT]
[{tNav('about')}]
</Link>
<ThemeToggle />
</div>
@@ -46,15 +55,13 @@ export default async function HomePage() {
<div className="border-l-4 border-cyan-700 dark:border-cyan-900 pl-6 mb-8">
<p className="font-mono text-xs text-slate-500 dark:text-slate-500 uppercase tracking-widest mb-2">
DOCUMENT LEVEL-1 //
{t('documentLevel')}
</p>
<h1 className="text-4xl md:text-6xl lg:text-7xl font-mono font-bold text-slate-900 dark:text-slate-100 uppercase tracking-tight mb-6">
BUILD. WRITE.
<br />
SHARE.
{t('heroTitle')}
</h1>
<p className="text-base md:text-lg lg:text-xl text-slate-700 dark:text-slate-400 font-mono leading-relaxed max-w-2xl">
&gt; Explore ideas
{t('heroSubtitle')}
</p>
</div>
@@ -63,13 +70,13 @@ export default async function HomePage() {
href="/blog"
className="px-6 md:px-8 py-3 md:py-4 bg-cyan-700 dark:bg-cyan-900 text-white dark:text-slate-100 border-2 border-cyan-600 dark:border-cyan-700 font-mono font-bold uppercase text-xs md:text-sm tracking-wider hover:bg-cyan-600 dark:hover:bg-cyan-800 hover:border-cyan-500 dark:hover:border-cyan-600 rounded-none transition-colors duration-200"
>
[CHECK POSTS]
{t('checkPostsButton')}
</Link>
<Link
href="/about"
className="px-6 md:px-8 py-3 md:py-4 bg-transparent text-slate-700 dark:text-slate-300 border-2 border-slate-400 dark:border-slate-700 font-mono font-bold uppercase text-xs md:text-sm tracking-wider hover:bg-slate-200 dark:hover:bg-slate-800 hover:border-slate-500 dark:hover:border-slate-600 rounded-none transition-colors duration-200"
>
[ABOUT ME]
{t('aboutMeButton')}
</Link>
</div>
</div>
@@ -81,10 +88,10 @@ export default async function HomePage() {
<div className="max-w-7xl mx-auto px-6">
<div className="border-l-4 border-emerald-700 dark:border-emerald-900 pl-6 mb-12">
<p className="font-mono text-xs text-slate-500 dark:text-slate-500 uppercase tracking-widest mb-2">
ARCHIVE ACCESS // RECENT ENTRIES
{t('recentEntriesLabel')}
</p>
<h2 className="text-3xl md:text-5xl font-mono font-bold text-slate-900 dark:text-slate-100 uppercase tracking-tight">
&gt; RECENT ENTRIES
{t('recentEntriesTitle')}
</h2>
</div>
@@ -134,7 +141,7 @@ export default async function HomePage() {
href={`/blog/${post.slug}`}
className="inline-flex items-center text-cyan-600 dark:text-cyan-400 font-mono text-xs font-bold uppercase tracking-wider hover:text-cyan-500 dark:hover:text-cyan-300 border-2 border-slate-400 dark:border-slate-700 px-4 py-2 hover:border-cyan-700 dark:hover:border-cyan-900 transition-colors duration-200"
>
[ACCESEAZĂ] &gt;&gt;
{t('accessButton')}
</Link>
</div>
</article>
@@ -147,14 +154,14 @@ export default async function HomePage() {
href="/blog"
className="inline-flex items-center px-8 py-4 bg-transparent text-slate-700 dark:text-slate-300 border-2 border-slate-400 dark:border-slate-700 font-mono font-bold uppercase text-sm tracking-wider hover:bg-slate-200 dark:hover:bg-slate-800 hover:border-slate-500 dark:hover:border-slate-600 transition-colors duration-200"
>
[SEE POSTS] &gt;&gt;
{t('seePostsButton')}
</Link>
)}
<Link
href="/tags"
className="inline-flex items-center px-8 py-4 bg-transparent text-slate-700 dark:text-slate-300 border-2 border-slate-400 dark:border-slate-700 font-mono font-bold uppercase text-sm tracking-wider hover:bg-slate-200 dark:hover:bg-slate-800 hover:border-slate-500 dark:hover:border-slate-600 transition-colors duration-200"
>
[SEE ALL TAGS] &gt;&gt;
{t('seeAllTagsButton')}
</Link>
</div>
</div>

View File

@@ -1,11 +1,9 @@
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import {Link} from '@/src/i18n/navigation'
import { Link } from '@/src/i18n/navigation'
import { getAllTags, getPostsByTag, getTagInfo, getRelatedTags } from '@/lib/tags'
import { TagList } from '@/components/blog/tag-list'
import { formatDate } from '@/lib/utils'
import {setRequestLocale} from 'next-intl/server'
import {routing} from '@/src/i18n/routing'
export async function generateStaticParams() {
const tags = await getAllTags()

View File

@@ -1,9 +1,9 @@
import { Metadata } from 'next'
import {Link} from '@/src/i18n/navigation'
import { Link } from '@/src/i18n/navigation'
import { getAllTags, getTagCloud } from '@/lib/tags'
import { TagCloud } from '@/components/blog/tag-cloud'
import { TagBadge } from '@/components/blog/tag-badge'
import {setRequestLocale} from 'next-intl/server'
import { setRequestLocale } from 'next-intl/server'
export const metadata: Metadata = {
title: 'Tag-uri',
@@ -11,11 +11,11 @@ export const metadata: Metadata = {
}
type Props = {
params: Promise<{locale: string}>
params: Promise<{ locale: string }>
}
export default async function TagsPage({params}: Props) {
const {locale} = await params
export default async function TagsPage({ params }: Props) {
const { locale } = await params
setRequestLocale(locale)
const allTags = await getAllTags()
const tagCloud = await getTagCloud()

View File

@@ -3,7 +3,7 @@ import { NextResponse } from 'next/server'
export async function GET() {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3030'
const posts = await getAllPosts("en", false)
const posts = await getAllPosts('en', false)
const rss = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">

View File

@@ -3,8 +3,8 @@ import { JetBrains_Mono } from 'next/font/google'
import './globals.css'
import { ThemeProvider } from '@/providers/providers'
import '@/lib/env-validation'
import {NextIntlClientProvider} from 'next-intl'
import {getMessages} from 'next-intl/server'
import { NextIntlClientProvider } from 'next-intl'
import { getMessages } from 'next-intl/server'
const jetbrainsMono = JetBrains_Mono({ subsets: ['latin'], variable: '--font-mono' })
@@ -30,11 +30,7 @@ export const metadata: Metadata = {
},
}
export default async function RootLayout({
children
}: {
children: React.ReactNode
}) {
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const messages = await getMessages()
return (

View File

@@ -8,9 +8,9 @@ export default function robots(): MetadataRoute.Robots {
userAgent: '*',
allow: '/',
disallow: [
'/api/', // Disallow API routes (if any)
'/_next/', // Disallow Next.js internals
'/admin/', // Disallow admin (if any)
'/api/', // Disallow API routes (if any)
'/_next/', // Disallow Next.js internals
'/admin/', // Disallow admin (if any)
],
},
sitemap: `${baseUrl}/sitemap.xml`,

View File

@@ -5,7 +5,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3030'
// Get all blog posts
const posts = await getAllPosts("en", false)
const posts = await getAllPosts('en', false)
// Generate sitemap entries for blog posts
const blogPosts: MetadataRoute.Sitemap = posts.map(post => ({

View File

@@ -62,9 +62,7 @@ export function OptimizedImage({
return (
<span className={`block my-8 ${className}`}>
{imageElement}
{caption && (
<span className="block mt-3 text-center text-sm text-zinc-400">{caption}</span>
)}
{caption && <span className="block mt-3 text-center text-sm text-zinc-400">{caption}</span>}
</span>
)
}

View File

@@ -40,7 +40,7 @@ export function BlogCard({ post, variant }: BlogCardProps) {
))}
</div>
<span className="inline-flex items-center font-mono text-xs uppercase text-cyan-400 hover:text-cyan-300 transition-colors">
&gt; {t('readingTime', {minutes: post.readingTime})}
&gt; {t('readingTime', { minutes: post.readingTime })}
</span>
</article>
</Link>
@@ -84,7 +84,7 @@ export function BlogCard({ post, variant }: BlogCardProps) {
))}
</div>
<span className="inline-flex items-center font-mono text-xs uppercase text-cyan-400 hover:text-cyan-300 transition-colors">
&gt; {t('readingTime', {minutes: post.readingTime})}
&gt; {t('readingTime', { minutes: post.readingTime })}
</span>
</div>
</div>
@@ -129,7 +129,7 @@ export function BlogCard({ post, variant }: BlogCardProps) {
))}
</div>
<span className="inline-flex items-center font-mono text-xs uppercase text-cyan-400 hover:text-cyan-300 transition-colors">
&gt; {t('readingTime', {minutes: post.readingTime})}
&gt; {t('readingTime', { minutes: post.readingTime })}
</span>
</div>
</article>

View File

@@ -6,10 +6,10 @@ interface CodeBlockProps {
code: string
language: string
filename?: string
showLineNumbers?: boolean
_showLineNumbers?: boolean
}
export function CodeBlock({ code, language, filename, showLineNumbers = true }: CodeBlockProps) {
export function CodeBlock({ code, language, filename, _showLineNumbers = true }: CodeBlockProps) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {

View File

@@ -15,7 +15,7 @@ interface MarkdownRendererProps {
}
export default function MarkdownRenderer({ content, className = '' }: MarkdownRendererProps) {
const locale = useLocale()
const _locale = useLocale()
return (
<div className={`prose prose-invert prose-zinc max-w-none ${className}`}>
<ReactMarkdown

View File

@@ -3,7 +3,7 @@ import { getPopularTags } from '@/lib/tags'
import { TagBadge } from './tag-badge'
export async function PopularTags({ limit = 5 }: { limit?: number }) {
const tags = await getPopularTags("en", limit)
const tags = await getPopularTags('en', limit)
if (tags.length === 0) return null

View File

@@ -28,7 +28,7 @@ export function TagCloud({ tags }: TagCloudProps) {
hover:text-cyan-400
transition-colors
`}
title={t('postsWithTag', {count: tag.count, tag: tag.name})}
title={t('postsWithTag', { count: tag.count, tag: tag.name })}
>
#{tag.name}
</Link>

View File

@@ -1,6 +1,6 @@
'use client'
import {Link} from '@/i18n/navigation'
import { Link } from '@/i18n/navigation'
import { usePathname } from 'next/navigation'
import { useLocale, useTranslations } from 'next-intl'
import { Fragment } from 'react'

View File

@@ -1,21 +1,21 @@
'use client';
'use client'
import {useLocale} from 'next-intl';
import {useRouter, usePathname} from '@/i18n/navigation';
import {routing} from '@/i18n/routing';
import {useState} from 'react';
import { useLocale } from 'next-intl'
import { useRouter, usePathname } from '@/i18n/navigation'
import { routing } from '@/i18n/routing'
import { useState } from 'react'
export default function LanguageSwitcher() {
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const [isOpen, setIsOpen] = useState(false);
const locale = useLocale()
const router = useRouter()
const pathname = usePathname()
const [isOpen, setIsOpen] = useState(false)
const handleLocaleChange = (newLocale: string) => {
router.replace(pathname, {locale: newLocale});
router.refresh();
setIsOpen(false);
};
router.replace(pathname, { locale: newLocale })
router.refresh()
setIsOpen(false)
}
return (
<div className="relative z-[100]">
@@ -36,9 +36,8 @@ export default function LanguageSwitcher() {
className={`
w-full text-left px-4 py-2 font-mono uppercase text-xs
border-b border-slate-700 last:border-b-0
${locale === loc
? 'bg-cyan-900 text-cyan-300'
: 'text-slate-400 hover:bg-slate-800'
${
locale === loc ? 'bg-cyan-900 text-cyan-300' : 'text-slate-400 hover:bg-slate-800'
}
`}
>
@@ -48,12 +47,7 @@ export default function LanguageSwitcher() {
</div>
)}
{isOpen && (
<div
className="fixed inset-0 z-40"
onClick={() => setIsOpen(false)}
/>
)}
{isOpen && <div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />}
</div>
);
)
}

View File

@@ -1,5 +1,5 @@
---
title: 'Why I created this page'
title: 'First post'
description: 'First post'
date: '2025-12-02'
author: 'Rares'
@@ -24,7 +24,7 @@ Well, yes, there are. But I believe that sharing some of my opinions and experie
## Why self-host?
![Selfhosting rig](./whythispage/selfhostedrig.gif?w=400 "My self-hosting setup | A look at the hardware running this blog")
![Selfhosting rig](./whythispage/selfhostedrig.gif?w=400 'My self-hosting setup | A look at the hardware running this blog')
Now, let's talk about why I chose to self-host this blog. In a nutshell, self-hosting gave me:

View File

@@ -1,6 +1,6 @@
---
title: 'Why I created this page'
description: 'First post'
title: 'Primul post'
description: 'Primul post'
date: '2025-12-02'
author: 'Rares'
category: 'Opinion'
@@ -23,7 +23,7 @@ Dacă te gândești de ce să mai creezi inca un blog cand sunt atea pe net, pai
## De ce selfhost?
![Selfhosting rig](./whythispage/selfhostedrig.gif?w=400 "Acesta este pc-ul | Hardware-ul pe care ruleaza cest webpage")
![Selfhosting rig](./whythispage/selfhostedrig.gif?w=400 'Acesta este pc-ul | Hardware-ul pe care ruleaza cest webpage')
Am inceput sa fac hosting acasa din cateva motive:

View File

@@ -5,6 +5,7 @@
This guide documents the configuration for build-time environment variables in the Next.js CI/CD pipeline.
**Problem Solved:** Next.js 16 requires `NEXT_PUBLIC_*` variables available during `npm run build` for:
- SEO metadata (`metadataBase`)
- Sitemap generation
- OpenGraph URLs
@@ -19,6 +20,7 @@ This guide documents the configuration for build-time environment variables in t
### 1. `.gitea/workflows/main.yml`
**Changes:**
- Added step to create `.env` from Gitea secrets (after checkout)
- Added cleanup step to remove `.env` after Docker push
@@ -55,6 +57,7 @@ This guide documents the configuration for build-time environment variables in t
### 2. `Dockerfile.nextjs`
**Changes:**
- Added `COPY .env* ./` in builder stage (after copying node_modules, before copying source code)
**Added Section:**
@@ -73,6 +76,7 @@ COPY .env* ./
### 3. `.dockerignore`
**Changes:**
- Modified to allow `.env` file (created by CI/CD) while excluding other `.env*` files
**Updated Section:**
@@ -85,6 +89,7 @@ COPY .env* ./
```
**Explanation:**
- `.env*` excludes all environment files
- `!.env` creates exception for main `.env` (from CI/CD)
- `.env.local`, `.env.development`, `.env.production.local` remain excluded
@@ -99,11 +104,12 @@ Navigate to: **Repository Settings → Secrets**
Add the following secret:
| Secret Name | Value | Type | Description |
|------------|-------|------|-------------|
| Secret Name | Value | Type | Description |
| ---------------------- | ------------------------ | ------------------ | ------------------- |
| `NEXT_PUBLIC_SITE_URL` | `https://yourdomain.com` | Secret or Variable | Production site URL |
**Notes:**
- Can be configured as **Secret** (masked in logs) or **Variable** (visible in logs)
- Recommended: Use **Variable** since it's a public URL
- For sensitive values (API keys), always use **Secret**
@@ -113,12 +119,14 @@ Add the following secret:
To add more build-time variables:
1. **Add to Gitea Secrets/Variables:**
```
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
NEXT_PUBLIC_API_URL=https://api.example.com
```
2. **Update workflow `.env` creation step:**
```yaml
cat > .env << EOF
NEXT_PUBLIC_SITE_URL=${{ secrets.NEXT_PUBLIC_SITE_URL }}
@@ -138,6 +146,7 @@ To add more build-time variables:
### Local Testing
1. **Create test `.env` file:**
```bash
cat > .env << EOF
NEXT_PUBLIC_SITE_URL=http://localhost:3030
@@ -147,11 +156,13 @@ To add more build-time variables:
```
2. **Build Docker image:**
```bash
docker build -t mypage:test -f Dockerfile.nextjs .
```
3. **Verify variable is embedded (should show "NOT FOUND" - correct behavior):**
```bash
docker run --rm mypage:test node -e "console.log(process.env.NEXT_PUBLIC_SITE_URL || 'NOT FOUND')"
```
@@ -161,6 +172,7 @@ To add more build-time variables:
**Why?** `NEXT_PUBLIC_*` variables are embedded in JavaScript bundle during build, NOT available as runtime environment variables.
4. **Test application starts:**
```bash
docker run --rm -p 3030:3030 mypage:test
```
@@ -214,10 +226,10 @@ To add more build-time variables:
### 🔒 Sensitive Data Guidelines
| Type | Use For | Access |
|------|---------|--------|
| `NEXT_PUBLIC_*` | Client-side config (URLs, feature flags) | Public (embedded in JS bundle) |
| `SECRET_*` | Server-side secrets (API keys, DB passwords) | Private (runtime only) |
| Type | Use For | Access |
| --------------- | -------------------------------------------- | ------------------------------ |
| `NEXT_PUBLIC_*` | Client-side config (URLs, feature flags) | Public (embedded in JS bundle) |
| `SECRET_*` | Server-side secrets (API keys, DB passwords) | Private (runtime only) |
---
@@ -226,10 +238,12 @@ To add more build-time variables:
### Issue: Variables not available during build
**Symptoms:**
- Next.js build errors about missing `NEXT_PUBLIC_SITE_URL`
- Metadata/sitemap generation fails
**Solution:**
- Verify `NEXT_PUBLIC_SITE_URL` secret exists in Gitea
- Check workflow logs for `.env` creation step
- Ensure `.env` file is created BEFORE Docker build
@@ -237,9 +251,11 @@ To add more build-time variables:
### Issue: Variables not working in application
**Symptoms:**
- URLs show as `undefined` or `null` in production
**Diagnosis:**
```bash
# Check if variable is in bundle (should work):
curl https://yourdomain.com | grep -o 'NEXT_PUBLIC_SITE_URL'
@@ -249,6 +265,7 @@ docker exec mypage-prod node -e "console.log(process.env.NEXT_PUBLIC_SITE_URL)"
```
**Solution:**
- Verify `.env` was copied during Docker build
- Check Dockerfile logs for `COPY .env* ./` step
- Rebuild with `--no-cache` if needed
@@ -256,9 +273,11 @@ docker exec mypage-prod node -e "console.log(process.env.NEXT_PUBLIC_SITE_URL)"
### Issue: `.env` file not found during Docker build
**Symptoms:**
- Docker build warning: `COPY .env* ./` - no files matched
**Solution:**
- Check `.dockerignore` allows `.env` file
- Verify workflow creates `.env` BEFORE Docker build
- Check file exists: `ls -la .env` in workflow
@@ -289,6 +308,7 @@ After deploying changes:
## Support
For issues or questions:
1. Check workflow logs in Gitea Actions
2. Review Docker build logs
3. Verify Gitea secrets configuration

View File

@@ -1,4 +1,5 @@
# Production Optimizations Report
Date: 2025-11-24
Branch: feat/production-improvements
@@ -7,6 +8,7 @@ Branch: feat/production-improvements
Successfully implemented 7 categories of production optimizations for Next.js 16 blog application.
### Build Status: SUCCESS
- Build Time: ~3.9s compilation + ~1.5s static generation
- Static Pages Generated: 19 pages
- Bundle Size: 1.2MB (static assets)
@@ -17,10 +19,12 @@ Successfully implemented 7 categories of production optimizations for Next.js 16
## 1. Bundle Size Optimization - Remove Unused Dependencies
### Actions Taken:
- Removed `react-syntax-highlighter` (11 packages eliminated)
- Removed `@types/react-syntax-highlighter`
### Impact:
- **11 packages removed** from dependency tree
- Cleaner bundle, faster npm installs
- All remaining dependencies verified as actively used
@@ -30,11 +34,13 @@ Successfully implemented 7 categories of production optimizations for Next.js 16
## 2. Lazy Loading for Heavy Components
### Status:
- Attempted to implement dynamic imports for CodeBlock component
- Tool limitations prevented full implementation
- Benefit would be minimal (CodeBlock already client-side rendered)
### Recommendation:
- Consider manual lazy loading in future if CodeBlock becomes heavier
- Current implementation is already performant
@@ -45,16 +51,19 @@ Successfully implemented 7 categories of production optimizations for Next.js 16
### Security Enhancements Applied:
**Dockerfile.nextjs:**
- Remove SUID/SGID binaries (prevent privilege escalation)
- Remove apk package manager after dependencies installed
- Create proper permissions for /tmp, /.next/cache, /app/logs directories
**docker-compose.prod.yml:**
- Added `security_opt: no-new-privileges:true`
- Added commented read-only filesystem option (optional hardening)
- Documented tmpfs mounts for extra security
### Security Posture:
- Minimal attack surface in production container
- Non-root user execution enforced
- Package manager unavailable at runtime
@@ -66,22 +75,26 @@ Successfully implemented 7 categories of production optimizations for Next.js 16
### Files Created:
**app/sitemap.ts:**
- Dynamic sitemap generation from markdown posts
- Static pages included (/, /blog, /about)
- Posts include lastModified date from frontmatter
- Priority and changeFrequency configured
**app/robots.ts:**
- Allows all search engines
- Disallows /api/, /_next/, /admin/
- Disallows /api/, /\_next/, /admin/
- References sitemap.xml
**app/feed.xml/route.ts:**
- RSS 2.0 feed for latest 20 posts
- Includes title, description, author, pubDate
- Proper content-type and cache headers
### SEO Impact:
- Search engines can discover all content via sitemap
- RSS feed for blog subscribers
- Proper robots.txt prevents indexing of internal routes
@@ -93,16 +106,19 @@ Successfully implemented 7 categories of production optimizations for Next.js 16
### Configuration Updates:
**Sharp:**
- Already installed (production-grade image optimizer)
- Faster than default Next.js image optimizer
**next.config.js - Image Settings:**
- Cache optimized images for 30 days (`minimumCacheTTL`)
- Support AVIF and WebP formats
- SVG rendering enabled with security CSP
- Responsive image sizes configured (640px to 3840px)
### Performance Impact:
- Faster image processing during builds
- Smaller image file sizes (AVIF/WebP)
- Better Core Web Vitals (LCP, CLS)
@@ -113,21 +129,25 @@ Successfully implemented 7 categories of production optimizations for Next.js 16
### Cache Headers Added:
**Static Assets (/_next/static/*):**
**Static Assets (/\_next/static/\*):**
- `Cache-Control: public, max-age=31536000, immutable`
- 1 year cache for versioned assets
**Images (/images/*):**
**Images (/images/\*):**
- `Cache-Control: public, max-age=31536000, immutable`
### Experimental Features Enabled:
**next.config.js - experimental:**
- `staleTimes.dynamic: 30s` (client-side cache for dynamic pages)
- `staleTimes.static: 180s` (client-side cache for static pages)
- `optimizePackageImports` for react-markdown ecosystem
### Performance Impact:
- Reduced bandwidth usage
- Faster repeat visits (cached assets)
- Improved navigation speed (stale-while-revalidate)
@@ -137,18 +157,22 @@ Successfully implemented 7 categories of production optimizations for Next.js 16
## 7. Bundle Analyzer Setup
### Tools Installed:
- `@next/bundle-analyzer` (16.0.3)
### NPM Scripts Added:
- `npm run analyze` - Full bundle analysis
- `npm run analyze:server` - Server bundle only
- `npm run analyze:browser` - Browser bundle only
### Configuration:
- `next.config.analyzer.js` created
- Enabled with `ANALYZE=true` environment variable
### Usage:
```bash
npm run analyze
# Opens browser with bundle visualization
@@ -160,6 +184,7 @@ npm run analyze
## Bundle Size Analysis
### Static Assets:
```
Total Static: 1.2MB
- Largest chunks:
@@ -170,10 +195,12 @@ Total Static: 1.2MB
```
### Standalone Output:
- Total: 44MB (includes Node.js runtime, dependencies, server)
- Expected Docker image size: ~150MB (Alpine + Node.js + app)
### Bundle Composition:
- React + React-DOM: Largest dependencies
- react-markdown ecosystem: Second largest
- Next.js framework: Optimized with tree-shaking
@@ -183,6 +210,7 @@ Total Static: 1.2MB
## Build Verification
### Build Output:
```
Creating an optimized production build ...
✓ Compiled successfully in 3.9s
@@ -200,6 +228,7 @@ Route (app)
```
### Pre-rendered Pages:
- 19 static pages generated
- 3 blog posts
- 7 tag pages
@@ -210,6 +239,7 @@ Route (app)
## Files Modified/Created
### Modified:
- `Dockerfile.nextjs` (security hardening)
- `docker-compose.prod.yml` (security options)
- `next.config.js` (image optimization, caching headers)
@@ -217,6 +247,7 @@ Route (app)
- `package-lock.json` (dependency updates)
### Created:
- `app/sitemap.ts` (dynamic sitemap)
- `app/robots.ts` (robots.txt)
- `app/feed.xml/route.ts` (RSS feed)
@@ -227,6 +258,7 @@ Route (app)
## Performance Recommendations
### Implemented:
1. Bundle size reduced (11 packages removed)
2. Security hardened (Docker + CSP)
3. SEO optimized (sitemap + robots + RSS)
@@ -235,7 +267,8 @@ Route (app)
6. Bundle analyzer ready for monitoring
### Future Optimizations:
1. Consider CDN for static assets (/images, /_next/static)
1. Consider CDN for static assets (/images, /\_next/static)
2. Monitor bundle sizes with `npm run analyze` on each release
3. Add bundle size limits in CI/CD (fail if > threshold)
4. Consider Edge deployment for global performance
@@ -246,6 +279,7 @@ Route (app)
## Production Deployment Checklist
Before deploying:
- [ ] Set `NEXT_PUBLIC_SITE_URL` in production environment
- [ ] Verify Caddy reverse proxy configuration
- [ ] Test Docker build: `npm run docker:build`

View File

@@ -12,7 +12,7 @@ export default [
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
{ argsIgnorePattern: '^_|node', varsIgnorePattern: '^_' },
],
'no-console': ['warn', { allow: ['warn', 'error'] }],
},
@@ -26,6 +26,7 @@ export default [
'dist/',
'.cache/',
'*.config.js',
'next.config.analyzer.js',
'public/',
'coverage/',
],

11
fix.js
View File

@@ -1,11 +0,0 @@
const fs = require('fs')
let content = fs.readFileSync('lib/remark-copy-images.ts', 'utf8')
const lines = content.split('\n')
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes('replace')) {
console.log(`Line ${i + 1}:`, JSON.stringify(lines[i]))
lines[i] = lines[i].replace(/replace\(\/\\/g / g, 'replace(/\\/g')
console.log(`Fixed:`, JSON.stringify(lines[i]))
}
}
fs.writeFileSync('lib/remark-copy-images.ts', lines.join('\n'))

View File

@@ -3,16 +3,9 @@
* Ensures all required environment variables are set before deployment
*/
const requiredEnvVars = [
'NEXT_PUBLIC_SITE_URL',
'NODE_ENV',
] as const
const requiredEnvVars = ['NEXT_PUBLIC_SITE_URL', 'NODE_ENV'] as const
const optionalEnvVars = [
'PORT',
'HOSTNAME',
'NEXT_PUBLIC_GA_ID',
] as const
const _optionalEnvVars = ['PORT', 'HOSTNAME', 'NEXT_PUBLIC_GA_ID'] as const
export function validateEnvironment() {
const missingVars: string[] = []

View File

@@ -85,7 +85,7 @@ async function copyAndRewritePath(node: ImageNode, options: Options): Promise<vo
node.url = publicUrl + queryParams
return
}
} catch (error) {
} catch {
// Stat failed, proceed with copy
}

View File

@@ -65,7 +65,11 @@ export async function getPopularTags(locale: string = 'en', limit = 10): Promise
return allTags.slice(0, limit)
}
export async function getRelatedTags(tagSlug: string, locale: string = 'en', limit = 5): Promise<TagInfo[]> {
export async function getRelatedTags(
tagSlug: string,
locale: string = 'en',
limit = 5
): Promise<TagInfo[]> {
const posts = await getPostsByTag(tagSlug, locale)
const relatedTagMap = new Map<string, number>()
@@ -107,7 +111,9 @@ export function validateTags(tags: any): string[] {
return validTags
}
export async function getTagCloud(locale: string = 'en'): Promise<Array<TagInfo & { size: 'sm' | 'md' | 'lg' | 'xl' }>> {
export async function getTagCloud(
locale: string = 'en'
): Promise<Array<TagInfo & { size: 'sm' | 'md' | 'lg' | 'xl' }>> {
const tags = await getAllTags(locale)
if (tags.length === 0) return []

View File

@@ -18,6 +18,21 @@
"about": "About"
},
"Home": {
"terminalVersion": "TERMINAL:// V2.0",
"documentLevel": "DOCUMENT LEVEL-1 //",
"heroTitle": "BUILD. WRITE. SHARE.",
"heroSubtitle": "> Explore ideas",
"checkPostsButton": "[CHECK POSTS]",
"aboutMeButton": "[ABOUT ME]",
"recentEntriesLabel": "ARCHIVE ACCESS // RECENT ENTRIES",
"recentEntriesTitle": "> RECENT ENTRIES",
"fileLabel": "FILE#{number} // {category}",
"accessButton": "[ACCESS] >>",
"seePostsButton": "[SEE POSTS] >>",
"seeAllTagsButton": "[SEE ALL TAGS] >>"
},
"BlogListing": {
"title": "Blog",
"subtitle": "Latest articles and thoughts",
@@ -29,7 +44,9 @@
"filterByTag": "Filter by tag",
"clearFilters": "Clear filters",
"foundPosts": "Found {count} posts",
"noPosts": "No posts found"
"noPosts": "No posts found",
"prev": "< PREV",
"next": "NEXT >"
},
"BlogPost": {
@@ -53,7 +70,54 @@
"About": {
"title": "About",
"subtitle": "Learn more about me"
"subtitle": "Learn more about me",
"classificationHeader": ">> _DOC://PUBLIC_ACCESS",
"mainTitle": "ABOUT ME_",
"introLabel": "STATUS: ACTIVE // ROLE: DAD + DEV",
"introParagraph1": "Welcome to my corner of the internet! This is where I share my thoughts, opinions, and experiences - from tech adventures to life as a family man. Yes, I love technology, but there's so much more to life than just code and servers.",
"lifeValuesTitle": "> LIFE & VALUES",
"familyFirstTitle": "[FAMILY FIRST]",
"familyFirstText": "Being a dad to an amazing toddler is my most important role. Family time is sacred - whether it's building block towers, exploring parks, or just enjoying the chaos of everyday life together. Tech can wait; these moments can't.",
"activeLifestyleTitle": "[ACTIVE LIFESTYLE]",
"activeLifestyleText": "I believe in keeping the body active. Whether it's hitting the gym, playing sports, or just staying on the move - physical activity keeps me sharp, balanced, and ready for whatever life throws my way.",
"simpleThingsTitle": "[ENJOYING THE SIMPLE THINGS]",
"simpleThingsText": "Life's too short not to enjoy it. A good drink, a relaxing evening after a long day, or just not doing anything a blowing some steam off.",
"techPurposeTitle": "[TECH WITH PURPOSE]",
"techPurposeText": "Yes, I love tech - self-hosting, privacy, tinkering with hardware. But it's a tool, not a lifestyle. Tech should serve life, not the other way around.",
"contentTitle": "> WHAT YOU'LL FIND HERE",
"contentSubtitle": "CONTENT SCOPE // EVERYTHING FROM TECH TO LIFE",
"contentThoughts": "Thoughts & Opinions",
"contentThoughtsDesc": "My take on life, work, and everything in between",
"contentLifeFamily": "Life & Family",
"contentLifeFamilyDesc": "Adventures in parenting, sports, and enjoying the simple things",
"contentTechResearch": "Tech Research",
"contentTechResearchDesc": "When I dive into interesting technologies and experiments",
"contentSysAdmin": "System Administration",
"contentSysAdminDesc": "Self-hosting, infrastructure, and DevOps adventures",
"contentDevelopment": "Development Insights",
"contentDevelopmentDesc": "Lessons learned from building software",
"contentRandom": "Random Stuff",
"contentRandomDesc": "Because life doesn't fit into neat categories!",
"focusTitle": "> AREAS OF FOCUS",
"focusBeingDadTitle": "[BEING A DAD]",
"focusBeingDadText": "Playing with my boy, teaching moments, watching him grow, building memories together",
"focusStayingActiveTitle": "[STAYING ACTIVE]",
"focusStayingActiveText": "Gym sessions, sports, keeping fit, maintaining energy for life's demands",
"focusTechnologyTitle": "[TECHNOLOGY & SYSTEMS]",
"focusTechnologyText": "Software development, infrastructure, DevOps, self-hosting adventures",
"focusLifeBalanceTitle": "[LIFE BALANCE]",
"focusLifeBalanceText": "Relaxing with good company, enjoying downtime, appreciating the simple moments",
"techStackTitle": "> TECH STACK",
"techStackSubtitle": "TOOLS I USE // WHEN NEEDED",
"techStackDevelopmentTitle": "[DEVELOPMENT]",
"techStackDevelopmentText": ".NET, Golang, TypeScript, Next.js, React",
"techStackInfrastructureTitle": "[INFRASTRUCTURE]",
"techStackInfrastructureText": "Windows Server, Linux, Docker, Hyper-V",
"techStackDesignTitle": "[DESIGN]",
"techStackDesignText": "Tailwind CSS, Markdown, Terminal aesthetics",
"techStackSelfHostingTitle": "[SELF-HOSTING]",
"techStackSelfHostingText": "Home lab, privacy-focused services, full control, Git server",
"contactTitle": "> CONTACT"
},
"NotFound": {

View File

@@ -3,21 +3,32 @@
"siteTitle": "Blog Personal",
"siteDescription": "Gânduri despre tehnologie și dezvoltare"
},
"Navigation": {
"home": "Acasă",
"blog": "Blog",
"tags": "Etichete",
"about": "Despre"
},
"Breadcrumbs": {
"home": "Acasă",
"blog": "Blog",
"tags": "Etichete",
"about": "Despre"
},
"Home": {
"terminalVersion": "TERMINAL:// V2.0",
"documentLevel": "DOCUMENT LEVEL-1 //",
"heroTitle": "BUILD. WRITE. SHARE.",
"heroSubtitle": "> Explore ideas",
"checkPostsButton": "[CHECK POSTS]",
"aboutMeButton": "[ABOUT ME]",
"recentEntriesLabel": "ARCHIVE ACCESS // RECENT ENTRIES",
"recentEntriesTitle": "> RECENT ENTRIES",
"fileLabel": "FILE#{number} // {category}",
"accessButton": "[ACCESS] >>",
"seePostsButton": "[SEE POSTS] >>",
"seeAllTagsButton": "[SEE ALL TAGS] >>"
},
"BlogListing": {
"title": "Blog",
"subtitle": "Ultimele articole și gânduri",
@@ -29,9 +40,10 @@
"filterByTag": "Filtrează după etichetă",
"clearFilters": "Șterge filtrele",
"foundPosts": "{count} articole găsite",
"noPosts": "Niciun articol găsit"
"noPosts": "Niciun articol găsit",
"prev": "< PREV",
"next": "NEXT >"
},
"BlogPost": {
"readMore": "Citește mai mult",
"readingTime": "{minutes} min citire",
@@ -41,7 +53,6 @@
"relatedPosts": "Articole similare",
"sharePost": "Distribuie acest articol"
},
"Tags": {
"title": "Etichete",
"subtitle": "Navighează după subiect",
@@ -50,18 +61,62 @@
"relatedTags": "Etichete similare",
"quickNav": "Navigare rapidă"
},
"About": {
"title": "Despre",
"subtitle": "Află mai multe despre mine"
"subtitle": "Află mai multe despre mine",
"classificationHeader": ">> _DOC://PUBLIC_ACCESS",
"mainTitle": "DESPRE_",
"introLabel": "STATUS: ACTIV // ROL: TATĂ + DEV",
"introParagraph1": "Mi-am făcut un colțișor pe internet unde pot să împărtășesc cam tot ce vreau. O să găsești aici și tech, și viață, și haosul de zi cu zi.",
"lifeValuesTitle": "> VIAȚĂ & VALORI",
"familyFirstTitle": "[FAMILIA PE PRIMUL LOC]",
"familyFirstText": "Să fiu tată pentru un puști genial e cel mai important rol al meu. Timpul cu familia e sfânt fie că construim turnuri din cuburi, explorăm parcuri sau doar trăim frumos haosul de zi cu zi. Tech-ul poate să aștepte, momentele astea nu.",
"activeLifestyleTitle": "[STIL DE VIAȚĂ ACTIV]",
"activeLifestyleText": "Încerc să-mi țin corpul în mișcare. Sală, sport, orice mă scoate din scaun. Mă ajută să fiu mai clar la minte, mai echilibrat și pregătit de ce aruncă viața în mine.",
"simpleThingsTitle": "[BUCURIA LUCRURILOR SIMPLE]",
"simpleThingsText": "Viața e prea scurtă să n-o savurezi. O băutură bună, o seară liniștită după o zi grea sau pur și simplu să nu faci nimic și să lași aburii să iasă… și e perfect așa.",
"techPurposeTitle": "[TECH CU SENS]",
"techPurposeText": "Da, îmi place tehnologia self-hosting, privacy, joacă cu hardware. Dar pentru mine e o unealtă, nu un stil de viață. Tech-ul ar trebui să lucreze pentru tine, nu tu pentru el.",
"contentTitle": "> CE GĂSEȘTI AICI",
"contentSubtitle": "CONTENT SCOPE // DE LA TECH LA VIAȚĂ",
"contentThoughts": "Gânduri & Opinii",
"contentThoughtsDesc": "Cum văd eu viața, munca și tot ce e între ele",
"contentLifeFamily": "Viață & Familie",
"contentLifeFamilyDesc": "Aventuri de părinte, sport și bucuria lucrurilor mici",
"contentTechResearch": "Experimente Tech",
"contentTechResearchDesc": "Când mă afund în tehnologii interesante și experimente ciudate",
"contentSysAdmin": "Administrare Sisteme",
"contentSysAdminDesc": "Self-hosting, infrastructură și aventuri de tip DevOps",
"contentDevelopment": "Development Insights",
"contentDevelopmentDesc": "Lecții învățate din proiectele pe care le construiesc",
"contentRandom": "Chestii Random",
"contentRandomDesc": "Pentru că viața nu intră mereu frumos pe categorii!",
"focusTitle": "> ZONE DE FOCUS",
"focusBeingDadTitle": "[TATĂ ÎN PRIMUL RÂND]",
"focusBeingDadText": "Joacă cu băiatul meu, momente de învățat, să-l văd cum crește și să strângem amintiri împreună",
"focusStayingActiveTitle": "[SĂ RĂMÂN ACTIV]",
"focusStayingActiveText": "Sesiuni la sală, sport, să mă țin în formă și cu energie pentru tot ce am de dus",
"focusTechnologyTitle": "[TECH & SISTEME]",
"focusTechnologyText": "Dezvoltare software, infrastructură, DevOps, aventuri de self-hosting",
"focusLifeBalanceTitle": "[ECHILIBRU ÎN VIAȚĂ]",
"focusLifeBalanceText": "Relax cu prietenii, timp de respiro, apreciat momentele simple",
"techStackTitle": "> TECH STACK",
"techStackSubtitle": "UNELTELE PE CARE LE FOLOSESC // CÂND TREBUIE",
"techStackDevelopmentTitle": "[DEVELOPMENT]",
"techStackDevelopmentText": ".NET, Golang, TypeScript, Next.js, React",
"techStackInfrastructureTitle": "[INFRASTRUCTURĂ]",
"techStackInfrastructureText": "Windows Server, Linux, Docker, Hyper-V",
"techStackDesignTitle": "[DESIGN]",
"techStackDesignText": "Tailwind CSS, Markdown",
"techStackSelfHostingTitle": "[SELF-HOSTING]",
"techStackSelfHostingText": "Home lab, servicii cu focus pe privacy, control total, server Git",
"contactTitle": "> CONTACT"
},
"NotFound": {
"title": "Pagina nu a fost găsită",
"description": "Pagina pe care o cauți nu există",
"goHome": "Mergi la pagina principală"
},
"LanguageSwitcher": {
"switchLanguage": "Schimbă limba",
"currentLanguage": "Limba curentă"

View File

@@ -1,5 +1,5 @@
import createMiddleware from 'next-intl/middleware';
import {routing} from './src/i18n/routing';
import createMiddleware from 'next-intl/middleware'
import { routing } from './src/i18n/routing'
export default createMiddleware({
...routing,
@@ -7,14 +7,10 @@ export default createMiddleware({
localeCookie: {
name: 'NEXT_LOCALE',
maxAge: 60 * 60 * 24 * 365,
sameSite: 'lax'
}
});
sameSite: 'lax',
},
})
export const config = {
matcher: [
'/',
'/(en|ro)/:path*',
'/((?!api|_next|_vercel|.*\\..*).*)'
]
};
matcher: ['/', '/(en|ro)/:path*', '/((?!api|_next|_vercel|.*\\..*).*)'],
}

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -1,4 +1,4 @@
const withNextIntl = require('next-intl/plugin')();
const withNextIntl = require('next-intl/plugin')()
/** @type {import('next').NextConfig} */
// ============================================
@@ -8,7 +8,6 @@ const withNextIntl = require('next-intl/plugin')();
// Deprecated options have been removed (swcMinify, reactStrictMode)
// SWC minification is now default in Next.js 16
// Production-ready Next.js configuration with standalone output
// This configuration is optimized for Docker deployment with minimal image size
//
@@ -123,12 +122,7 @@ const nextConfig = {
},
// Optimize package imports for smaller bundles
optimizePackageImports: [
'react-markdown',
'rehype-raw',
'rehype-sanitize',
'remark-gfm',
],
optimizePackageImports: ['react-markdown', 'rehype-raw', 'rehype-sanitize', 'remark-gfm'],
// Enable PPR (Partial Prerendering) - Next.js 16 feature
// Uncomment to enable (currently in beta)

View File

@@ -1,5 +1,4 @@
import {createNavigation} from 'next-intl/navigation';
import {routing} from './routing';
import { createNavigation } from 'next-intl/navigation'
import { routing } from './routing'
export const {Link, redirect, usePathname, useRouter} =
createNavigation(routing);
export const { Link, redirect, usePathname, useRouter } = createNavigation(routing)

View File

@@ -1,15 +1,15 @@
import {getRequestConfig} from 'next-intl/server';
import {routing} from './routing';
import { getRequestConfig } from 'next-intl/server'
import { routing } from './routing'
export default getRequestConfig(async ({requestLocale}) => {
let locale = await requestLocale;
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
locale = routing.defaultLocale
}
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default
};
});
messages: (await import(`../../messages/${locale}.json`)).default,
}
})

View File

@@ -1,4 +1,4 @@
import {defineRouting} from 'next-intl/routing';
import { defineRouting } from 'next-intl/routing'
export const routing = defineRouting({
locales: ['en', 'ro'],
@@ -6,8 +6,8 @@ export const routing = defineRouting({
localePrefix: 'always',
localeNames: {
en: 'English',
ro: 'Română'
}
} as any);
ro: 'Română',
},
} as any)
export type Locale = (typeof routing.locales)[number];
export type Locale = (typeof routing.locales)[number]

View File

@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -23,12 +19,8 @@
}
],
"paths": {
"@/*": [
"./*"
],
"@/i18n/*": [
"./src/i18n/*"
]
"@/*": ["./*"],
"@/i18n/*": ["./src/i18n/*"]
}
},
"include": [
@@ -38,7 +30,5 @@
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
"exclude": ["node_modules"]
}

View File

@@ -1,5 +1,6 @@
type Messages = typeof import('../messages/en.json');
type Messages = typeof import('../messages/en.json')
declare global {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface IntlMessages extends Messages {}
}