📝 priettier check
All checks were successful
PR Checks / lint-and-build (pull_request) Successful in 18s

This commit is contained in:
RJ
2025-12-04 15:57:39 +02:00
parent b68325123b
commit 101624c4d5
32 changed files with 185 additions and 172 deletions

View File

@@ -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, getTranslations} from 'next-intl/server' import { setRequestLocale, getTranslations } from 'next-intl/server'
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'About', title: 'About',
@@ -8,11 +8,11 @@ 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') const t = await getTranslations('About')
return ( return (

View File

@@ -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"

View File

@@ -68,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">
&gt; {t("title")}_ &gt; {t('title')}_
</h1> </h1>
</div> </div>
@@ -103,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>
@@ -127,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>
)} )}

View File

@@ -3,7 +3,7 @@ import BlogPageClient from './blog-client'
import { setRequestLocale } from 'next-intl/server' import { setRequestLocale } from 'next-intl/server'
export default async function BlogPage({ params }: { params: Promise<{ locale: string }> }) { export default async function BlogPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params; const { locale } = await params
await setRequestLocale(locale) await setRequestLocale(locale)
const posts = await getAllPosts(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()

View File

@@ -1,25 +1,21 @@
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()
} }

View File

@@ -1,16 +1,16 @@
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, getTranslations} from 'next-intl/server' import { setRequestLocale, getTranslations } from 'next-intl/server'
type Props = { type Props = {
params: Promise<{locale: string}> params: Promise<{ locale: string }>
} }
export default async function HomePage({params}: Props) { export default async function HomePage({ params }: Props) {
const {locale} = await params const { locale } = await params
setRequestLocale(locale) setRequestLocale(locale)
const t = await getTranslations('Home') const t = await getTranslations('Home')
const tNav = await getTranslations('Navigation') const tNav = await getTranslations('Navigation')

View File

@@ -1,11 +1,11 @@
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 { setRequestLocale } from 'next-intl/server'
import {routing} from '@/src/i18n/routing' import { routing } from '@/src/i18n/routing'
export async function generateStaticParams() { export async function generateStaticParams() {
const tags = await getAllTags() const tags = await getAllTags()

View File

@@ -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()

View File

@@ -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">

View File

@@ -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 (

View File

@@ -2,15 +2,15 @@ import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots { export default function robots(): MetadataRoute.Robots {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3030' const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3030'
return { return {
rules: { rules: {
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`,

View File

@@ -3,10 +3,10 @@ import { getAllPosts } from '@/lib/markdown'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> { 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 => ({
url: `${baseUrl}/blog/${post.slug}`, url: `${baseUrl}/blog/${post.slug}`,
@@ -14,7 +14,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
changeFrequency: 'monthly' as const, changeFrequency: 'monthly' as const,
priority: 0.8, priority: 0.8,
})) }))
// Static pages // Static pages
const staticPages: MetadataRoute.Sitemap = [ const staticPages: MetadataRoute.Sitemap = [
{ {
@@ -36,6 +36,6 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
priority: 0.7, priority: 0.7,
}, },
] ]
return [...staticPages, ...blogPosts] return [...staticPages, ...blogPosts]
} }

View File

@@ -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>
) )
} }

View File

@@ -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">
&gt; {t('readingTime', {minutes: post.readingTime})} &gt; {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">
&gt; {t('readingTime', {minutes: post.readingTime})} &gt; {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">
&gt; {t('readingTime', {minutes: post.readingTime})} &gt; {t('readingTime', { minutes: post.readingTime })}
</span> </span>
</div> </div>
</article> </article>

View File

@@ -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

View File

@@ -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>

View File

@@ -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'

View File

@@ -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>
); )
} }

View File

@@ -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?
![Selfhosting rig](./whythispage/selfhostedrig.gif?w=400 "My self-hosting setup | A look at the hardware running this blog") ![Selfhosting rig](./whythispage/selfhostedrig.gif?w=400 'My self-hosting setup | A look at the hardware running this blog')
Now, let's talk about why I chose to self-host this blog. In a nutshell, self-hosting gave me: Now, let's talk about why I chose to self-host this blog. In a nutshell, self-hosting gave me:

View File

@@ -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?
![Selfhosting rig](./whythispage/selfhostedrig.gif?w=400 "Acesta este pc-ul | Hardware-ul pe care ruleaza cest webpage") ![Selfhosting rig](./whythispage/selfhostedrig.gif?w=400 'Acesta este pc-ul | Hardware-ul pe care ruleaza cest webpage')
Am inceput sa fac hosting acasa din cateva motive: Am inceput sa fac hosting acasa din cateva motive:

View File

@@ -5,6 +5,7 @@
This guide documents the configuration for build-time environment variables in the Next.js CI/CD pipeline. 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
@@ -34,7 +36,7 @@ This guide documents the configuration for build-time environment variables in t
NODE_ENV=production NODE_ENV=production
NEXT_TELEMETRY_DISABLED=1 NEXT_TELEMETRY_DISABLED=1
EOF EOF
echo "✅ .env file created successfully" echo "✅ .env file created successfully"
echo "Preview (secrets masked):" echo "Preview (secrets masked):"
cat .env | sed 's/=.*/=***MASKED***/g' cat .env | sed 's/=.*/=***MASKED***/g'
@@ -44,7 +46,7 @@ This guide documents the configuration for build-time environment variables in t
- name: 🚀 Push Docker image to registry - name: 🚀 Push Docker image to registry
run: | run: |
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
# Clean up sensitive files # Clean up sensitive files
rm -f .env rm -f .env
echo "✅ Cleaned up .env file" echo "✅ Cleaned up .env file"
@@ -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,24 +156,27 @@ 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')"
``` ```
**Expected Output:** `NOT FOUND` **Expected Output:** `NOT FOUND`
**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
``` ```
Visit `http://localhost:3030` to verify. Visit `http://localhost:3030` to verify.
5. **Cleanup:** 5. **Cleanup:**
@@ -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

View File

@@ -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`

View File

@@ -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[] = []

View File

@@ -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 []

View File

@@ -121,4 +121,4 @@
"switchLanguage": "Schimbă limba", "switchLanguage": "Schimbă limba",
"currentLanguage": "Limba curentă" "currentLanguage": "Limba curentă"
} }
} }

View File

@@ -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|.*\\..*).*)'
]
};

View File

@@ -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)

View File

@@ -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);

View File

@@ -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,
}; }
}); })

View File

@@ -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]

View File

@@ -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" }
]
}

View File

@@ -1,4 +1,4 @@
type Messages = typeof import('../messages/en.json'); type Messages = typeof import('../messages/en.json')
declare global { declare global {
interface IntlMessages extends Messages {} interface IntlMessages extends Messages {}