diff --git a/app/[locale]/about/page.tsx b/app/[locale]/about/page.tsx index 059a0cd..6553ac4 100644 --- a/app/[locale]/about/page.tsx +++ b/app/[locale]/about/page.tsx @@ -1,6 +1,6 @@ import { Metadata } from 'next' import { Navbar } from '@/components/blog/navbar' -import {setRequestLocale, getTranslations} from 'next-intl/server' +import { setRequestLocale, getTranslations } from 'next-intl/server' export const metadata: Metadata = { title: 'About', @@ -8,11 +8,11 @@ export const metadata: Metadata = { } type Props = { - params: Promise<{locale: string}> + params: Promise<{ locale: string }> } -export default async function AboutPage({params}: Props) { - const {locale} = await params +export default async function AboutPage({ params }: Props) { + const { locale } = await params setRequestLocale(locale) const t = await getTranslations('About') return ( diff --git a/app/[locale]/blog/[...slug]/not-found.tsx b/app/[locale]/blog/[...slug]/not-found.tsx index b0e9602..1bcc9e5 100644 --- a/app/[locale]/blog/[...slug]/not-found.tsx +++ b/app/[locale]/blog/[...slug]/not-found.tsx @@ -9,9 +9,7 @@ export default function NotFound() {

404

{t('title')}

-

- {t('description')} -

+

{t('description')}

- {t("subtitle")} + {t('subtitle')}

- > {t("title")}_ + > {t('title')}_

@@ -103,8 +103,7 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps) {/* Results Count */}

- {t("foundPosts", {count: filteredAndSortedPosts.length})}{' '} - + {t('foundPosts', { count: filteredAndSortedPosts.length })}{' '}

@@ -127,7 +126,7 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps) ) : (

- {t("noPosts")} + {t('noPosts')}

)} diff --git a/app/[locale]/blog/page.tsx b/app/[locale]/blog/page.tsx index c80bdd4..cde074b 100644 --- a/app/[locale]/blog/page.tsx +++ b/app/[locale]/blog/page.tsx @@ -3,7 +3,7 @@ import BlogPageClient from './blog-client' import { setRequestLocale } from 'next-intl/server' export default async function BlogPage({ params }: { params: Promise<{ locale: string }> }) { - const { locale } = await params; + 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() diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index 009cb7c..55666f7 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -1,25 +1,21 @@ -import {notFound} from 'next/navigation' -import {setRequestLocale} from 'next-intl/server' -import {routing} from '@/src/i18n/routing' -import {ReactNode} from 'react' +import { notFound } from 'next/navigation' +import { setRequestLocale } from 'next-intl/server' +import { routing } from '@/src/i18n/routing' +import { ReactNode } from 'react' type Props = { children: ReactNode breadcrumbs: ReactNode - params: Promise<{locale: string}> + params: Promise<{ locale: string }> } export function generateStaticParams() { - return routing.locales.map((locale) => ({locale})) + return routing.locales.map(locale => ({ locale })) } -export default async function LocaleLayout({ - children, - breadcrumbs, - params -}: Props) { - const {locale} = await params - +export default async function LocaleLayout({ children, breadcrumbs, params }: Props) { + const { locale } = await params + if (!routing.locales.includes(locale as any)) { notFound() } diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 4d59ec2..72e2f9d 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -1,16 +1,16 @@ -import {Link} from '@/src/i18n/navigation' +import { Link } from '@/src/i18n/navigation' import Image from 'next/image' import { getAllPosts } from '@/lib/markdown' import { formatDate } from '@/lib/utils' import { ThemeToggle } from '@/components/theme-toggle' -import {setRequestLocale, getTranslations} from 'next-intl/server' +import { setRequestLocale, getTranslations } from 'next-intl/server' type Props = { - params: Promise<{locale: string}> + params: Promise<{ locale: string }> } -export default async function HomePage({params}: Props) { - const {locale} = await params +export default async function HomePage({ params }: Props) { + const { locale } = await params setRequestLocale(locale) const t = await getTranslations('Home') const tNav = await getTranslations('Navigation') diff --git a/app/[locale]/tags/[tag]/page.tsx b/app/[locale]/tags/[tag]/page.tsx index eb1c06d..0a4fbce 100644 --- a/app/[locale]/tags/[tag]/page.tsx +++ b/app/[locale]/tags/[tag]/page.tsx @@ -1,11 +1,11 @@ import { Metadata } from 'next' import { notFound } from 'next/navigation' -import {Link} from '@/src/i18n/navigation' +import { Link } from '@/src/i18n/navigation' import { getAllTags, getPostsByTag, getTagInfo, getRelatedTags } from '@/lib/tags' import { TagList } from '@/components/blog/tag-list' import { formatDate } from '@/lib/utils' -import {setRequestLocale} from 'next-intl/server' -import {routing} from '@/src/i18n/routing' +import { setRequestLocale } from 'next-intl/server' +import { routing } from '@/src/i18n/routing' export async function generateStaticParams() { const tags = await getAllTags() diff --git a/app/[locale]/tags/page.tsx b/app/[locale]/tags/page.tsx index 156dd73..7a84d93 100644 --- a/app/[locale]/tags/page.tsx +++ b/app/[locale]/tags/page.tsx @@ -1,9 +1,9 @@ import { Metadata } from 'next' -import {Link} from '@/src/i18n/navigation' +import { Link } from '@/src/i18n/navigation' import { getAllTags, getTagCloud } from '@/lib/tags' import { TagCloud } from '@/components/blog/tag-cloud' import { TagBadge } from '@/components/blog/tag-badge' -import {setRequestLocale} from 'next-intl/server' +import { setRequestLocale } from 'next-intl/server' export const metadata: Metadata = { title: 'Tag-uri', @@ -11,11 +11,11 @@ export const metadata: Metadata = { } type Props = { - params: Promise<{locale: string}> + params: Promise<{ locale: string }> } -export default async function TagsPage({params}: Props) { - const {locale} = await params +export default async function TagsPage({ params }: Props) { + const { locale } = await params setRequestLocale(locale) const allTags = await getAllTags() const tagCloud = await getTagCloud() diff --git a/app/feed.xml/route.ts b/app/feed.xml/route.ts index e1a42c1..7101786 100644 --- a/app/feed.xml/route.ts +++ b/app/feed.xml/route.ts @@ -3,7 +3,7 @@ import { NextResponse } from 'next/server' export async function GET() { const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3030' - const posts = await getAllPosts("en", false) + const posts = await getAllPosts('en', false) const rss = ` diff --git a/app/layout.tsx b/app/layout.tsx index 9de5d47..690aa98 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,8 +3,8 @@ import { JetBrains_Mono } from 'next/font/google' import './globals.css' import { ThemeProvider } from '@/providers/providers' import '@/lib/env-validation' -import {NextIntlClientProvider} from 'next-intl' -import {getMessages} from 'next-intl/server' +import { NextIntlClientProvider } from 'next-intl' +import { getMessages } from 'next-intl/server' const jetbrainsMono = JetBrains_Mono({ subsets: ['latin'], variable: '--font-mono' }) @@ -30,11 +30,7 @@ export const metadata: Metadata = { }, } -export default async function RootLayout({ - children -}: { - children: React.ReactNode -}) { +export default async function RootLayout({ children }: { children: React.ReactNode }) { const messages = await getMessages() return ( diff --git a/app/robots.ts b/app/robots.ts index 01e3f32..a036ad8 100644 --- a/app/robots.ts +++ b/app/robots.ts @@ -2,15 +2,15 @@ import { MetadataRoute } from 'next' export default function robots(): MetadataRoute.Robots { const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3030' - + return { rules: { userAgent: '*', allow: '/', disallow: [ - '/api/', // Disallow API routes (if any) - '/_next/', // Disallow Next.js internals - '/admin/', // Disallow admin (if any) + '/api/', // Disallow API routes (if any) + '/_next/', // Disallow Next.js internals + '/admin/', // Disallow admin (if any) ], }, sitemap: `${baseUrl}/sitemap.xml`, diff --git a/app/sitemap.ts b/app/sitemap.ts index 9325927..d73e95f 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -3,10 +3,10 @@ import { getAllPosts } from '@/lib/markdown' export default async function sitemap(): Promise { const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3030' - + // Get all blog posts - const posts = await getAllPosts("en", false) - + const posts = await getAllPosts('en', false) + // Generate sitemap entries for blog posts const blogPosts: MetadataRoute.Sitemap = posts.map(post => ({ url: `${baseUrl}/blog/${post.slug}`, @@ -14,7 +14,7 @@ export default async function sitemap(): Promise { changeFrequency: 'monthly' as const, priority: 0.8, })) - + // Static pages const staticPages: MetadataRoute.Sitemap = [ { @@ -36,6 +36,6 @@ export default async function sitemap(): Promise { priority: 0.7, }, ] - + return [...staticPages, ...blogPosts] } diff --git a/components/blog/OptimizedImage.tsx b/components/blog/OptimizedImage.tsx index 500ab57..41ae046 100644 --- a/components/blog/OptimizedImage.tsx +++ b/components/blog/OptimizedImage.tsx @@ -62,9 +62,7 @@ export function OptimizedImage({ return ( {imageElement} - {caption && ( - {caption} - )} + {caption && {caption}} ) } diff --git a/components/blog/blog-card.tsx b/components/blog/blog-card.tsx index aaaa1b5..e39ed73 100644 --- a/components/blog/blog-card.tsx +++ b/components/blog/blog-card.tsx @@ -40,7 +40,7 @@ export function BlogCard({ post, variant }: BlogCardProps) { ))}
- > {t('readingTime', {minutes: post.readingTime})} + > {t('readingTime', { minutes: post.readingTime })} @@ -84,7 +84,7 @@ export function BlogCard({ post, variant }: BlogCardProps) { ))} - > {t('readingTime', {minutes: post.readingTime})} + > {t('readingTime', { minutes: post.readingTime })} @@ -129,7 +129,7 @@ export function BlogCard({ post, variant }: BlogCardProps) { ))} - > {t('readingTime', {minutes: post.readingTime})} + > {t('readingTime', { minutes: post.readingTime })} diff --git a/components/blog/popular-tags.tsx b/components/blog/popular-tags.tsx index 8ea1217..ff80305 100644 --- a/components/blog/popular-tags.tsx +++ b/components/blog/popular-tags.tsx @@ -3,7 +3,7 @@ import { getPopularTags } from '@/lib/tags' import { TagBadge } from './tag-badge' export async function PopularTags({ limit = 5 }: { limit?: number }) { - const tags = await getPopularTags("en", limit) + const tags = await getPopularTags('en', limit) if (tags.length === 0) return null diff --git a/components/blog/tag-cloud.tsx b/components/blog/tag-cloud.tsx index 26d258f..c5f31fa 100644 --- a/components/blog/tag-cloud.tsx +++ b/components/blog/tag-cloud.tsx @@ -28,7 +28,7 @@ export function TagCloud({ tags }: TagCloudProps) { hover:text-cyan-400 transition-colors `} - title={t('postsWithTag', {count: tag.count, tag: tag.name})} + title={t('postsWithTag', { count: tag.count, tag: tag.name })} > #{tag.name} diff --git a/components/layout/Breadcrumbs.tsx b/components/layout/Breadcrumbs.tsx index 57f9fa5..1005f20 100644 --- a/components/layout/Breadcrumbs.tsx +++ b/components/layout/Breadcrumbs.tsx @@ -1,6 +1,6 @@ 'use client' -import {Link} from '@/i18n/navigation' +import { Link } from '@/i18n/navigation' import { usePathname } from 'next/navigation' import { useLocale, useTranslations } from 'next-intl' import { Fragment } from 'react' diff --git a/components/layout/LanguageSwitcher.tsx b/components/layout/LanguageSwitcher.tsx index 078ea8f..2b89bd8 100644 --- a/components/layout/LanguageSwitcher.tsx +++ b/components/layout/LanguageSwitcher.tsx @@ -1,21 +1,21 @@ -'use client'; +'use client' -import {useLocale} from 'next-intl'; -import {useRouter, usePathname} from '@/i18n/navigation'; -import {routing} from '@/i18n/routing'; -import {useState} from 'react'; +import { useLocale } from 'next-intl' +import { useRouter, usePathname } from '@/i18n/navigation' +import { routing } from '@/i18n/routing' +import { useState } from 'react' export default function LanguageSwitcher() { - const locale = useLocale(); - const router = useRouter(); - const pathname = usePathname(); - const [isOpen, setIsOpen] = useState(false); + const locale = useLocale() + const router = useRouter() + const pathname = usePathname() + const [isOpen, setIsOpen] = useState(false) const handleLocaleChange = (newLocale: string) => { - router.replace(pathname, {locale: newLocale}); - router.refresh(); - setIsOpen(false); - }; + router.replace(pathname, { locale: newLocale }) + router.refresh() + setIsOpen(false) + } return (
@@ -36,9 +36,8 @@ export default function LanguageSwitcher() { className={` w-full text-left px-4 py-2 font-mono uppercase text-xs border-b border-slate-700 last:border-b-0 - ${locale === loc - ? 'bg-cyan-900 text-cyan-300' - : 'text-slate-400 hover:bg-slate-800' + ${ + locale === loc ? 'bg-cyan-900 text-cyan-300' : 'text-slate-400 hover:bg-slate-800' } `} > @@ -48,12 +47,7 @@ export default function LanguageSwitcher() {
)} - {isOpen && ( -
setIsOpen(false)} - /> - )} + {isOpen &&
setIsOpen(false)} />}
- ); + ) } diff --git a/content/blog/en/why-this-page.md b/content/blog/en/why-this-page.md index 614dfc5..ba3fbb5 100644 --- a/content/blog/en/why-this-page.md +++ b/content/blog/en/why-this-page.md @@ -24,7 +24,7 @@ Well, yes, there are. But I believe that sharing some of my opinions and experie ## Why self-host? -![Selfhosting rig](./whythispage/selfhostedrig.gif?w=400 "My self-hosting setup | A look at the hardware running this blog") +![Selfhosting rig](./whythispage/selfhostedrig.gif?w=400 'My self-hosting setup | A look at the hardware running this blog') Now, let's talk about why I chose to self-host this blog. In a nutshell, self-hosting gave me: diff --git a/content/blog/ro/why-this-page.md b/content/blog/ro/why-this-page.md index 454216b..23ed5df 100644 --- a/content/blog/ro/why-this-page.md +++ b/content/blog/ro/why-this-page.md @@ -23,7 +23,7 @@ Dacă te gândești de ce să mai creezi inca un blog cand sunt atea pe net, pai ## De ce selfhost? -![Selfhosting rig](./whythispage/selfhostedrig.gif?w=400 "Acesta este pc-ul | Hardware-ul pe care ruleaza cest webpage") +![Selfhosting rig](./whythispage/selfhostedrig.gif?w=400 'Acesta este pc-ul | Hardware-ul pe care ruleaza cest webpage') Am inceput sa fac hosting acasa din cateva motive: diff --git a/docs/ENV_CONFIG_GUIDE.md b/docs/ENV_CONFIG_GUIDE.md index d21b057..ebc26ea 100644 --- a/docs/ENV_CONFIG_GUIDE.md +++ b/docs/ENV_CONFIG_GUIDE.md @@ -5,6 +5,7 @@ This guide documents the configuration for build-time environment variables in the Next.js CI/CD pipeline. **Problem Solved:** Next.js 16 requires `NEXT_PUBLIC_*` variables available during `npm run build` for: + - SEO metadata (`metadataBase`) - Sitemap generation - OpenGraph URLs @@ -19,6 +20,7 @@ This guide documents the configuration for build-time environment variables in t ### 1. `.gitea/workflows/main.yml` **Changes:** + - Added step to create `.env` from Gitea secrets (after checkout) - Added cleanup step to remove `.env` after Docker push @@ -34,7 +36,7 @@ This guide documents the configuration for build-time environment variables in t NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 EOF - + echo "✅ .env file created successfully" echo "Preview (secrets masked):" 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 run: | docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest - + # Clean up sensitive files rm -f .env echo "✅ Cleaned up .env file" @@ -55,6 +57,7 @@ This guide documents the configuration for build-time environment variables in t ### 2. `Dockerfile.nextjs` **Changes:** + - Added `COPY .env* ./` in builder stage (after copying node_modules, before copying source code) **Added Section:** @@ -73,6 +76,7 @@ COPY .env* ./ ### 3. `.dockerignore` **Changes:** + - Modified to allow `.env` file (created by CI/CD) while excluding other `.env*` files **Updated Section:** @@ -85,6 +89,7 @@ COPY .env* ./ ``` **Explanation:** + - `.env*` excludes all environment files - `!.env` creates exception for main `.env` (from CI/CD) - `.env.local`, `.env.development`, `.env.production.local` remain excluded @@ -99,11 +104,12 @@ Navigate to: **Repository Settings → Secrets** Add the following secret: -| Secret Name | Value | Type | Description | -|------------|-------|------|-------------| +| Secret Name | Value | Type | Description | +| ---------------------- | ------------------------ | ------------------ | ------------------- | | `NEXT_PUBLIC_SITE_URL` | `https://yourdomain.com` | Secret or Variable | Production site URL | **Notes:** + - Can be configured as **Secret** (masked in logs) or **Variable** (visible in logs) - Recommended: Use **Variable** since it's a public URL - For sensitive values (API keys), always use **Secret** @@ -113,12 +119,14 @@ Add the following secret: To add more build-time variables: 1. **Add to Gitea Secrets/Variables:** + ``` NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX NEXT_PUBLIC_API_URL=https://api.example.com ``` 2. **Update workflow `.env` creation step:** + ```yaml cat > .env << EOF NEXT_PUBLIC_SITE_URL=${{ secrets.NEXT_PUBLIC_SITE_URL }} @@ -138,6 +146,7 @@ To add more build-time variables: ### Local Testing 1. **Create test `.env` file:** + ```bash cat > .env << EOF NEXT_PUBLIC_SITE_URL=http://localhost:3030 @@ -147,24 +156,27 @@ To add more build-time variables: ``` 2. **Build Docker image:** + ```bash docker build -t mypage:test -f Dockerfile.nextjs . ``` 3. **Verify variable is embedded (should show "NOT FOUND" - correct behavior):** + ```bash docker run --rm mypage:test node -e "console.log(process.env.NEXT_PUBLIC_SITE_URL || 'NOT FOUND')" ``` - + **Expected Output:** `NOT FOUND` - + **Why?** `NEXT_PUBLIC_*` variables are embedded in JavaScript bundle during build, NOT available as runtime environment variables. 4. **Test application starts:** + ```bash docker run --rm -p 3030:3030 mypage:test ``` - + Visit `http://localhost:3030` to verify. 5. **Cleanup:** @@ -214,10 +226,10 @@ To add more build-time variables: ### 🔒 Sensitive Data Guidelines -| Type | Use For | Access | -|------|---------|--------| -| `NEXT_PUBLIC_*` | Client-side config (URLs, feature flags) | Public (embedded in JS bundle) | -| `SECRET_*` | Server-side secrets (API keys, DB passwords) | Private (runtime only) | +| Type | Use For | Access | +| --------------- | -------------------------------------------- | ------------------------------ | +| `NEXT_PUBLIC_*` | Client-side config (URLs, feature flags) | Public (embedded in JS bundle) | +| `SECRET_*` | Server-side secrets (API keys, DB passwords) | Private (runtime only) | --- @@ -226,10 +238,12 @@ To add more build-time variables: ### Issue: Variables not available during build **Symptoms:** + - Next.js build errors about missing `NEXT_PUBLIC_SITE_URL` - Metadata/sitemap generation fails **Solution:** + - Verify `NEXT_PUBLIC_SITE_URL` secret exists in Gitea - Check workflow logs for `.env` creation step - Ensure `.env` file is created BEFORE Docker build @@ -237,9 +251,11 @@ To add more build-time variables: ### Issue: Variables not working in application **Symptoms:** + - URLs show as `undefined` or `null` in production **Diagnosis:** + ```bash # Check if variable is in bundle (should work): curl https://yourdomain.com | grep -o 'NEXT_PUBLIC_SITE_URL' @@ -249,6 +265,7 @@ docker exec mypage-prod node -e "console.log(process.env.NEXT_PUBLIC_SITE_URL)" ``` **Solution:** + - Verify `.env` was copied during Docker build - Check Dockerfile logs for `COPY .env* ./` step - Rebuild with `--no-cache` if needed @@ -256,9 +273,11 @@ docker exec mypage-prod node -e "console.log(process.env.NEXT_PUBLIC_SITE_URL)" ### Issue: `.env` file not found during Docker build **Symptoms:** + - Docker build warning: `COPY .env* ./` - no files matched **Solution:** + - Check `.dockerignore` allows `.env` file - Verify workflow creates `.env` BEFORE Docker build - Check file exists: `ls -la .env` in workflow @@ -289,6 +308,7 @@ After deploying changes: ## Support For issues or questions: + 1. Check workflow logs in Gitea Actions 2. Review Docker build logs 3. Verify Gitea secrets configuration diff --git a/docs/OPTIMIZATION_REPORT.md b/docs/OPTIMIZATION_REPORT.md index 0699a86..412db3a 100644 --- a/docs/OPTIMIZATION_REPORT.md +++ b/docs/OPTIMIZATION_REPORT.md @@ -1,4 +1,5 @@ # Production Optimizations Report + Date: 2025-11-24 Branch: feat/production-improvements @@ -7,6 +8,7 @@ Branch: feat/production-improvements Successfully implemented 7 categories of production optimizations for Next.js 16 blog application. ### Build Status: SUCCESS + - Build Time: ~3.9s compilation + ~1.5s static generation - Static Pages Generated: 19 pages - Bundle Size: 1.2MB (static assets) @@ -17,10 +19,12 @@ Successfully implemented 7 categories of production optimizations for Next.js 16 ## 1. Bundle Size Optimization - Remove Unused Dependencies ### Actions Taken: + - Removed `react-syntax-highlighter` (11 packages eliminated) - Removed `@types/react-syntax-highlighter` ### Impact: + - **11 packages removed** from dependency tree - Cleaner bundle, faster npm installs - All remaining dependencies verified as actively used @@ -30,11 +34,13 @@ Successfully implemented 7 categories of production optimizations for Next.js 16 ## 2. Lazy Loading for Heavy Components ### Status: + - Attempted to implement dynamic imports for CodeBlock component - Tool limitations prevented full implementation - Benefit would be minimal (CodeBlock already client-side rendered) ### Recommendation: + - Consider manual lazy loading in future if CodeBlock becomes heavier - Current implementation is already performant @@ -45,16 +51,19 @@ Successfully implemented 7 categories of production optimizations for Next.js 16 ### Security Enhancements Applied: **Dockerfile.nextjs:** + - Remove SUID/SGID binaries (prevent privilege escalation) - Remove apk package manager after dependencies installed - Create proper permissions for /tmp, /.next/cache, /app/logs directories **docker-compose.prod.yml:** + - Added `security_opt: no-new-privileges:true` - Added commented read-only filesystem option (optional hardening) - Documented tmpfs mounts for extra security ### Security Posture: + - Minimal attack surface in production container - Non-root user execution enforced - Package manager unavailable at runtime @@ -66,22 +75,26 @@ Successfully implemented 7 categories of production optimizations for Next.js 16 ### Files Created: **app/sitemap.ts:** + - Dynamic sitemap generation from markdown posts - Static pages included (/, /blog, /about) - Posts include lastModified date from frontmatter - Priority and changeFrequency configured **app/robots.ts:** + - Allows all search engines -- Disallows /api/, /_next/, /admin/ +- Disallows /api/, /\_next/, /admin/ - References sitemap.xml **app/feed.xml/route.ts:** + - RSS 2.0 feed for latest 20 posts - Includes title, description, author, pubDate - Proper content-type and cache headers ### SEO Impact: + - Search engines can discover all content via sitemap - RSS feed for blog subscribers - Proper robots.txt prevents indexing of internal routes @@ -93,16 +106,19 @@ Successfully implemented 7 categories of production optimizations for Next.js 16 ### Configuration Updates: **Sharp:** + - Already installed (production-grade image optimizer) - Faster than default Next.js image optimizer **next.config.js - Image Settings:** + - Cache optimized images for 30 days (`minimumCacheTTL`) - Support AVIF and WebP formats - SVG rendering enabled with security CSP - Responsive image sizes configured (640px to 3840px) ### Performance Impact: + - Faster image processing during builds - Smaller image file sizes (AVIF/WebP) - Better Core Web Vitals (LCP, CLS) @@ -113,21 +129,25 @@ Successfully implemented 7 categories of production optimizations for Next.js 16 ### Cache Headers Added: -**Static Assets (/_next/static/*):** +**Static Assets (/\_next/static/\*):** + - `Cache-Control: public, max-age=31536000, immutable` - 1 year cache for versioned assets -**Images (/images/*):** +**Images (/images/\*):** + - `Cache-Control: public, max-age=31536000, immutable` ### Experimental Features Enabled: **next.config.js - experimental:** + - `staleTimes.dynamic: 30s` (client-side cache for dynamic pages) - `staleTimes.static: 180s` (client-side cache for static pages) - `optimizePackageImports` for react-markdown ecosystem ### Performance Impact: + - Reduced bandwidth usage - Faster repeat visits (cached assets) - Improved navigation speed (stale-while-revalidate) @@ -137,18 +157,22 @@ Successfully implemented 7 categories of production optimizations for Next.js 16 ## 7. Bundle Analyzer Setup ### Tools Installed: + - `@next/bundle-analyzer` (16.0.3) ### NPM Scripts Added: + - `npm run analyze` - Full bundle analysis - `npm run analyze:server` - Server bundle only - `npm run analyze:browser` - Browser bundle only ### Configuration: + - `next.config.analyzer.js` created - Enabled with `ANALYZE=true` environment variable ### Usage: + ```bash npm run analyze # Opens browser with bundle visualization @@ -160,6 +184,7 @@ npm run analyze ## Bundle Size Analysis ### Static Assets: + ``` Total Static: 1.2MB - Largest chunks: @@ -170,10 +195,12 @@ Total Static: 1.2MB ``` ### Standalone Output: + - Total: 44MB (includes Node.js runtime, dependencies, server) - Expected Docker image size: ~150MB (Alpine + Node.js + app) ### Bundle Composition: + - React + React-DOM: Largest dependencies - react-markdown ecosystem: Second largest - Next.js framework: Optimized with tree-shaking @@ -183,6 +210,7 @@ Total Static: 1.2MB ## Build Verification ### Build Output: + ``` Creating an optimized production build ... ✓ Compiled successfully in 3.9s @@ -200,6 +228,7 @@ Route (app) ``` ### Pre-rendered Pages: + - 19 static pages generated - 3 blog posts - 7 tag pages @@ -210,6 +239,7 @@ Route (app) ## Files Modified/Created ### Modified: + - `Dockerfile.nextjs` (security hardening) - `docker-compose.prod.yml` (security options) - `next.config.js` (image optimization, caching headers) @@ -217,6 +247,7 @@ Route (app) - `package-lock.json` (dependency updates) ### Created: + - `app/sitemap.ts` (dynamic sitemap) - `app/robots.ts` (robots.txt) - `app/feed.xml/route.ts` (RSS feed) @@ -227,6 +258,7 @@ Route (app) ## Performance Recommendations ### Implemented: + 1. Bundle size reduced (11 packages removed) 2. Security hardened (Docker + CSP) 3. SEO optimized (sitemap + robots + RSS) @@ -235,7 +267,8 @@ Route (app) 6. Bundle analyzer ready for monitoring ### Future Optimizations: -1. Consider CDN for static assets (/images, /_next/static) + +1. Consider CDN for static assets (/images, /\_next/static) 2. Monitor bundle sizes with `npm run analyze` on each release 3. Add bundle size limits in CI/CD (fail if > threshold) 4. Consider Edge deployment for global performance @@ -246,6 +279,7 @@ Route (app) ## Production Deployment Checklist Before deploying: + - [ ] Set `NEXT_PUBLIC_SITE_URL` in production environment - [ ] Verify Caddy reverse proxy configuration - [ ] Test Docker build: `npm run docker:build` diff --git a/lib/env-validation.ts b/lib/env-validation.ts index 86219c2..28038d1 100644 --- a/lib/env-validation.ts +++ b/lib/env-validation.ts @@ -3,16 +3,9 @@ * Ensures all required environment variables are set before deployment */ -const requiredEnvVars = [ - 'NEXT_PUBLIC_SITE_URL', - 'NODE_ENV', -] as const +const requiredEnvVars = ['NEXT_PUBLIC_SITE_URL', 'NODE_ENV'] as const -const optionalEnvVars = [ - 'PORT', - 'HOSTNAME', - 'NEXT_PUBLIC_GA_ID', -] as const +const optionalEnvVars = ['PORT', 'HOSTNAME', 'NEXT_PUBLIC_GA_ID'] as const export function validateEnvironment() { const missingVars: string[] = [] diff --git a/lib/tags.ts b/lib/tags.ts index 392929a..aa75628 100644 --- a/lib/tags.ts +++ b/lib/tags.ts @@ -65,7 +65,11 @@ export async function getPopularTags(locale: string = 'en', limit = 10): Promise return allTags.slice(0, limit) } -export async function getRelatedTags(tagSlug: string, locale: string = 'en', limit = 5): Promise { +export async function getRelatedTags( + tagSlug: string, + locale: string = 'en', + limit = 5 +): Promise { const posts = await getPostsByTag(tagSlug, locale) const relatedTagMap = new Map() @@ -107,7 +111,9 @@ export function validateTags(tags: any): string[] { return validTags } -export async function getTagCloud(locale: string = 'en'): Promise> { +export async function getTagCloud( + locale: string = 'en' +): Promise> { const tags = await getAllTags(locale) if (tags.length === 0) return [] diff --git a/messages/ro.json b/messages/ro.json index e2ddba4..5f7f57a 100644 --- a/messages/ro.json +++ b/messages/ro.json @@ -121,4 +121,4 @@ "switchLanguage": "Schimbă limba", "currentLanguage": "Limba curentă" } -} \ No newline at end of file +} diff --git a/middleware.ts b/middleware.ts index 5d0d195..6ed5c63 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,5 +1,5 @@ -import createMiddleware from 'next-intl/middleware'; -import {routing} from './src/i18n/routing'; +import createMiddleware from 'next-intl/middleware' +import { routing } from './src/i18n/routing' export default createMiddleware({ ...routing, @@ -7,14 +7,10 @@ export default createMiddleware({ localeCookie: { name: 'NEXT_LOCALE', maxAge: 60 * 60 * 24 * 365, - sameSite: 'lax' - } -}); + sameSite: 'lax', + }, +}) export const config = { - matcher: [ - '/', - '/(en|ro)/:path*', - '/((?!api|_next|_vercel|.*\\..*).*)' - ] -}; + matcher: ['/', '/(en|ro)/:path*', '/((?!api|_next|_vercel|.*\\..*).*)'], +} diff --git a/next.config.js b/next.config.js index 492484c..28f0760 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,4 @@ -const withNextIntl = require('next-intl/plugin')(); +const withNextIntl = require('next-intl/plugin')() /** @type {import('next').NextConfig} */ // ============================================ @@ -8,7 +8,6 @@ const withNextIntl = require('next-intl/plugin')(); // Deprecated options have been removed (swcMinify, reactStrictMode) // SWC minification is now default in Next.js 16 - // Production-ready Next.js configuration with standalone output // This configuration is optimized for Docker deployment with minimal image size // @@ -123,12 +122,7 @@ const nextConfig = { }, // Optimize package imports for smaller bundles - optimizePackageImports: [ - 'react-markdown', - 'rehype-raw', - 'rehype-sanitize', - 'remark-gfm', - ], + optimizePackageImports: ['react-markdown', 'rehype-raw', 'rehype-sanitize', 'remark-gfm'], // Enable PPR (Partial Prerendering) - Next.js 16 feature // Uncomment to enable (currently in beta) diff --git a/src/i18n/navigation.ts b/src/i18n/navigation.ts index 8f5a5e2..a045d71 100644 --- a/src/i18n/navigation.ts +++ b/src/i18n/navigation.ts @@ -1,5 +1,4 @@ -import {createNavigation} from 'next-intl/navigation'; -import {routing} from './routing'; +import { createNavigation } from 'next-intl/navigation' +import { routing } from './routing' -export const {Link, redirect, usePathname, useRouter} = - createNavigation(routing); +export const { Link, redirect, usePathname, useRouter } = createNavigation(routing) diff --git a/src/i18n/request.ts b/src/i18n/request.ts index d00e5cb..0d5af21 100644 --- a/src/i18n/request.ts +++ b/src/i18n/request.ts @@ -1,15 +1,15 @@ -import {getRequestConfig} from 'next-intl/server'; -import {routing} from './routing'; - -export default getRequestConfig(async ({requestLocale}) => { - let locale = await requestLocale; - +import { getRequestConfig } from 'next-intl/server' +import { routing } from './routing' + +export default getRequestConfig(async ({ requestLocale }) => { + let locale = await requestLocale + if (!locale || !routing.locales.includes(locale as any)) { - locale = routing.defaultLocale; + locale = routing.defaultLocale } - + return { locale, - messages: (await import(`../../messages/${locale}.json`)).default - }; -}); + messages: (await import(`../../messages/${locale}.json`)).default, + } +}) diff --git a/src/i18n/routing.ts b/src/i18n/routing.ts index 9d21ecb..1b94c0a 100644 --- a/src/i18n/routing.ts +++ b/src/i18n/routing.ts @@ -1,4 +1,4 @@ -import {defineRouting} from 'next-intl/routing'; +import { defineRouting } from 'next-intl/routing' export const routing = defineRouting({ locales: ['en', 'ro'], @@ -6,8 +6,8 @@ export const routing = defineRouting({ localePrefix: 'always', localeNames: { en: 'English', - ro: 'Română' - } -} as any); + ro: 'Română', + }, +} as any) -export type Locale = (typeof routing.locales)[number]; +export type Locale = (typeof routing.locales)[number] diff --git a/tsconfig.json b/tsconfig.json index 3900e3a..4441b4c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2020", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -23,12 +19,8 @@ } ], "paths": { - "@/*": [ - "./*" - ], - "@/i18n/*": [ - "./src/i18n/*" - ] + "@/*": ["./*"], + "@/i18n/*": ["./src/i18n/*"] } }, "include": [ @@ -38,7 +30,5 @@ ".next/types/**/*.ts", ".next/dev/types/**/*.ts" ], - "exclude": [ - "node_modules" - ] -} \ No newline at end of file + "exclude": ["node_modules"] +} diff --git a/types/translations.d.ts b/types/translations.d.ts index fc4e94f..837db6e 100644 --- a/types/translations.d.ts +++ b/types/translations.d.ts @@ -1,4 +1,4 @@ -type Messages = typeof import('../messages/en.json'); +type Messages = typeof import('../messages/en.json') declare global { interface IntlMessages extends Messages {}