staging #16
46
.vscode/launch.json
vendored
Normal file
46
.vscode/launch.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next'
|
||||||
import { Navbar } from '@/components/blog/navbar'
|
import { Navbar } from '@/components/blog/navbar'
|
||||||
import {setRequestLocale} from 'next-intl/server'
|
import { setRequestLocale, getTranslations } from 'next-intl/server'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'About',
|
title: 'About',
|
||||||
@@ -8,12 +8,13 @@ export const metadata: Metadata = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
params: Promise<{locale: string}>
|
params: Promise<{ locale: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function AboutPage({params}: Props) {
|
export default async function AboutPage({ params }: Props) {
|
||||||
const {locale} = await params
|
const { locale } = await params
|
||||||
setRequestLocale(locale)
|
setRequestLocale(locale)
|
||||||
|
const t = await getTranslations('About')
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
@@ -22,10 +23,10 @@ export default async function AboutPage({params}: Props) {
|
|||||||
{/* Classification Header */}
|
{/* Classification Header */}
|
||||||
<div className="border-2 border-[rgb(var(--border-primary))] p-8 mb-10">
|
<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">
|
<p className="text-[rgb(var(--text-muted))] font-mono text-xs uppercase tracking-widest mb-4">
|
||||||
>> _DOC://PUBLIC_ACCESS
|
{t('classificationHeader')}
|
||||||
</p>
|
</p>
|
||||||
<h1 className="text-4xl md:text-5xl font-mono font-bold uppercase text-[rgb(var(--text-primary))] tracking-tight">
|
<h1 className="text-4xl md:text-5xl font-mono font-bold uppercase text-[rgb(var(--text-primary))] tracking-tight">
|
||||||
ABOUT ME_
|
{t('mainTitle')}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -35,12 +36,10 @@ export default async function AboutPage({params}: Props) {
|
|||||||
<section className="border-2 border-[rgb(var(--border-primary))] p-8">
|
<section className="border-2 border-[rgb(var(--border-primary))] p-8">
|
||||||
<div className="border-l-4 border-[var(--neon-cyan)] pl-6">
|
<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">
|
<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,
|
{t('introParagraph1')}
|
||||||
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.
|
|
||||||
</p>
|
</p>
|
||||||
<p className="font-mono text-sm text-[rgb(var(--text-muted))] uppercase tracking-wider">
|
<p className="font-mono text-sm text-[rgb(var(--text-muted))] uppercase tracking-wider">
|
||||||
STATUS: ACTIVE // ROLE: DAD + DEV + LIFE ENTHUSIAST
|
{t('introLabel')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -48,46 +47,39 @@ export default async function AboutPage({params}: Props) {
|
|||||||
{/* Life & Values Section */}
|
{/* Life & Values Section */}
|
||||||
<section className="border-2 border-[rgb(var(--border-primary))] p-8">
|
<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))]">
|
<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))]">
|
||||||
> LIFE & VALUES
|
{t('lifeValuesTitle')}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="border-l-4 border-[var(--neon-pink)] pl-6">
|
<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">
|
<h3 className="font-mono text-sm font-bold text-[var(--neon-pink)] uppercase mb-2">
|
||||||
[FAMILY FIRST]
|
{t('familyFirstTitle')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
|
<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
|
{t('familyFirstText')}
|
||||||
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.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-l-4 border-[var(--neon-cyan)] pl-6">
|
<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">
|
<h3 className="font-mono text-sm font-bold text-[var(--neon-cyan)] uppercase mb-2">
|
||||||
[ACTIVE LIFESTYLE]
|
{t('activeLifestyleTitle')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
|
<p className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
|
||||||
I believe in keeping the body active. Whether it's hitting the gym, playing
|
{t('activeLifestyleText')}
|
||||||
sports, or just staying on the move - physical activity keeps me sharp,
|
|
||||||
balanced, and ready for whatever life throws my way.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-l-4 border-[var(--neon-green)] pl-6">
|
<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">
|
<h3 className="font-mono text-sm font-bold text-[var(--neon-green)] uppercase mb-2">
|
||||||
[ENJOYING THE SIMPLE THINGS]
|
{t('simpleThingsTitle')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
|
<p className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
|
||||||
Life's too short not to enjoy it. A good drink, a relaxing
|
{t('simpleThingsText')}
|
||||||
evening after a long day, or just not doing anything a blowing some steam off.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-l-4 border-[var(--neon-orange)] pl-6">
|
<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">
|
<h3 className="font-mono text-sm font-bold text-[var(--neon-orange)] uppercase mb-2">
|
||||||
[TECH WITH PURPOSE]
|
{t('techPurposeTitle')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
|
<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's
|
{t('techPurposeText')}
|
||||||
a tool, not a lifestyle. Tech should serve life, not the other way around.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,51 +88,46 @@ export default async function AboutPage({params}: Props) {
|
|||||||
{/* Content Section */}
|
{/* Content Section */}
|
||||||
<section className="border-2 border-[rgb(var(--border-primary))] p-8">
|
<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))]">
|
<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))]">
|
||||||
> WHAT YOU'LL FIND HERE
|
{t('contentTitle')}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="font-mono text-sm text-[rgb(var(--text-muted))] uppercase tracking-wider mb-6">
|
<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>
|
</p>
|
||||||
<ul className="space-y-3">
|
<ul className="space-y-3">
|
||||||
<li className="flex items-start gap-3">
|
<li className="flex items-start gap-3">
|
||||||
<span className="text-[var(--neon-pink)] font-mono font-bold">></span>
|
<span className="text-[var(--neon-pink)] font-mono font-bold">></span>
|
||||||
<span className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
|
<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
|
<strong>{t('contentThoughts')}</strong> - {t('contentThoughtsDesc')}
|
||||||
between
|
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-3">
|
<li className="flex items-start gap-3">
|
||||||
<span className="text-[var(--neon-pink)] font-mono font-bold">></span>
|
<span className="text-[var(--neon-pink)] font-mono font-bold">></span>
|
||||||
<span className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
|
<span className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
|
||||||
<strong>Life & Family</strong> - Adventures in parenting, sports, and enjoying
|
<strong>{t('contentLifeFamily')}</strong> - {t('contentLifeFamilyDesc')}
|
||||||
the simple things
|
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-3">
|
<li className="flex items-start gap-3">
|
||||||
<span className="text-[var(--neon-cyan)] font-mono font-bold">></span>
|
<span className="text-[var(--neon-cyan)] font-mono font-bold">></span>
|
||||||
<span className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
|
<span className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
|
||||||
<strong>Tech Research</strong> - When I dive into interesting technologies and
|
<strong>{t('contentTechResearch')}</strong> - {t('contentTechResearchDesc')}
|
||||||
experiments
|
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-3">
|
<li className="flex items-start gap-3">
|
||||||
<span className="text-[var(--neon-cyan)] font-mono font-bold">></span>
|
<span className="text-[var(--neon-cyan)] font-mono font-bold">></span>
|
||||||
<span className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
|
<span className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
|
||||||
<strong>System Administration</strong> - Self-hosting, infrastructure, and
|
<strong>{t('contentSysAdmin')}</strong> - {t('contentSysAdminDesc')}
|
||||||
DevOps adventures
|
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-3">
|
<li className="flex items-start gap-3">
|
||||||
<span className="text-[var(--neon-cyan)] font-mono font-bold">></span>
|
<span className="text-[var(--neon-cyan)] font-mono font-bold">></span>
|
||||||
<span className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
|
<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>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-3">
|
<li className="flex items-start gap-3">
|
||||||
<span className="text-[var(--neon-green)] font-mono font-bold">></span>
|
<span className="text-[var(--neon-green)] font-mono font-bold">></span>
|
||||||
<span className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
|
<span className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed">
|
||||||
<strong>Random Stuff</strong> - Because life doesn't fit into neat
|
<strong>{t('contentRandom')}</strong> - {t('contentRandomDesc')}
|
||||||
categories!
|
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -149,40 +136,39 @@ export default async function AboutPage({params}: Props) {
|
|||||||
{/* Areas of Focus Section */}
|
{/* Areas of Focus Section */}
|
||||||
<section className="border-2 border-[rgb(var(--border-primary))] p-8">
|
<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))]">
|
<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))]">
|
||||||
> AREAS OF FOCUS
|
{t('focusTitle')}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
<div className="border border-[rgb(var(--border-primary))] p-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">
|
<h3 className="font-mono text-xs font-bold text-[var(--neon-pink)] uppercase mb-2">
|
||||||
[BEING A DAD]
|
{t('focusBeingDadTitle')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
|
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
|
||||||
Playing with my boy, teaching moments, watching him grow, building memories
|
{t('focusBeingDadText')}
|
||||||
together
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="border border-[rgb(var(--border-primary))] p-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">
|
<h3 className="font-mono text-xs font-bold text-[var(--neon-cyan)] uppercase mb-2">
|
||||||
[STAYING ACTIVE]
|
{t('focusStayingActiveTitle')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
|
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
|
||||||
Gym sessions, sports, keeping fit, maintaining energy for life's demands
|
{t('focusStayingActiveText')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="border border-[rgb(var(--border-primary))] p-4">
|
<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">
|
<h3 className="font-mono text-xs font-bold text-[var(--neon-green)] uppercase mb-2">
|
||||||
[TECHNOLOGY & SYSTEMS]
|
{t('focusTechnologyTitle')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
|
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
|
||||||
Software development, infrastructure, DevOps, self-hosting adventures
|
{t('focusTechnologyText')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="border border-[rgb(var(--border-primary))] p-4">
|
<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">
|
<h3 className="font-mono text-xs font-bold text-[var(--neon-orange)] uppercase mb-2">
|
||||||
[LIFE BALANCE]
|
{t('focusLifeBalanceTitle')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,42 +176,42 @@ export default async function AboutPage({params}: Props) {
|
|||||||
{/* Tech Stack Section */}
|
{/* Tech Stack Section */}
|
||||||
<section className="border-2 border-[rgb(var(--border-primary))] p-8">
|
<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))]">
|
<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))]">
|
||||||
> TECH STACK
|
{t('techStackTitle')}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="font-mono text-sm text-[rgb(var(--text-muted))] uppercase tracking-wider mb-6">
|
<p className="font-mono text-sm text-[rgb(var(--text-muted))] uppercase tracking-wider mb-6">
|
||||||
TOOLS I USE // WHEN NEEDED
|
{t('techStackSubtitle')}
|
||||||
</p>
|
</p>
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
<div className="border border-[rgb(var(--border-primary))] p-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">
|
<h3 className="font-mono text-xs font-bold text-[var(--neon-cyan)] uppercase mb-2">
|
||||||
[DEVELOPMENT]
|
{t('techStackDevelopmentTitle')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
|
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
|
||||||
.NET, Golang, TypeScript, Next.js, React
|
{t('techStackDevelopmentText')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="border border-[rgb(var(--border-primary))] p-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">
|
<h3 className="font-mono text-xs font-bold text-[var(--neon-cyan)] uppercase mb-2">
|
||||||
[INFRASTRUCTURE]
|
{t('techStackInfrastructureTitle')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
|
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
|
||||||
Windows Server, Linux, Docker, Hyper-V
|
{t('techStackInfrastructureText')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="border border-[rgb(var(--border-primary))] p-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">
|
<h3 className="font-mono text-xs font-bold text-[var(--neon-cyan)] uppercase mb-2">
|
||||||
[DESIGN]
|
{t('techStackDesignTitle')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
|
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
|
||||||
Tailwind CSS, Markdown, Terminal aesthetics
|
{t('techStackDesignText')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="border border-[rgb(var(--border-primary))] p-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">
|
<h3 className="font-mono text-xs font-bold text-[var(--neon-cyan)] uppercase mb-2">
|
||||||
[SELF-HOSTING]
|
{t('techStackSelfHostingTitle')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="font-mono text-xs text-[rgb(var(--text-primary))] leading-relaxed">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -233,7 +219,7 @@ export default async function AboutPage({params}: Props) {
|
|||||||
{/* Contact Section */}
|
{/* Contact Section */}
|
||||||
<section className="border-2 border-[rgb(var(--border-primary))] p-8">
|
<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))]">
|
<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))]">
|
||||||
> CONTACT
|
{t('contactTitle')}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="border-l-4 border-[var(--neon-pink)] pl-6">
|
<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">
|
{/* <p className="font-mono text-sm text-[rgb(var(--text-primary))] leading-relaxed mb-4">
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ export default function NotFound() {
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-6xl font-bold text-gray-300 dark:text-gray-700 mb-4">404</h1>
|
<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>
|
<h2 className="text-2xl font-semibold mb-4">{t('title')}</h2>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-8">
|
<p className="text-gray-600 dark:text-gray-400 mb-8">{t('description')}</p>
|
||||||
{t('description')}
|
|
||||||
</p>
|
|
||||||
<div className="space-x-4">
|
<div className="space-x-4">
|
||||||
<Link
|
<Link
|
||||||
href="/blog"
|
href="/blog"
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ import { Metadata } from 'next'
|
|||||||
import { notFound } from 'next/navigation'
|
import { notFound } from 'next/navigation'
|
||||||
import { Link } from '@/src/i18n/navigation'
|
import { Link } from '@/src/i18n/navigation'
|
||||||
import { getAllPosts, getPostBySlug, getRelatedPosts } from '@/lib/markdown'
|
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 { TableOfContents } from '@/components/blog/table-of-contents'
|
||||||
import { ReadingProgress } from '@/components/blog/reading-progress'
|
import { ReadingProgress } from '@/components/blog/reading-progress'
|
||||||
import { StickyFooter } from '@/components/blog/sticky-footer'
|
import { StickyFooter } from '@/components/blog/sticky-footer'
|
||||||
import MarkdownRenderer from '@/components/blog/markdown-renderer'
|
import MarkdownRenderer from '@/components/blog/markdown-renderer'
|
||||||
import { setRequestLocale } from 'next-intl/server'
|
|
||||||
import { routing } from '@/src/i18n/routing'
|
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
const locales = ['en', 'ro']
|
const locales = ['en', 'ro']
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { BlogCard } from '@/components/blog/blog-card'
|
|||||||
import { SearchBar } from '@/components/blog/search-bar'
|
import { SearchBar } from '@/components/blog/search-bar'
|
||||||
import { SortDropdown } from '@/components/blog/sort-dropdown'
|
import { SortDropdown } from '@/components/blog/sort-dropdown'
|
||||||
import { TagFilter } from '@/components/blog/tag-filter'
|
import { TagFilter } from '@/components/blog/tag-filter'
|
||||||
import { Navbar } from '@/components/blog/navbar'
|
|
||||||
|
|
||||||
interface BlogPageClientProps {
|
interface BlogPageClientProps {
|
||||||
posts: Post[]
|
posts: Post[]
|
||||||
@@ -69,10 +68,10 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="border-l border-[var(--neon-cyan)] pl-6 mb-12">
|
<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">
|
<p className="font-mono text-xs text-[rgb(var(--text-muted))] uppercase tracking-widest mb-2">
|
||||||
{t("subtitle")}
|
{t('subtitle')}
|
||||||
</p>
|
</p>
|
||||||
<h1 className="text-4xl md:text-6xl font-mono font-bold text-[rgb(var(--text-primary))] uppercase tracking-tight">
|
<h1 className="text-4xl md:text-6xl font-mono font-bold text-[rgb(var(--text-primary))] uppercase tracking-tight">
|
||||||
> {t("title")}_
|
> {t('title')}_
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -104,8 +103,7 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
|
|||||||
{/* Results Count */}
|
{/* Results Count */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<p className="font-mono text-sm text-[rgb(var(--text-muted))] uppercase">
|
<p className="font-mono text-sm text-[rgb(var(--text-muted))] uppercase">
|
||||||
{t("foundPosts", {count: filteredAndSortedPosts.length})}{' '}
|
{t('foundPosts', { count: filteredAndSortedPosts.length })}{' '}
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<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">
|
<p className="font-mono text-lg text-[rgb(var(--text-muted))] uppercase">
|
||||||
{t("noPosts")}
|
{t('noPosts')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -142,7 +140,7 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
|
|||||||
disabled={currentPage === 1}
|
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"
|
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"
|
||||||
>
|
>
|
||||||
< PREV
|
{t('prev')}
|
||||||
</button>
|
</button>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
|
{Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
|
||||||
@@ -164,7 +162,7 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
|
|||||||
disabled={currentPage === totalPages}
|
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"
|
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 >
|
{t('next')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { getAllPosts } from '@/lib/markdown'
|
import { getAllPosts } from '@/lib/markdown'
|
||||||
import BlogPageClient from './blog-client'
|
import BlogPageClient from './blog-client'
|
||||||
import {setRequestLocale} from 'next-intl/server'
|
import { setRequestLocale } from 'next-intl/server'
|
||||||
|
|
||||||
export default async function BlogPage() {
|
export default async function BlogPage({ params }: { params: Promise<{ locale: string }> }) {
|
||||||
const posts = await getAllPosts()
|
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()
|
const allTags = Array.from(new Set(posts.flatMap(post => post.frontmatter.tags))).sort()
|
||||||
|
|
||||||
return <BlogPageClient posts={posts} allTags={allTags} />
|
return <BlogPageClient posts={posts} allTags={allTags} />
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
import {notFound} from 'next/navigation'
|
import { notFound } from 'next/navigation'
|
||||||
import {setRequestLocale} from 'next-intl/server'
|
import { setRequestLocale } from 'next-intl/server'
|
||||||
import {routing} from '@/src/i18n/routing'
|
import { routing } from '@/src/i18n/routing'
|
||||||
import {ReactNode} from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
breadcrumbs: ReactNode
|
breadcrumbs: ReactNode
|
||||||
params: Promise<{locale: string}>
|
params: Promise<{ locale: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
return routing.locales.map((locale) => ({locale}))
|
return routing.locales.map(locale => ({ locale }))
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function LocaleLayout({
|
export default async function LocaleLayout({ children, breadcrumbs, params }: Props) {
|
||||||
children,
|
const { locale } = await params
|
||||||
breadcrumbs,
|
|
||||||
params
|
|
||||||
}: Props) {
|
|
||||||
const {locale} = await params
|
|
||||||
|
|
||||||
if (!routing.locales.includes(locale as any)) {
|
if (!routing.locales.includes(locale as any)) {
|
||||||
notFound()
|
notFound()
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import {Link} from '@/src/i18n/navigation'
|
import { Link } from '@/src/i18n/navigation'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { getAllPosts } from '@/lib/markdown'
|
import { getAllPosts } from '@/lib/markdown'
|
||||||
import { formatDate } from '@/lib/utils'
|
import { formatDate } from '@/lib/utils'
|
||||||
import { ThemeToggle } from '@/components/theme-toggle'
|
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 allPosts = await getAllPosts()
|
||||||
const featuredPosts = allPosts.slice(0, 6)
|
const featuredPosts = allPosts.slice(0, 6)
|
||||||
|
|
||||||
@@ -24,7 +33,7 @@ export default async function HomePage() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Image src="/logo.png" alt="Logo" width={32} height={32} className="opacity-80" />
|
<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">
|
<span className="font-mono text-xs text-slate-500 uppercase tracking-widest">
|
||||||
TERMINAL:// V2.0
|
{t('terminalVersion')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
@@ -32,13 +41,13 @@ export default async function HomePage() {
|
|||||||
href="/blog"
|
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"
|
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>
|
||||||
<Link
|
<Link
|
||||||
href="/about"
|
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"
|
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>
|
</Link>
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</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">
|
<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">
|
<p className="font-mono text-xs text-slate-500 dark:text-slate-500 uppercase tracking-widest mb-2">
|
||||||
DOCUMENT LEVEL-1 //
|
{t('documentLevel')}
|
||||||
</p>
|
</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">
|
<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.
|
{t('heroTitle')}
|
||||||
<br />
|
|
||||||
SHARE.
|
|
||||||
</h1>
|
</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">
|
<p className="text-base md:text-lg lg:text-xl text-slate-700 dark:text-slate-400 font-mono leading-relaxed max-w-2xl">
|
||||||
> Explore ideas
|
{t('heroSubtitle')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -63,13 +70,13 @@ export default async function HomePage() {
|
|||||||
href="/blog"
|
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"
|
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>
|
||||||
<Link
|
<Link
|
||||||
href="/about"
|
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"
|
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>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,10 +88,10 @@ export default async function HomePage() {
|
|||||||
<div className="max-w-7xl mx-auto px-6">
|
<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">
|
<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">
|
<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>
|
</p>
|
||||||
<h2 className="text-3xl md:text-5xl font-mono font-bold text-slate-900 dark:text-slate-100 uppercase tracking-tight">
|
<h2 className="text-3xl md:text-5xl font-mono font-bold text-slate-900 dark:text-slate-100 uppercase tracking-tight">
|
||||||
> RECENT ENTRIES
|
{t('recentEntriesTitle')}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -134,7 +141,7 @@ export default async function HomePage() {
|
|||||||
href={`/blog/${post.slug}`}
|
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"
|
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Ă] >>
|
{t('accessButton')}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@@ -147,14 +154,14 @@ export default async function HomePage() {
|
|||||||
href="/blog"
|
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"
|
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] >>
|
{t('seePostsButton')}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<Link
|
<Link
|
||||||
href="/tags"
|
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"
|
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] >>
|
{t('seeAllTagsButton')}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next'
|
||||||
import { notFound } from 'next/navigation'
|
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 { getAllTags, getPostsByTag, getTagInfo, getRelatedTags } from '@/lib/tags'
|
||||||
import { TagList } from '@/components/blog/tag-list'
|
import { TagList } from '@/components/blog/tag-list'
|
||||||
import { formatDate } from '@/lib/utils'
|
import { formatDate } from '@/lib/utils'
|
||||||
import {setRequestLocale} from 'next-intl/server'
|
|
||||||
import {routing} from '@/src/i18n/routing'
|
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
const tags = await getAllTags()
|
const tags = await getAllTags()
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next'
|
||||||
import {Link} from '@/src/i18n/navigation'
|
import { Link } from '@/src/i18n/navigation'
|
||||||
import { getAllTags, getTagCloud } from '@/lib/tags'
|
import { getAllTags, getTagCloud } from '@/lib/tags'
|
||||||
import { TagCloud } from '@/components/blog/tag-cloud'
|
import { TagCloud } from '@/components/blog/tag-cloud'
|
||||||
import { TagBadge } from '@/components/blog/tag-badge'
|
import { TagBadge } from '@/components/blog/tag-badge'
|
||||||
import {setRequestLocale} from 'next-intl/server'
|
import { setRequestLocale } from 'next-intl/server'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Tag-uri',
|
title: 'Tag-uri',
|
||||||
@@ -11,11 +11,11 @@ export const metadata: Metadata = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
params: Promise<{locale: string}>
|
params: Promise<{ locale: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function TagsPage({params}: Props) {
|
export default async function TagsPage({ params }: Props) {
|
||||||
const {locale} = await params
|
const { locale } = await params
|
||||||
setRequestLocale(locale)
|
setRequestLocale(locale)
|
||||||
const allTags = await getAllTags()
|
const allTags = await getAllTags()
|
||||||
const tagCloud = await getTagCloud()
|
const tagCloud = await getTagCloud()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { NextResponse } from 'next/server'
|
|||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3030'
|
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"?>
|
const rss = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { JetBrains_Mono } from 'next/font/google'
|
|||||||
import './globals.css'
|
import './globals.css'
|
||||||
import { ThemeProvider } from '@/providers/providers'
|
import { ThemeProvider } from '@/providers/providers'
|
||||||
import '@/lib/env-validation'
|
import '@/lib/env-validation'
|
||||||
import {NextIntlClientProvider} from 'next-intl'
|
import { NextIntlClientProvider } from 'next-intl'
|
||||||
import {getMessages} from 'next-intl/server'
|
import { getMessages } from 'next-intl/server'
|
||||||
|
|
||||||
const jetbrainsMono = JetBrains_Mono({ subsets: ['latin'], variable: '--font-mono' })
|
const jetbrainsMono = JetBrains_Mono({ subsets: ['latin'], variable: '--font-mono' })
|
||||||
|
|
||||||
@@ -30,11 +30,7 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function RootLayout({
|
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
children
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
const messages = await getMessages()
|
const messages = await getMessages()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ export default function robots(): MetadataRoute.Robots {
|
|||||||
userAgent: '*',
|
userAgent: '*',
|
||||||
allow: '/',
|
allow: '/',
|
||||||
disallow: [
|
disallow: [
|
||||||
'/api/', // Disallow API routes (if any)
|
'/api/', // Disallow API routes (if any)
|
||||||
'/_next/', // Disallow Next.js internals
|
'/_next/', // Disallow Next.js internals
|
||||||
'/admin/', // Disallow admin (if any)
|
'/admin/', // Disallow admin (if any)
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
sitemap: `${baseUrl}/sitemap.xml`,
|
sitemap: `${baseUrl}/sitemap.xml`,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|||||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3030'
|
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3030'
|
||||||
|
|
||||||
// Get all blog posts
|
// Get all blog posts
|
||||||
const posts = await getAllPosts("en", false)
|
const posts = await getAllPosts('en', false)
|
||||||
|
|
||||||
// Generate sitemap entries for blog posts
|
// Generate sitemap entries for blog posts
|
||||||
const blogPosts: MetadataRoute.Sitemap = posts.map(post => ({
|
const blogPosts: MetadataRoute.Sitemap = posts.map(post => ({
|
||||||
|
|||||||
@@ -62,9 +62,7 @@ export function OptimizedImage({
|
|||||||
return (
|
return (
|
||||||
<span className={`block my-8 ${className}`}>
|
<span className={`block my-8 ${className}`}>
|
||||||
{imageElement}
|
{imageElement}
|
||||||
{caption && (
|
{caption && <span className="block mt-3 text-center text-sm text-zinc-400">{caption}</span>}
|
||||||
<span className="block mt-3 text-center text-sm text-zinc-400">{caption}</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function BlogCard({ post, variant }: BlogCardProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<span className="inline-flex items-center font-mono text-xs uppercase text-cyan-400 hover:text-cyan-300 transition-colors">
|
<span className="inline-flex items-center font-mono text-xs uppercase text-cyan-400 hover:text-cyan-300 transition-colors">
|
||||||
> {t('readingTime', {minutes: post.readingTime})}
|
> {t('readingTime', { minutes: post.readingTime })}
|
||||||
</span>
|
</span>
|
||||||
</article>
|
</article>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -84,7 +84,7 @@ export function BlogCard({ post, variant }: BlogCardProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<span className="inline-flex items-center font-mono text-xs uppercase text-cyan-400 hover:text-cyan-300 transition-colors">
|
<span className="inline-flex items-center font-mono text-xs uppercase text-cyan-400 hover:text-cyan-300 transition-colors">
|
||||||
> {t('readingTime', {minutes: post.readingTime})}
|
> {t('readingTime', { minutes: post.readingTime })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,7 +129,7 @@ export function BlogCard({ post, variant }: BlogCardProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<span className="inline-flex items-center font-mono text-xs uppercase text-cyan-400 hover:text-cyan-300 transition-colors">
|
<span className="inline-flex items-center font-mono text-xs uppercase text-cyan-400 hover:text-cyan-300 transition-colors">
|
||||||
> {t('readingTime', {minutes: post.readingTime})}
|
> {t('readingTime', { minutes: post.readingTime })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ interface CodeBlockProps {
|
|||||||
code: string
|
code: string
|
||||||
language: string
|
language: string
|
||||||
filename?: 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 [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ interface MarkdownRendererProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function MarkdownRenderer({ content, className = '' }: MarkdownRendererProps) {
|
export default function MarkdownRenderer({ content, className = '' }: MarkdownRendererProps) {
|
||||||
const locale = useLocale()
|
const _locale = useLocale()
|
||||||
return (
|
return (
|
||||||
<div className={`prose prose-invert prose-zinc max-w-none ${className}`}>
|
<div className={`prose prose-invert prose-zinc max-w-none ${className}`}>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getPopularTags } from '@/lib/tags'
|
|||||||
import { TagBadge } from './tag-badge'
|
import { TagBadge } from './tag-badge'
|
||||||
|
|
||||||
export async function PopularTags({ limit = 5 }: { limit?: number }) {
|
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
|
if (tags.length === 0) return null
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function TagCloud({ tags }: TagCloudProps) {
|
|||||||
hover:text-cyan-400
|
hover:text-cyan-400
|
||||||
transition-colors
|
transition-colors
|
||||||
`}
|
`}
|
||||||
title={t('postsWithTag', {count: tag.count, tag: tag.name})}
|
title={t('postsWithTag', { count: tag.count, tag: tag.name })}
|
||||||
>
|
>
|
||||||
#{tag.name}
|
#{tag.name}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import {Link} from '@/i18n/navigation'
|
import { Link } from '@/i18n/navigation'
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
import { useLocale, useTranslations } from 'next-intl'
|
import { useLocale, useTranslations } from 'next-intl'
|
||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
'use client';
|
'use client'
|
||||||
|
|
||||||
import {useLocale} from 'next-intl';
|
import { useLocale } from 'next-intl'
|
||||||
import {useRouter, usePathname} from '@/i18n/navigation';
|
import { useRouter, usePathname } from '@/i18n/navigation'
|
||||||
import {routing} from '@/i18n/routing';
|
import { routing } from '@/i18n/routing'
|
||||||
import {useState} from 'react';
|
import { useState } from 'react'
|
||||||
|
|
||||||
export default function LanguageSwitcher() {
|
export default function LanguageSwitcher() {
|
||||||
const locale = useLocale();
|
const locale = useLocale()
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
const pathname = usePathname();
|
const pathname = usePathname()
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
|
||||||
const handleLocaleChange = (newLocale: string) => {
|
const handleLocaleChange = (newLocale: string) => {
|
||||||
router.replace(pathname, {locale: newLocale});
|
router.replace(pathname, { locale: newLocale })
|
||||||
router.refresh();
|
router.refresh()
|
||||||
setIsOpen(false);
|
setIsOpen(false)
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-[100]">
|
<div className="relative z-[100]">
|
||||||
@@ -36,9 +36,8 @@ export default function LanguageSwitcher() {
|
|||||||
className={`
|
className={`
|
||||||
w-full text-left px-4 py-2 font-mono uppercase text-xs
|
w-full text-left px-4 py-2 font-mono uppercase text-xs
|
||||||
border-b border-slate-700 last:border-b-0
|
border-b border-slate-700 last:border-b-0
|
||||||
${locale === loc
|
${
|
||||||
? 'bg-cyan-900 text-cyan-300'
|
locale === loc ? 'bg-cyan-900 text-cyan-300' : 'text-slate-400 hover:bg-slate-800'
|
||||||
: 'text-slate-400 hover:bg-slate-800'
|
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
@@ -48,12 +47,7 @@ export default function LanguageSwitcher() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isOpen && (
|
{isOpen && <div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />}
|
||||||
<div
|
|
||||||
className="fixed inset-0 z-40"
|
|
||||||
onClick={() => setIsOpen(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: 'Why I created this page'
|
title: 'First post'
|
||||||
description: 'First post'
|
description: 'First post'
|
||||||
date: '2025-12-02'
|
date: '2025-12-02'
|
||||||
author: 'Rares'
|
author: 'Rares'
|
||||||
@@ -24,7 +24,7 @@ Well, yes, there are. But I believe that sharing some of my opinions and experie
|
|||||||
|
|
||||||
## Why self-host?
|
## Why self-host?
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Now, let's talk about why I chose to self-host this blog. In a nutshell, self-hosting gave me:
|
Now, let's talk about why I chose to self-host this blog. In a nutshell, self-hosting gave me:
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: 'Why I created this page'
|
title: 'Primul post'
|
||||||
description: 'First post'
|
description: 'Primul post'
|
||||||
date: '2025-12-02'
|
date: '2025-12-02'
|
||||||
author: 'Rares'
|
author: 'Rares'
|
||||||
category: 'Opinion'
|
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?
|
## De ce selfhost?
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Am inceput sa fac hosting acasa din cateva motive:
|
Am inceput sa fac hosting acasa din cateva motive:
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
This guide documents the configuration for build-time environment variables in the Next.js CI/CD pipeline.
|
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:
|
**Problem Solved:** Next.js 16 requires `NEXT_PUBLIC_*` variables available during `npm run build` for:
|
||||||
|
|
||||||
- SEO metadata (`metadataBase`)
|
- SEO metadata (`metadataBase`)
|
||||||
- Sitemap generation
|
- Sitemap generation
|
||||||
- OpenGraph URLs
|
- OpenGraph URLs
|
||||||
@@ -19,6 +20,7 @@ This guide documents the configuration for build-time environment variables in t
|
|||||||
### 1. `.gitea/workflows/main.yml`
|
### 1. `.gitea/workflows/main.yml`
|
||||||
|
|
||||||
**Changes:**
|
**Changes:**
|
||||||
|
|
||||||
- Added step to create `.env` from Gitea secrets (after checkout)
|
- Added step to create `.env` from Gitea secrets (after checkout)
|
||||||
- Added cleanup step to remove `.env` after Docker push
|
- 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`
|
### 2. `Dockerfile.nextjs`
|
||||||
|
|
||||||
**Changes:**
|
**Changes:**
|
||||||
|
|
||||||
- Added `COPY .env* ./` in builder stage (after copying node_modules, before copying source code)
|
- Added `COPY .env* ./` in builder stage (after copying node_modules, before copying source code)
|
||||||
|
|
||||||
**Added Section:**
|
**Added Section:**
|
||||||
@@ -73,6 +76,7 @@ COPY .env* ./
|
|||||||
### 3. `.dockerignore`
|
### 3. `.dockerignore`
|
||||||
|
|
||||||
**Changes:**
|
**Changes:**
|
||||||
|
|
||||||
- Modified to allow `.env` file (created by CI/CD) while excluding other `.env*` files
|
- Modified to allow `.env` file (created by CI/CD) while excluding other `.env*` files
|
||||||
|
|
||||||
**Updated Section:**
|
**Updated Section:**
|
||||||
@@ -85,6 +89,7 @@ COPY .env* ./
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Explanation:**
|
**Explanation:**
|
||||||
|
|
||||||
- `.env*` excludes all environment files
|
- `.env*` excludes all environment files
|
||||||
- `!.env` creates exception for main `.env` (from CI/CD)
|
- `!.env` creates exception for main `.env` (from CI/CD)
|
||||||
- `.env.local`, `.env.development`, `.env.production.local` remain excluded
|
- `.env.local`, `.env.development`, `.env.production.local` remain excluded
|
||||||
@@ -99,11 +104,12 @@ Navigate to: **Repository Settings → Secrets**
|
|||||||
|
|
||||||
Add the following secret:
|
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 |
|
| `NEXT_PUBLIC_SITE_URL` | `https://yourdomain.com` | Secret or Variable | Production site URL |
|
||||||
|
|
||||||
**Notes:**
|
**Notes:**
|
||||||
|
|
||||||
- Can be configured as **Secret** (masked in logs) or **Variable** (visible in logs)
|
- Can be configured as **Secret** (masked in logs) or **Variable** (visible in logs)
|
||||||
- Recommended: Use **Variable** since it's a public URL
|
- Recommended: Use **Variable** since it's a public URL
|
||||||
- For sensitive values (API keys), always use **Secret**
|
- For sensitive values (API keys), always use **Secret**
|
||||||
@@ -113,12 +119,14 @@ Add the following secret:
|
|||||||
To add more build-time variables:
|
To add more build-time variables:
|
||||||
|
|
||||||
1. **Add to Gitea Secrets/Variables:**
|
1. **Add to Gitea Secrets/Variables:**
|
||||||
|
|
||||||
```
|
```
|
||||||
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
|
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
|
||||||
NEXT_PUBLIC_API_URL=https://api.example.com
|
NEXT_PUBLIC_API_URL=https://api.example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Update workflow `.env` creation step:**
|
2. **Update workflow `.env` creation step:**
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
cat > .env << EOF
|
cat > .env << EOF
|
||||||
NEXT_PUBLIC_SITE_URL=${{ secrets.NEXT_PUBLIC_SITE_URL }}
|
NEXT_PUBLIC_SITE_URL=${{ secrets.NEXT_PUBLIC_SITE_URL }}
|
||||||
@@ -138,6 +146,7 @@ To add more build-time variables:
|
|||||||
### Local Testing
|
### Local Testing
|
||||||
|
|
||||||
1. **Create test `.env` file:**
|
1. **Create test `.env` file:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cat > .env << EOF
|
cat > .env << EOF
|
||||||
NEXT_PUBLIC_SITE_URL=http://localhost:3030
|
NEXT_PUBLIC_SITE_URL=http://localhost:3030
|
||||||
@@ -147,11 +156,13 @@ To add more build-time variables:
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. **Build Docker image:**
|
2. **Build Docker image:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -t mypage:test -f Dockerfile.nextjs .
|
docker build -t mypage:test -f Dockerfile.nextjs .
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Verify variable is embedded (should show "NOT FOUND" - correct behavior):**
|
3. **Verify variable is embedded (should show "NOT FOUND" - correct behavior):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run --rm mypage:test node -e "console.log(process.env.NEXT_PUBLIC_SITE_URL || 'NOT FOUND')"
|
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.
|
**Why?** `NEXT_PUBLIC_*` variables are embedded in JavaScript bundle during build, NOT available as runtime environment variables.
|
||||||
|
|
||||||
4. **Test application starts:**
|
4. **Test application starts:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run --rm -p 3030:3030 mypage:test
|
docker run --rm -p 3030:3030 mypage:test
|
||||||
```
|
```
|
||||||
@@ -214,10 +226,10 @@ To add more build-time variables:
|
|||||||
|
|
||||||
### 🔒 Sensitive Data Guidelines
|
### 🔒 Sensitive Data Guidelines
|
||||||
|
|
||||||
| Type | Use For | Access |
|
| Type | Use For | Access |
|
||||||
|------|---------|--------|
|
| --------------- | -------------------------------------------- | ------------------------------ |
|
||||||
| `NEXT_PUBLIC_*` | Client-side config (URLs, feature flags) | Public (embedded in JS bundle) |
|
| `NEXT_PUBLIC_*` | Client-side config (URLs, feature flags) | Public (embedded in JS bundle) |
|
||||||
| `SECRET_*` | Server-side secrets (API keys, DB passwords) | Private (runtime only) |
|
| `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
|
### Issue: Variables not available during build
|
||||||
|
|
||||||
**Symptoms:**
|
**Symptoms:**
|
||||||
|
|
||||||
- Next.js build errors about missing `NEXT_PUBLIC_SITE_URL`
|
- Next.js build errors about missing `NEXT_PUBLIC_SITE_URL`
|
||||||
- Metadata/sitemap generation fails
|
- Metadata/sitemap generation fails
|
||||||
|
|
||||||
**Solution:**
|
**Solution:**
|
||||||
|
|
||||||
- Verify `NEXT_PUBLIC_SITE_URL` secret exists in Gitea
|
- Verify `NEXT_PUBLIC_SITE_URL` secret exists in Gitea
|
||||||
- Check workflow logs for `.env` creation step
|
- Check workflow logs for `.env` creation step
|
||||||
- Ensure `.env` file is created BEFORE Docker build
|
- Ensure `.env` file is created BEFORE Docker build
|
||||||
@@ -237,9 +251,11 @@ To add more build-time variables:
|
|||||||
### Issue: Variables not working in application
|
### Issue: Variables not working in application
|
||||||
|
|
||||||
**Symptoms:**
|
**Symptoms:**
|
||||||
|
|
||||||
- URLs show as `undefined` or `null` in production
|
- URLs show as `undefined` or `null` in production
|
||||||
|
|
||||||
**Diagnosis:**
|
**Diagnosis:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Check if variable is in bundle (should work):
|
# Check if variable is in bundle (should work):
|
||||||
curl https://yourdomain.com | grep -o 'NEXT_PUBLIC_SITE_URL'
|
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:**
|
**Solution:**
|
||||||
|
|
||||||
- Verify `.env` was copied during Docker build
|
- Verify `.env` was copied during Docker build
|
||||||
- Check Dockerfile logs for `COPY .env* ./` step
|
- Check Dockerfile logs for `COPY .env* ./` step
|
||||||
- Rebuild with `--no-cache` if needed
|
- 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
|
### Issue: `.env` file not found during Docker build
|
||||||
|
|
||||||
**Symptoms:**
|
**Symptoms:**
|
||||||
|
|
||||||
- Docker build warning: `COPY .env* ./` - no files matched
|
- Docker build warning: `COPY .env* ./` - no files matched
|
||||||
|
|
||||||
**Solution:**
|
**Solution:**
|
||||||
|
|
||||||
- Check `.dockerignore` allows `.env` file
|
- Check `.dockerignore` allows `.env` file
|
||||||
- Verify workflow creates `.env` BEFORE Docker build
|
- Verify workflow creates `.env` BEFORE Docker build
|
||||||
- Check file exists: `ls -la .env` in workflow
|
- Check file exists: `ls -la .env` in workflow
|
||||||
@@ -289,6 +308,7 @@ After deploying changes:
|
|||||||
## Support
|
## Support
|
||||||
|
|
||||||
For issues or questions:
|
For issues or questions:
|
||||||
|
|
||||||
1. Check workflow logs in Gitea Actions
|
1. Check workflow logs in Gitea Actions
|
||||||
2. Review Docker build logs
|
2. Review Docker build logs
|
||||||
3. Verify Gitea secrets configuration
|
3. Verify Gitea secrets configuration
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# Production Optimizations Report
|
# Production Optimizations Report
|
||||||
|
|
||||||
Date: 2025-11-24
|
Date: 2025-11-24
|
||||||
Branch: feat/production-improvements
|
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.
|
Successfully implemented 7 categories of production optimizations for Next.js 16 blog application.
|
||||||
|
|
||||||
### Build Status: SUCCESS
|
### Build Status: SUCCESS
|
||||||
|
|
||||||
- Build Time: ~3.9s compilation + ~1.5s static generation
|
- Build Time: ~3.9s compilation + ~1.5s static generation
|
||||||
- Static Pages Generated: 19 pages
|
- Static Pages Generated: 19 pages
|
||||||
- Bundle Size: 1.2MB (static assets)
|
- 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
|
## 1. Bundle Size Optimization - Remove Unused Dependencies
|
||||||
|
|
||||||
### Actions Taken:
|
### Actions Taken:
|
||||||
|
|
||||||
- Removed `react-syntax-highlighter` (11 packages eliminated)
|
- Removed `react-syntax-highlighter` (11 packages eliminated)
|
||||||
- Removed `@types/react-syntax-highlighter`
|
- Removed `@types/react-syntax-highlighter`
|
||||||
|
|
||||||
### Impact:
|
### Impact:
|
||||||
|
|
||||||
- **11 packages removed** from dependency tree
|
- **11 packages removed** from dependency tree
|
||||||
- Cleaner bundle, faster npm installs
|
- Cleaner bundle, faster npm installs
|
||||||
- All remaining dependencies verified as actively used
|
- 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
|
## 2. Lazy Loading for Heavy Components
|
||||||
|
|
||||||
### Status:
|
### Status:
|
||||||
|
|
||||||
- Attempted to implement dynamic imports for CodeBlock component
|
- Attempted to implement dynamic imports for CodeBlock component
|
||||||
- Tool limitations prevented full implementation
|
- Tool limitations prevented full implementation
|
||||||
- Benefit would be minimal (CodeBlock already client-side rendered)
|
- Benefit would be minimal (CodeBlock already client-side rendered)
|
||||||
|
|
||||||
### Recommendation:
|
### Recommendation:
|
||||||
|
|
||||||
- Consider manual lazy loading in future if CodeBlock becomes heavier
|
- Consider manual lazy loading in future if CodeBlock becomes heavier
|
||||||
- Current implementation is already performant
|
- Current implementation is already performant
|
||||||
|
|
||||||
@@ -45,16 +51,19 @@ Successfully implemented 7 categories of production optimizations for Next.js 16
|
|||||||
### Security Enhancements Applied:
|
### Security Enhancements Applied:
|
||||||
|
|
||||||
**Dockerfile.nextjs:**
|
**Dockerfile.nextjs:**
|
||||||
|
|
||||||
- Remove SUID/SGID binaries (prevent privilege escalation)
|
- Remove SUID/SGID binaries (prevent privilege escalation)
|
||||||
- Remove apk package manager after dependencies installed
|
- Remove apk package manager after dependencies installed
|
||||||
- Create proper permissions for /tmp, /.next/cache, /app/logs directories
|
- Create proper permissions for /tmp, /.next/cache, /app/logs directories
|
||||||
|
|
||||||
**docker-compose.prod.yml:**
|
**docker-compose.prod.yml:**
|
||||||
|
|
||||||
- Added `security_opt: no-new-privileges:true`
|
- Added `security_opt: no-new-privileges:true`
|
||||||
- Added commented read-only filesystem option (optional hardening)
|
- Added commented read-only filesystem option (optional hardening)
|
||||||
- Documented tmpfs mounts for extra security
|
- Documented tmpfs mounts for extra security
|
||||||
|
|
||||||
### Security Posture:
|
### Security Posture:
|
||||||
|
|
||||||
- Minimal attack surface in production container
|
- Minimal attack surface in production container
|
||||||
- Non-root user execution enforced
|
- Non-root user execution enforced
|
||||||
- Package manager unavailable at runtime
|
- Package manager unavailable at runtime
|
||||||
@@ -66,22 +75,26 @@ Successfully implemented 7 categories of production optimizations for Next.js 16
|
|||||||
### Files Created:
|
### Files Created:
|
||||||
|
|
||||||
**app/sitemap.ts:**
|
**app/sitemap.ts:**
|
||||||
|
|
||||||
- Dynamic sitemap generation from markdown posts
|
- Dynamic sitemap generation from markdown posts
|
||||||
- Static pages included (/, /blog, /about)
|
- Static pages included (/, /blog, /about)
|
||||||
- Posts include lastModified date from frontmatter
|
- Posts include lastModified date from frontmatter
|
||||||
- Priority and changeFrequency configured
|
- Priority and changeFrequency configured
|
||||||
|
|
||||||
**app/robots.ts:**
|
**app/robots.ts:**
|
||||||
|
|
||||||
- Allows all search engines
|
- Allows all search engines
|
||||||
- Disallows /api/, /_next/, /admin/
|
- Disallows /api/, /\_next/, /admin/
|
||||||
- References sitemap.xml
|
- References sitemap.xml
|
||||||
|
|
||||||
**app/feed.xml/route.ts:**
|
**app/feed.xml/route.ts:**
|
||||||
|
|
||||||
- RSS 2.0 feed for latest 20 posts
|
- RSS 2.0 feed for latest 20 posts
|
||||||
- Includes title, description, author, pubDate
|
- Includes title, description, author, pubDate
|
||||||
- Proper content-type and cache headers
|
- Proper content-type and cache headers
|
||||||
|
|
||||||
### SEO Impact:
|
### SEO Impact:
|
||||||
|
|
||||||
- Search engines can discover all content via sitemap
|
- Search engines can discover all content via sitemap
|
||||||
- RSS feed for blog subscribers
|
- RSS feed for blog subscribers
|
||||||
- Proper robots.txt prevents indexing of internal routes
|
- 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:
|
### Configuration Updates:
|
||||||
|
|
||||||
**Sharp:**
|
**Sharp:**
|
||||||
|
|
||||||
- Already installed (production-grade image optimizer)
|
- Already installed (production-grade image optimizer)
|
||||||
- Faster than default Next.js image optimizer
|
- Faster than default Next.js image optimizer
|
||||||
|
|
||||||
**next.config.js - Image Settings:**
|
**next.config.js - Image Settings:**
|
||||||
|
|
||||||
- Cache optimized images for 30 days (`minimumCacheTTL`)
|
- Cache optimized images for 30 days (`minimumCacheTTL`)
|
||||||
- Support AVIF and WebP formats
|
- Support AVIF and WebP formats
|
||||||
- SVG rendering enabled with security CSP
|
- SVG rendering enabled with security CSP
|
||||||
- Responsive image sizes configured (640px to 3840px)
|
- Responsive image sizes configured (640px to 3840px)
|
||||||
|
|
||||||
### Performance Impact:
|
### Performance Impact:
|
||||||
|
|
||||||
- Faster image processing during builds
|
- Faster image processing during builds
|
||||||
- Smaller image file sizes (AVIF/WebP)
|
- Smaller image file sizes (AVIF/WebP)
|
||||||
- Better Core Web Vitals (LCP, CLS)
|
- Better Core Web Vitals (LCP, CLS)
|
||||||
@@ -113,21 +129,25 @@ Successfully implemented 7 categories of production optimizations for Next.js 16
|
|||||||
|
|
||||||
### Cache Headers Added:
|
### Cache Headers Added:
|
||||||
|
|
||||||
**Static Assets (/_next/static/*):**
|
**Static Assets (/\_next/static/\*):**
|
||||||
|
|
||||||
- `Cache-Control: public, max-age=31536000, immutable`
|
- `Cache-Control: public, max-age=31536000, immutable`
|
||||||
- 1 year cache for versioned assets
|
- 1 year cache for versioned assets
|
||||||
|
|
||||||
**Images (/images/*):**
|
**Images (/images/\*):**
|
||||||
|
|
||||||
- `Cache-Control: public, max-age=31536000, immutable`
|
- `Cache-Control: public, max-age=31536000, immutable`
|
||||||
|
|
||||||
### Experimental Features Enabled:
|
### Experimental Features Enabled:
|
||||||
|
|
||||||
**next.config.js - experimental:**
|
**next.config.js - experimental:**
|
||||||
|
|
||||||
- `staleTimes.dynamic: 30s` (client-side cache for dynamic pages)
|
- `staleTimes.dynamic: 30s` (client-side cache for dynamic pages)
|
||||||
- `staleTimes.static: 180s` (client-side cache for static pages)
|
- `staleTimes.static: 180s` (client-side cache for static pages)
|
||||||
- `optimizePackageImports` for react-markdown ecosystem
|
- `optimizePackageImports` for react-markdown ecosystem
|
||||||
|
|
||||||
### Performance Impact:
|
### Performance Impact:
|
||||||
|
|
||||||
- Reduced bandwidth usage
|
- Reduced bandwidth usage
|
||||||
- Faster repeat visits (cached assets)
|
- Faster repeat visits (cached assets)
|
||||||
- Improved navigation speed (stale-while-revalidate)
|
- 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
|
## 7. Bundle Analyzer Setup
|
||||||
|
|
||||||
### Tools Installed:
|
### Tools Installed:
|
||||||
|
|
||||||
- `@next/bundle-analyzer` (16.0.3)
|
- `@next/bundle-analyzer` (16.0.3)
|
||||||
|
|
||||||
### NPM Scripts Added:
|
### NPM Scripts Added:
|
||||||
|
|
||||||
- `npm run analyze` - Full bundle analysis
|
- `npm run analyze` - Full bundle analysis
|
||||||
- `npm run analyze:server` - Server bundle only
|
- `npm run analyze:server` - Server bundle only
|
||||||
- `npm run analyze:browser` - Browser bundle only
|
- `npm run analyze:browser` - Browser bundle only
|
||||||
|
|
||||||
### Configuration:
|
### Configuration:
|
||||||
|
|
||||||
- `next.config.analyzer.js` created
|
- `next.config.analyzer.js` created
|
||||||
- Enabled with `ANALYZE=true` environment variable
|
- Enabled with `ANALYZE=true` environment variable
|
||||||
|
|
||||||
### Usage:
|
### Usage:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run analyze
|
npm run analyze
|
||||||
# Opens browser with bundle visualization
|
# Opens browser with bundle visualization
|
||||||
@@ -160,6 +184,7 @@ npm run analyze
|
|||||||
## Bundle Size Analysis
|
## Bundle Size Analysis
|
||||||
|
|
||||||
### Static Assets:
|
### Static Assets:
|
||||||
|
|
||||||
```
|
```
|
||||||
Total Static: 1.2MB
|
Total Static: 1.2MB
|
||||||
- Largest chunks:
|
- Largest chunks:
|
||||||
@@ -170,10 +195,12 @@ Total Static: 1.2MB
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Standalone Output:
|
### Standalone Output:
|
||||||
|
|
||||||
- Total: 44MB (includes Node.js runtime, dependencies, server)
|
- Total: 44MB (includes Node.js runtime, dependencies, server)
|
||||||
- Expected Docker image size: ~150MB (Alpine + Node.js + app)
|
- Expected Docker image size: ~150MB (Alpine + Node.js + app)
|
||||||
|
|
||||||
### Bundle Composition:
|
### Bundle Composition:
|
||||||
|
|
||||||
- React + React-DOM: Largest dependencies
|
- React + React-DOM: Largest dependencies
|
||||||
- react-markdown ecosystem: Second largest
|
- react-markdown ecosystem: Second largest
|
||||||
- Next.js framework: Optimized with tree-shaking
|
- Next.js framework: Optimized with tree-shaking
|
||||||
@@ -183,6 +210,7 @@ Total Static: 1.2MB
|
|||||||
## Build Verification
|
## Build Verification
|
||||||
|
|
||||||
### Build Output:
|
### Build Output:
|
||||||
|
|
||||||
```
|
```
|
||||||
Creating an optimized production build ...
|
Creating an optimized production build ...
|
||||||
✓ Compiled successfully in 3.9s
|
✓ Compiled successfully in 3.9s
|
||||||
@@ -200,6 +228,7 @@ Route (app)
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Pre-rendered Pages:
|
### Pre-rendered Pages:
|
||||||
|
|
||||||
- 19 static pages generated
|
- 19 static pages generated
|
||||||
- 3 blog posts
|
- 3 blog posts
|
||||||
- 7 tag pages
|
- 7 tag pages
|
||||||
@@ -210,6 +239,7 @@ Route (app)
|
|||||||
## Files Modified/Created
|
## Files Modified/Created
|
||||||
|
|
||||||
### Modified:
|
### Modified:
|
||||||
|
|
||||||
- `Dockerfile.nextjs` (security hardening)
|
- `Dockerfile.nextjs` (security hardening)
|
||||||
- `docker-compose.prod.yml` (security options)
|
- `docker-compose.prod.yml` (security options)
|
||||||
- `next.config.js` (image optimization, caching headers)
|
- `next.config.js` (image optimization, caching headers)
|
||||||
@@ -217,6 +247,7 @@ Route (app)
|
|||||||
- `package-lock.json` (dependency updates)
|
- `package-lock.json` (dependency updates)
|
||||||
|
|
||||||
### Created:
|
### Created:
|
||||||
|
|
||||||
- `app/sitemap.ts` (dynamic sitemap)
|
- `app/sitemap.ts` (dynamic sitemap)
|
||||||
- `app/robots.ts` (robots.txt)
|
- `app/robots.ts` (robots.txt)
|
||||||
- `app/feed.xml/route.ts` (RSS feed)
|
- `app/feed.xml/route.ts` (RSS feed)
|
||||||
@@ -227,6 +258,7 @@ Route (app)
|
|||||||
## Performance Recommendations
|
## Performance Recommendations
|
||||||
|
|
||||||
### Implemented:
|
### Implemented:
|
||||||
|
|
||||||
1. Bundle size reduced (11 packages removed)
|
1. Bundle size reduced (11 packages removed)
|
||||||
2. Security hardened (Docker + CSP)
|
2. Security hardened (Docker + CSP)
|
||||||
3. SEO optimized (sitemap + robots + RSS)
|
3. SEO optimized (sitemap + robots + RSS)
|
||||||
@@ -235,7 +267,8 @@ Route (app)
|
|||||||
6. Bundle analyzer ready for monitoring
|
6. Bundle analyzer ready for monitoring
|
||||||
|
|
||||||
### Future Optimizations:
|
### 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
|
2. Monitor bundle sizes with `npm run analyze` on each release
|
||||||
3. Add bundle size limits in CI/CD (fail if > threshold)
|
3. Add bundle size limits in CI/CD (fail if > threshold)
|
||||||
4. Consider Edge deployment for global performance
|
4. Consider Edge deployment for global performance
|
||||||
@@ -246,6 +279,7 @@ Route (app)
|
|||||||
## Production Deployment Checklist
|
## Production Deployment Checklist
|
||||||
|
|
||||||
Before deploying:
|
Before deploying:
|
||||||
|
|
||||||
- [ ] Set `NEXT_PUBLIC_SITE_URL` in production environment
|
- [ ] Set `NEXT_PUBLIC_SITE_URL` in production environment
|
||||||
- [ ] Verify Caddy reverse proxy configuration
|
- [ ] Verify Caddy reverse proxy configuration
|
||||||
- [ ] Test Docker build: `npm run docker:build`
|
- [ ] Test Docker build: `npm run docker:build`
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default [
|
|||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
'warn',
|
'warn',
|
||||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
{ argsIgnorePattern: '^_|node', varsIgnorePattern: '^_' },
|
||||||
],
|
],
|
||||||
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||||
},
|
},
|
||||||
@@ -26,6 +26,7 @@ export default [
|
|||||||
'dist/',
|
'dist/',
|
||||||
'.cache/',
|
'.cache/',
|
||||||
'*.config.js',
|
'*.config.js',
|
||||||
|
'next.config.analyzer.js',
|
||||||
'public/',
|
'public/',
|
||||||
'coverage/',
|
'coverage/',
|
||||||
],
|
],
|
||||||
|
|||||||
11
fix.js
11
fix.js
@@ -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'))
|
|
||||||
@@ -3,16 +3,9 @@
|
|||||||
* Ensures all required environment variables are set before deployment
|
* Ensures all required environment variables are set before deployment
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const requiredEnvVars = [
|
const requiredEnvVars = ['NEXT_PUBLIC_SITE_URL', 'NODE_ENV'] as const
|
||||||
'NEXT_PUBLIC_SITE_URL',
|
|
||||||
'NODE_ENV',
|
|
||||||
] as const
|
|
||||||
|
|
||||||
const optionalEnvVars = [
|
const _optionalEnvVars = ['PORT', 'HOSTNAME', 'NEXT_PUBLIC_GA_ID'] as const
|
||||||
'PORT',
|
|
||||||
'HOSTNAME',
|
|
||||||
'NEXT_PUBLIC_GA_ID',
|
|
||||||
] as const
|
|
||||||
|
|
||||||
export function validateEnvironment() {
|
export function validateEnvironment() {
|
||||||
const missingVars: string[] = []
|
const missingVars: string[] = []
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ async function copyAndRewritePath(node: ImageNode, options: Options): Promise<vo
|
|||||||
node.url = publicUrl + queryParams
|
node.url = publicUrl + queryParams
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Stat failed, proceed with copy
|
// Stat failed, proceed with copy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
lib/tags.ts
10
lib/tags.ts
@@ -65,7 +65,11 @@ export async function getPopularTags(locale: string = 'en', limit = 10): Promise
|
|||||||
return allTags.slice(0, limit)
|
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 posts = await getPostsByTag(tagSlug, locale)
|
||||||
const relatedTagMap = new Map<string, number>()
|
const relatedTagMap = new Map<string, number>()
|
||||||
|
|
||||||
@@ -107,7 +111,9 @@ export function validateTags(tags: any): string[] {
|
|||||||
return validTags
|
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)
|
const tags = await getAllTags(locale)
|
||||||
if (tags.length === 0) return []
|
if (tags.length === 0) return []
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,21 @@
|
|||||||
"about": "About"
|
"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": {
|
"BlogListing": {
|
||||||
"title": "Blog",
|
"title": "Blog",
|
||||||
"subtitle": "Latest articles and thoughts",
|
"subtitle": "Latest articles and thoughts",
|
||||||
@@ -29,7 +44,9 @@
|
|||||||
"filterByTag": "Filter by tag",
|
"filterByTag": "Filter by tag",
|
||||||
"clearFilters": "Clear filters",
|
"clearFilters": "Clear filters",
|
||||||
"foundPosts": "Found {count} posts",
|
"foundPosts": "Found {count} posts",
|
||||||
"noPosts": "No posts found"
|
"noPosts": "No posts found",
|
||||||
|
"prev": "< PREV",
|
||||||
|
"next": "NEXT >"
|
||||||
},
|
},
|
||||||
|
|
||||||
"BlogPost": {
|
"BlogPost": {
|
||||||
@@ -53,7 +70,54 @@
|
|||||||
|
|
||||||
"About": {
|
"About": {
|
||||||
"title": "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": {
|
"NotFound": {
|
||||||
|
|||||||
@@ -3,21 +3,32 @@
|
|||||||
"siteTitle": "Blog Personal",
|
"siteTitle": "Blog Personal",
|
||||||
"siteDescription": "Gânduri despre tehnologie și dezvoltare"
|
"siteDescription": "Gânduri despre tehnologie și dezvoltare"
|
||||||
},
|
},
|
||||||
|
|
||||||
"Navigation": {
|
"Navigation": {
|
||||||
"home": "Acasă",
|
"home": "Acasă",
|
||||||
"blog": "Blog",
|
"blog": "Blog",
|
||||||
"tags": "Etichete",
|
"tags": "Etichete",
|
||||||
"about": "Despre"
|
"about": "Despre"
|
||||||
},
|
},
|
||||||
|
|
||||||
"Breadcrumbs": {
|
"Breadcrumbs": {
|
||||||
"home": "Acasă",
|
"home": "Acasă",
|
||||||
"blog": "Blog",
|
"blog": "Blog",
|
||||||
"tags": "Etichete",
|
"tags": "Etichete",
|
||||||
"about": "Despre"
|
"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": {
|
"BlogListing": {
|
||||||
"title": "Blog",
|
"title": "Blog",
|
||||||
"subtitle": "Ultimele articole și gânduri",
|
"subtitle": "Ultimele articole și gânduri",
|
||||||
@@ -29,9 +40,10 @@
|
|||||||
"filterByTag": "Filtrează după etichetă",
|
"filterByTag": "Filtrează după etichetă",
|
||||||
"clearFilters": "Șterge filtrele",
|
"clearFilters": "Șterge filtrele",
|
||||||
"foundPosts": "{count} articole găsite",
|
"foundPosts": "{count} articole găsite",
|
||||||
"noPosts": "Niciun articol găsit"
|
"noPosts": "Niciun articol găsit",
|
||||||
|
"prev": "< PREV",
|
||||||
|
"next": "NEXT >"
|
||||||
},
|
},
|
||||||
|
|
||||||
"BlogPost": {
|
"BlogPost": {
|
||||||
"readMore": "Citește mai mult",
|
"readMore": "Citește mai mult",
|
||||||
"readingTime": "{minutes} min citire",
|
"readingTime": "{minutes} min citire",
|
||||||
@@ -41,7 +53,6 @@
|
|||||||
"relatedPosts": "Articole similare",
|
"relatedPosts": "Articole similare",
|
||||||
"sharePost": "Distribuie acest articol"
|
"sharePost": "Distribuie acest articol"
|
||||||
},
|
},
|
||||||
|
|
||||||
"Tags": {
|
"Tags": {
|
||||||
"title": "Etichete",
|
"title": "Etichete",
|
||||||
"subtitle": "Navighează după subiect",
|
"subtitle": "Navighează după subiect",
|
||||||
@@ -50,18 +61,62 @@
|
|||||||
"relatedTags": "Etichete similare",
|
"relatedTags": "Etichete similare",
|
||||||
"quickNav": "Navigare rapidă"
|
"quickNav": "Navigare rapidă"
|
||||||
},
|
},
|
||||||
|
|
||||||
"About": {
|
"About": {
|
||||||
"title": "Despre",
|
"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": {
|
"NotFound": {
|
||||||
"title": "Pagina nu a fost găsită",
|
"title": "Pagina nu a fost găsită",
|
||||||
"description": "Pagina pe care o cauți nu există",
|
"description": "Pagina pe care o cauți nu există",
|
||||||
"goHome": "Mergi la pagina principală"
|
"goHome": "Mergi la pagina principală"
|
||||||
},
|
},
|
||||||
|
|
||||||
"LanguageSwitcher": {
|
"LanguageSwitcher": {
|
||||||
"switchLanguage": "Schimbă limba",
|
"switchLanguage": "Schimbă limba",
|
||||||
"currentLanguage": "Limba curentă"
|
"currentLanguage": "Limba curentă"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import createMiddleware from 'next-intl/middleware';
|
import createMiddleware from 'next-intl/middleware'
|
||||||
import {routing} from './src/i18n/routing';
|
import { routing } from './src/i18n/routing'
|
||||||
|
|
||||||
export default createMiddleware({
|
export default createMiddleware({
|
||||||
...routing,
|
...routing,
|
||||||
@@ -7,14 +7,10 @@ export default createMiddleware({
|
|||||||
localeCookie: {
|
localeCookie: {
|
||||||
name: 'NEXT_LOCALE',
|
name: 'NEXT_LOCALE',
|
||||||
maxAge: 60 * 60 * 24 * 365,
|
maxAge: 60 * 60 * 24 * 365,
|
||||||
sameSite: 'lax'
|
sameSite: 'lax',
|
||||||
}
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
matcher: ['/', '/(en|ro)/:path*', '/((?!api|_next|_vercel|.*\\..*).*)'],
|
||||||
'/',
|
}
|
||||||
'/(en|ro)/:path*',
|
|
||||||
'/((?!api|_next|_vercel|.*\\..*).*)'
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|||||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <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
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const withNextIntl = require('next-intl/plugin')();
|
const withNextIntl = require('next-intl/plugin')()
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -8,7 +8,6 @@ const withNextIntl = require('next-intl/plugin')();
|
|||||||
// Deprecated options have been removed (swcMinify, reactStrictMode)
|
// Deprecated options have been removed (swcMinify, reactStrictMode)
|
||||||
// SWC minification is now default in Next.js 16
|
// SWC minification is now default in Next.js 16
|
||||||
|
|
||||||
|
|
||||||
// Production-ready Next.js configuration with standalone output
|
// Production-ready Next.js configuration with standalone output
|
||||||
// This configuration is optimized for Docker deployment with minimal image size
|
// This configuration is optimized for Docker deployment with minimal image size
|
||||||
//
|
//
|
||||||
@@ -123,12 +122,7 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Optimize package imports for smaller bundles
|
// Optimize package imports for smaller bundles
|
||||||
optimizePackageImports: [
|
optimizePackageImports: ['react-markdown', 'rehype-raw', 'rehype-sanitize', 'remark-gfm'],
|
||||||
'react-markdown',
|
|
||||||
'rehype-raw',
|
|
||||||
'rehype-sanitize',
|
|
||||||
'remark-gfm',
|
|
||||||
],
|
|
||||||
|
|
||||||
// Enable PPR (Partial Prerendering) - Next.js 16 feature
|
// Enable PPR (Partial Prerendering) - Next.js 16 feature
|
||||||
// Uncomment to enable (currently in beta)
|
// Uncomment to enable (currently in beta)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {createNavigation} from 'next-intl/navigation';
|
import { createNavigation } from 'next-intl/navigation'
|
||||||
import {routing} from './routing';
|
import { routing } from './routing'
|
||||||
|
|
||||||
export const {Link, redirect, usePathname, useRouter} =
|
export const { Link, redirect, usePathname, useRouter } = createNavigation(routing)
|
||||||
createNavigation(routing);
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import {getRequestConfig} from 'next-intl/server';
|
import { getRequestConfig } from 'next-intl/server'
|
||||||
import {routing} from './routing';
|
import { routing } from './routing'
|
||||||
|
|
||||||
export default getRequestConfig(async ({requestLocale}) => {
|
export default getRequestConfig(async ({ requestLocale }) => {
|
||||||
let locale = await requestLocale;
|
let locale = await requestLocale
|
||||||
|
|
||||||
if (!locale || !routing.locales.includes(locale as any)) {
|
if (!locale || !routing.locales.includes(locale as any)) {
|
||||||
locale = routing.defaultLocale;
|
locale = routing.defaultLocale
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
locale,
|
locale,
|
||||||
messages: (await import(`../../messages/${locale}.json`)).default
|
messages: (await import(`../../messages/${locale}.json`)).default,
|
||||||
};
|
}
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {defineRouting} from 'next-intl/routing';
|
import { defineRouting } from 'next-intl/routing'
|
||||||
|
|
||||||
export const routing = defineRouting({
|
export const routing = defineRouting({
|
||||||
locales: ['en', 'ro'],
|
locales: ['en', 'ro'],
|
||||||
@@ -6,8 +6,8 @@ export const routing = defineRouting({
|
|||||||
localePrefix: 'always',
|
localePrefix: 'always',
|
||||||
localeNames: {
|
localeNames: {
|
||||||
en: 'English',
|
en: 'English',
|
||||||
ro: 'Română'
|
ro: 'Română',
|
||||||
}
|
},
|
||||||
} as any);
|
} as any)
|
||||||
|
|
||||||
export type Locale = (typeof routing.locales)[number];
|
export type Locale = (typeof routing.locales)[number]
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -23,12 +19,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": ["./*"],
|
||||||
"./*"
|
"@/i18n/*": ["./src/i18n/*"]
|
||||||
],
|
|
||||||
"@/i18n/*": [
|
|
||||||
"./src/i18n/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
@@ -38,7 +30,5 @@
|
|||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
".next/dev/types/**/*.ts"
|
".next/dev/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": ["node_modules"]
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
3
types/translations.d.ts
vendored
3
types/translations.d.ts
vendored
@@ -1,5 +1,6 @@
|
|||||||
type Messages = typeof import('../messages/en.json');
|
type Messages = typeof import('../messages/en.json')
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
interface IntlMessages extends Messages {}
|
interface IntlMessages extends Messages {}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user