Compare commits
4 Commits
b68325123b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bec8b5120f | ||
|
|
6adb3a6979 | ||
|
|
bba507a7e8 | ||
|
|
101624c4d5 |
@@ -1,11 +1,16 @@
|
||||
'use client'
|
||||
|
||||
import { Breadcrumbs } from '@/components/layout/Breadcrumbs'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
export default function AboutBreadcrumb() {
|
||||
const t = useTranslations('Breadcrumbs')
|
||||
|
||||
return (
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{
|
||||
label: 'Despre',
|
||||
label: t('about'),
|
||||
href: '/about',
|
||||
current: true,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Breadcrumbs } from '@/components/layout/Breadcrumbs'
|
||||
import { getPostBySlug } from '@/lib/markdown'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
interface BreadcrumbItem {
|
||||
label: string
|
||||
@@ -7,28 +8,19 @@ interface BreadcrumbItem {
|
||||
current?: boolean
|
||||
}
|
||||
|
||||
function formatDirectoryName(name: string): string {
|
||||
const directoryNames: { [key: string]: string } = {
|
||||
tech: 'Tehnologie',
|
||||
design: 'Design',
|
||||
tutorial: 'Tutoriale',
|
||||
}
|
||||
|
||||
return directoryNames[name] || name.charAt(0).toUpperCase() + name.slice(1)
|
||||
}
|
||||
|
||||
export default async function BlogPostBreadcrumb({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string[] }>
|
||||
}) {
|
||||
const t = await getTranslations('Breadcrumbs')
|
||||
const { slug } = await params
|
||||
const slugPath = slug.join('/')
|
||||
const post = await getPostBySlug(slugPath)
|
||||
|
||||
const items: BreadcrumbItem[] = [
|
||||
{
|
||||
label: 'Blog',
|
||||
label: t('blog'),
|
||||
href: '/blog',
|
||||
},
|
||||
]
|
||||
@@ -36,8 +28,9 @@ export default async function BlogPostBreadcrumb({
|
||||
if (slug.length > 1) {
|
||||
for (let i = 0; i < slug.length - 1; i++) {
|
||||
const segmentPath = slug.slice(0, i + 1).join('/')
|
||||
const dirName = slug[i]
|
||||
items.push({
|
||||
label: formatDirectoryName(slug[i]),
|
||||
label: t(dirName) || dirName.charAt(0).toUpperCase() + dirName.slice(1),
|
||||
href: `/blog/${segmentPath}`,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
'use client'
|
||||
|
||||
import { Breadcrumbs } from '@/components/layout/Breadcrumbs'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
export default function BlogBreadcrumb() {
|
||||
const t = useTranslations('Breadcrumbs')
|
||||
|
||||
return (
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{
|
||||
label: 'Blog',
|
||||
label: t('blog'),
|
||||
href: '/blog',
|
||||
current: true,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Breadcrumbs } from '@/components/layout/Breadcrumbs'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
export default async function TagBreadcrumb({ params }: { params: Promise<{ tag: string }> }) {
|
||||
const t = await getTranslations('Breadcrumbs')
|
||||
const { tag } = await params
|
||||
const tagName = tag
|
||||
.split('-')
|
||||
@@ -11,7 +13,7 @@ export default async function TagBreadcrumb({ params }: { params: Promise<{ tag:
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{
|
||||
label: 'Tag-uri',
|
||||
label: t('tags'),
|
||||
href: '/tags',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
'use client'
|
||||
|
||||
import { Breadcrumbs } from '@/components/layout/Breadcrumbs'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
export default function TagsBreadcrumb() {
|
||||
const t = useTranslations('Breadcrumbs')
|
||||
|
||||
return (
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{
|
||||
label: 'Tag-uri',
|
||||
label: t('tags'),
|
||||
href: '/tags',
|
||||
current: true,
|
||||
},
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -9,9 +9,7 @@ export default function NotFound() {
|
||||
<div className="text-center">
|
||||
<h1 className="text-6xl font-bold text-gray-300 dark:text-gray-700 mb-4">404</h1>
|
||||
<h2 className="text-2xl font-semibold mb-4">{t('title')}</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-8">
|
||||
{t('description')}
|
||||
</p>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-8">{t('description')}</p>
|
||||
<div className="space-x-4">
|
||||
<Link
|
||||
href="/blog"
|
||||
|
||||
@@ -2,13 +2,11 @@ import { Metadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { Link } from '@/src/i18n/navigation'
|
||||
import { getAllPosts, getPostBySlug, getRelatedPosts } from '@/lib/markdown'
|
||||
import { formatDate, formatRelativeDate } from '@/lib/utils'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
import { TableOfContents } from '@/components/blog/table-of-contents'
|
||||
import { ReadingProgress } from '@/components/blog/reading-progress'
|
||||
import { StickyFooter } from '@/components/blog/sticky-footer'
|
||||
import MarkdownRenderer from '@/components/blog/markdown-renderer'
|
||||
import { setRequestLocale } from 'next-intl/server'
|
||||
import { routing } from '@/src/i18n/routing'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const locales = ['en', 'ro']
|
||||
|
||||
@@ -68,10 +68,10 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
|
||||
{/* Header */}
|
||||
<div className="border-l border-[var(--neon-cyan)] pl-6 mb-12">
|
||||
<p className="font-mono text-xs text-[rgb(var(--text-muted))] uppercase tracking-widest mb-2">
|
||||
{t("subtitle")}
|
||||
{t('subtitle')}
|
||||
</p>
|
||||
<h1 className="text-4xl md:text-6xl font-mono font-bold text-[rgb(var(--text-primary))] uppercase tracking-tight">
|
||||
> {t("title")}_
|
||||
> {t('title')}_
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -103,8 +103,7 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
|
||||
{/* Results Count */}
|
||||
<div className="mb-6">
|
||||
<p className="font-mono text-sm text-[rgb(var(--text-muted))] uppercase">
|
||||
{t("foundPosts", {count: filteredAndSortedPosts.length})}{' '}
|
||||
|
||||
{t('foundPosts', { count: filteredAndSortedPosts.length })}{' '}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -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">
|
||||
<p className="font-mono text-lg text-[rgb(var(--text-muted))] uppercase">
|
||||
{t("noPosts")}
|
||||
{t('noPosts')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
import {notFound} from 'next/navigation'
|
||||
import {setRequestLocale} from 'next-intl/server'
|
||||
import {routing} from '@/src/i18n/routing'
|
||||
import {ReactNode} from 'react'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { setRequestLocale } from 'next-intl/server'
|
||||
import { routing } from '@/src/i18n/routing'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
breadcrumbs: ReactNode
|
||||
params: Promise<{locale: string}>
|
||||
params: Promise<{ locale: string }>
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return routing.locales.map((locale) => ({locale}))
|
||||
return routing.locales.map(locale => ({ locale }))
|
||||
}
|
||||
|
||||
export default async function LocaleLayout({
|
||||
children,
|
||||
breadcrumbs,
|
||||
params
|
||||
}: Props) {
|
||||
const {locale} = await params
|
||||
export default async function LocaleLayout({ children, breadcrumbs, params }: Props) {
|
||||
const { locale } = await params
|
||||
|
||||
if (!routing.locales.includes(locale as any)) {
|
||||
notFound()
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
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 { HeroHeader } from '@/components/layout/hero-header'
|
||||
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')
|
||||
// const tNav = await getTranslations('Navigation')
|
||||
|
||||
const allPosts = await getAllPosts()
|
||||
const featuredPosts = allPosts.slice(0, 6)
|
||||
@@ -29,29 +29,7 @@ export default async function HomePage({params}: Props) {
|
||||
<div className="relative z-10 max-w-5xl mx-auto px-6 w-full">
|
||||
<div className="border-4 border-slate-300 dark:border-slate-700 bg-white/80 dark:bg-slate-900/80 p-8 md:p-12 transition-colors duration-300">
|
||||
{/* Logo */}
|
||||
<div className="mb-8 flex items-center justify-between border-b-2 border-slate-300 dark:border-slate-800 pb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Image src="/logo.png" alt="Logo" width={32} height={32} className="opacity-80" />
|
||||
<span className="font-mono text-xs text-slate-500 uppercase tracking-widest">
|
||||
{t('terminalVersion')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-4 items-center">
|
||||
<Link
|
||||
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"
|
||||
>
|
||||
[{tNav('blog')}]
|
||||
</Link>
|
||||
<Link
|
||||
href="/about"
|
||||
className="font-mono text-xs text-slate-600 dark:text-slate-400 uppercase tracking-wider hover:text-cyan-600 dark:hover:text-cyan-400"
|
||||
>
|
||||
[{tNav('about')}]
|
||||
</Link>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
<HeroHeader />
|
||||
|
||||
<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">
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Metadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
import {Link} from '@/src/i18n/navigation'
|
||||
import { Link } from '@/src/i18n/navigation'
|
||||
import { getAllTags, getPostsByTag, getTagInfo, getRelatedTags } from '@/lib/tags'
|
||||
import { TagList } from '@/components/blog/tag-list'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
import {setRequestLocale} from 'next-intl/server'
|
||||
import {routing} from '@/src/i18n/routing'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const tags = await getAllTags()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -3,7 +3,7 @@ import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET() {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3030'
|
||||
const posts = await getAllPosts("en", false)
|
||||
const posts = await getAllPosts('en', false)
|
||||
|
||||
const rss = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
|
||||
181
app/globals.css
181
app/globals.css
@@ -449,3 +449,184 @@
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* === MOBILE RESPONSIVE UTILITIES === */
|
||||
@media (max-width: 767px) {
|
||||
.show-mobile-only {
|
||||
display: block;
|
||||
}
|
||||
.hide-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.show-mobile-only {
|
||||
display: none !important;
|
||||
}
|
||||
.hide-mobile {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* === BUTTON GLITCH EFFECT === */
|
||||
@layer utilities {
|
||||
.glitch-btn-cyber {
|
||||
--glitch-shimmy: 5;
|
||||
--glitch-clip-1: polygon(
|
||||
0 2%,
|
||||
100% 2%,
|
||||
100% 95%,
|
||||
95% 95%,
|
||||
95% 90%,
|
||||
85% 90%,
|
||||
85% 95%,
|
||||
8% 95%,
|
||||
0 70%
|
||||
);
|
||||
--glitch-clip-2: polygon(
|
||||
0 78%,
|
||||
100% 78%,
|
||||
100% 100%,
|
||||
95% 100%,
|
||||
95% 90%,
|
||||
85% 90%,
|
||||
85% 100%,
|
||||
8% 100%,
|
||||
0 78%
|
||||
);
|
||||
--glitch-clip-3: polygon(
|
||||
0 44%,
|
||||
100% 44%,
|
||||
100% 54%,
|
||||
95% 54%,
|
||||
95% 54%,
|
||||
85% 54%,
|
||||
85% 54%,
|
||||
8% 54%,
|
||||
0 54%
|
||||
);
|
||||
--glitch-clip-4: polygon(0 0, 100% 0, 100% 0, 95% 0, 95% 0, 85% 0, 85% 0, 8% 0, 0 0);
|
||||
--glitch-clip-5: polygon(
|
||||
0 40%,
|
||||
100% 40%,
|
||||
100% 85%,
|
||||
95% 85%,
|
||||
95% 85%,
|
||||
85% 85%,
|
||||
85% 85%,
|
||||
8% 85%,
|
||||
0 70%
|
||||
);
|
||||
--glitch-clip-6: polygon(
|
||||
0 63%,
|
||||
100% 63%,
|
||||
100% 80%,
|
||||
95% 80%,
|
||||
95% 80%,
|
||||
85% 80%,
|
||||
85% 80%,
|
||||
8% 80%,
|
||||
0 70%
|
||||
);
|
||||
}
|
||||
|
||||
.glitch-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
color: var(--neon-cyan);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.glitch-btn-cyber:is(:hover, :focus-visible) .glitch-overlay {
|
||||
display: flex;
|
||||
animation: glitch-btn-animate 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes glitch-btn-animate {
|
||||
0% {
|
||||
clip-path: var(--glitch-clip-1);
|
||||
}
|
||||
2%,
|
||||
8% {
|
||||
clip-path: var(--glitch-clip-2);
|
||||
transform: translate(calc(var(--glitch-shimmy) * -1%), 0);
|
||||
}
|
||||
6% {
|
||||
clip-path: var(--glitch-clip-2);
|
||||
transform: translate(calc(var(--glitch-shimmy) * 1%), 0);
|
||||
}
|
||||
9% {
|
||||
clip-path: var(--glitch-clip-2);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
10% {
|
||||
clip-path: var(--glitch-clip-3);
|
||||
transform: translate(calc(var(--glitch-shimmy) * 1%), 0);
|
||||
}
|
||||
13% {
|
||||
clip-path: var(--glitch-clip-3);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
14%,
|
||||
21% {
|
||||
clip-path: var(--glitch-clip-4);
|
||||
transform: translate(calc(var(--glitch-shimmy) * 1%), 0);
|
||||
}
|
||||
25%,
|
||||
30% {
|
||||
clip-path: var(--glitch-clip-5);
|
||||
transform: translate(calc(var(--glitch-shimmy) * -1%), 0);
|
||||
}
|
||||
35%,
|
||||
45% {
|
||||
clip-path: var(--glitch-clip-6);
|
||||
transform: translate(calc(var(--glitch-shimmy) * -1%), 0);
|
||||
}
|
||||
40% {
|
||||
clip-path: var(--glitch-clip-6);
|
||||
transform: translate(calc(var(--glitch-shimmy) * 1%), 0);
|
||||
}
|
||||
50% {
|
||||
clip-path: var(--glitch-clip-6);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
55% {
|
||||
clip-path: var(--glitch-clip-3);
|
||||
transform: translate(calc(var(--glitch-shimmy) * 1%), 0);
|
||||
}
|
||||
60% {
|
||||
clip-path: var(--glitch-clip-3);
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
61%,
|
||||
100% {
|
||||
clip-path: var(--glitch-clip-4);
|
||||
}
|
||||
}
|
||||
|
||||
.glitch-btn-subtle {
|
||||
--glitch-shimmy: 2;
|
||||
}
|
||||
|
||||
.glitch-overlay-pink {
|
||||
color: var(--neon-pink);
|
||||
}
|
||||
.glitch-overlay-purple {
|
||||
color: var(--neon-purple);
|
||||
}
|
||||
.glitch-overlay-magenta {
|
||||
color: var(--neon-magenta);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.glitch-btn-cyber:is(:hover, :focus-visible) .glitch-overlay {
|
||||
animation: none;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -8,9 +8,9 @@ export default function robots(): MetadataRoute.Robots {
|
||||
userAgent: '*',
|
||||
allow: '/',
|
||||
disallow: [
|
||||
'/api/', // Disallow API routes (if any)
|
||||
'/_next/', // Disallow Next.js internals
|
||||
'/admin/', // Disallow admin (if any)
|
||||
'/api/', // Disallow API routes (if any)
|
||||
'/_next/', // Disallow Next.js internals
|
||||
'/admin/', // Disallow admin (if any)
|
||||
],
|
||||
},
|
||||
sitemap: `${baseUrl}/sitemap.xml`,
|
||||
|
||||
@@ -5,7 +5,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3030'
|
||||
|
||||
// Get all blog posts
|
||||
const posts = await getAllPosts("en", false)
|
||||
const posts = await getAllPosts('en', false)
|
||||
|
||||
// Generate sitemap entries for blog posts
|
||||
const blogPosts: MetadataRoute.Sitemap = posts.map(post => ({
|
||||
|
||||
@@ -62,9 +62,7 @@ export function OptimizedImage({
|
||||
return (
|
||||
<span className={`block my-8 ${className}`}>
|
||||
{imageElement}
|
||||
{caption && (
|
||||
<span className="block mt-3 text-center text-sm text-zinc-400">{caption}</span>
|
||||
)}
|
||||
{caption && <span className="block mt-3 text-center text-sm text-zinc-400">{caption}</span>}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export function BlogCard({ post, variant }: BlogCardProps) {
|
||||
))}
|
||||
</div>
|
||||
<span className="inline-flex items-center font-mono text-xs uppercase text-cyan-400 hover:text-cyan-300 transition-colors">
|
||||
> {t('readingTime', {minutes: post.readingTime})}
|
||||
> {t('readingTime', { minutes: post.readingTime })}
|
||||
</span>
|
||||
</article>
|
||||
</Link>
|
||||
@@ -84,7 +84,7 @@ export function BlogCard({ post, variant }: BlogCardProps) {
|
||||
))}
|
||||
</div>
|
||||
<span className="inline-flex items-center font-mono text-xs uppercase text-cyan-400 hover:text-cyan-300 transition-colors">
|
||||
> {t('readingTime', {minutes: post.readingTime})}
|
||||
> {t('readingTime', { minutes: post.readingTime })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -129,7 +129,7 @@ export function BlogCard({ post, variant }: BlogCardProps) {
|
||||
))}
|
||||
</div>
|
||||
<span className="inline-flex items-center font-mono text-xs uppercase text-cyan-400 hover:text-cyan-300 transition-colors">
|
||||
> {t('readingTime', {minutes: post.readingTime})}
|
||||
> {t('readingTime', { minutes: post.readingTime })}
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -6,10 +6,10 @@ interface CodeBlockProps {
|
||||
code: string
|
||||
language: string
|
||||
filename?: string
|
||||
showLineNumbers?: boolean
|
||||
_showLineNumbers?: boolean
|
||||
}
|
||||
|
||||
export function CodeBlock({ code, language, filename, showLineNumbers = true }: CodeBlockProps) {
|
||||
export function CodeBlock({ code, language, filename, _showLineNumbers = true }: CodeBlockProps) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = async () => {
|
||||
|
||||
@@ -15,7 +15,7 @@ interface MarkdownRendererProps {
|
||||
}
|
||||
|
||||
export default function MarkdownRenderer({ content, className = '' }: MarkdownRendererProps) {
|
||||
const locale = useLocale()
|
||||
const _locale = useLocale()
|
||||
return (
|
||||
<div className={`prose prose-invert prose-zinc max-w-none ${className}`}>
|
||||
<ReactMarkdown
|
||||
|
||||
@@ -5,11 +5,13 @@ import { useTranslations } from 'next-intl'
|
||||
import { Link } from '@/i18n/navigation'
|
||||
import { ThemeToggle } from '@/components/theme-toggle'
|
||||
import LanguageSwitcher from '@/components/layout/LanguageSwitcher'
|
||||
import { GlitchButton } from '@/components/effects/glitch-button'
|
||||
|
||||
export function Navbar() {
|
||||
const t = useTranslations('Navigation')
|
||||
const [isVisible, setIsVisible] = useState(true)
|
||||
const [lastScrollY, setLastScrollY] = useState(0)
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
@@ -19,6 +21,7 @@ export function Navbar() {
|
||||
setIsVisible(true)
|
||||
} else if (currentScrollY > lastScrollY) {
|
||||
setIsVisible(false)
|
||||
setIsMobileMenuOpen(false)
|
||||
} else {
|
||||
setIsVisible(true)
|
||||
}
|
||||
@@ -32,7 +35,7 @@ export function Navbar() {
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={`border-b-4 border-slate-700 bg-slate-900 dark:bg-zinc-950 sticky top-0 z-50 ${isVisible ? 'navbar-visible' : 'navbar-hidden'}`}
|
||||
className={`border-b-4 border-slate-700 bg-slate-900 dark:bg-zinc-950 top-0 z-50 ${isVisible ? 'navbar-visible' : 'navbar-hidden'}`}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -44,11 +47,12 @@ export function Navbar() {
|
||||
>
|
||||
< {t('home')}
|
||||
</Link>
|
||||
<span className="font-mono text-sm text-zinc-100 dark:text-zinc-300 uppercase tracking-wider">
|
||||
<span className="font-mono text-sm text-zinc-100 dark:text-zinc-300 uppercase tracking-wider hidden md:block">
|
||||
// <span style={{ color: 'var(--neon-pink)' }}>{t('blog')}</span> ARCHIVE
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
|
||||
<div className="hidden md:flex items-center gap-6">
|
||||
<Link
|
||||
href="/about"
|
||||
className="font-mono text-sm text-zinc-400 dark:text-zinc-500 uppercase tracking-wider hover:text-cyan-400 dark:hover:text-cyan-300 transition-colors cursor-pointer"
|
||||
@@ -64,7 +68,45 @@ export function Navbar() {
|
||||
<ThemeToggle />
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
|
||||
<div className="md:hidden flex items-center gap-4">
|
||||
<GlitchButton
|
||||
variant="subtle"
|
||||
glitchColor="cyan"
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
className="font-mono text-sm uppercase tracking-wider px-4 py-2 border-4 border-slate-700 bg-slate-800 dark:bg-zinc-900 text-zinc-100 dark:text-zinc-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-500"
|
||||
aria-label="Toggle mobile menu"
|
||||
aria-expanded={isMobileMenuOpen}
|
||||
>
|
||||
// {isMobileMenuOpen ? 'CLOSE' : 'MENU'}
|
||||
</GlitchButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isMobileMenuOpen && (
|
||||
<div className="md:hidden mt-4 pt-4 border-t-4 border-slate-700">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Link
|
||||
href="/about"
|
||||
className="font-mono text-sm text-zinc-400 dark:text-zinc-500 uppercase tracking-wider hover:text-cyan-400 dark:hover:text-cyan-300 transition-colors cursor-pointer px-4 py-2 border-2 border-slate-700"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
[{t('about')}]
|
||||
</Link>
|
||||
<Link
|
||||
href="/blog"
|
||||
className="font-mono text-sm text-zinc-400 dark:text-zinc-500 uppercase tracking-wider hover:text-cyan-400 dark:hover:text-cyan-300 transition-colors cursor-pointer px-4 py-2 border-2 border-slate-700"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
[{t('blog')}]
|
||||
</Link>
|
||||
<div className="flex items-center gap-4 px-4 py-2">
|
||||
<ThemeToggle />
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export function TagCloud({ tags }: TagCloudProps) {
|
||||
hover:text-cyan-400
|
||||
transition-colors
|
||||
`}
|
||||
title={t('postsWithTag', {count: tag.count, tag: tag.name})}
|
||||
title={t('postsWithTag', { count: tag.count, tag: tag.name })}
|
||||
>
|
||||
#{tag.name}
|
||||
</Link>
|
||||
|
||||
43
components/effects/glitch-button.tsx
Normal file
43
components/effects/glitch-button.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface GlitchButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
children: React.ReactNode
|
||||
variant?: 'default' | 'subtle'
|
||||
glitchColor?: 'cyan' | 'pink' | 'purple' | 'magenta'
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function GlitchButton({
|
||||
children,
|
||||
variant = 'default',
|
||||
glitchColor = 'cyan',
|
||||
disabled = false,
|
||||
className,
|
||||
...props
|
||||
}: GlitchButtonProps) {
|
||||
const glitchClasses = !disabled
|
||||
? cn('glitch-btn-cyber', variant === 'subtle' && 'glitch-btn-subtle', 'relative')
|
||||
: ''
|
||||
|
||||
const overlayColorClass = {
|
||||
cyan: '',
|
||||
pink: 'glitch-overlay-pink',
|
||||
purple: 'glitch-overlay-purple',
|
||||
magenta: 'glitch-overlay-magenta',
|
||||
}[glitchColor]
|
||||
|
||||
return (
|
||||
<button className={cn(glitchClasses, className)} disabled={disabled} {...props}>
|
||||
{children}
|
||||
|
||||
{!disabled && (
|
||||
<div className={cn('glitch-overlay', overlayColorClass)} aria-hidden="true">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
'use client';
|
||||
'use client'
|
||||
|
||||
import {useLocale} from 'next-intl';
|
||||
import {useRouter, usePathname} from '@/i18n/navigation';
|
||||
import {routing} from '@/i18n/routing';
|
||||
import {useState} from 'react';
|
||||
import { useLocale } from 'next-intl'
|
||||
import { useRouter, usePathname } from '@/i18n/navigation'
|
||||
import { routing } from '@/i18n/routing'
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function LanguageSwitcher() {
|
||||
const locale = useLocale();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const locale = useLocale()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const handleLocaleChange = (newLocale: string) => {
|
||||
router.replace(pathname, {locale: newLocale});
|
||||
router.refresh();
|
||||
setIsOpen(false);
|
||||
};
|
||||
router.replace(pathname, { locale: newLocale })
|
||||
router.refresh()
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative z-[100]">
|
||||
@@ -36,9 +36,8 @@ export default function LanguageSwitcher() {
|
||||
className={`
|
||||
w-full text-left px-4 py-2 font-mono uppercase text-xs
|
||||
border-b border-slate-700 last:border-b-0
|
||||
${locale === loc
|
||||
? 'bg-cyan-900 text-cyan-300'
|
||||
: 'text-slate-400 hover:bg-slate-800'
|
||||
${
|
||||
locale === loc ? 'bg-cyan-900 text-cyan-300' : 'text-slate-400 hover:bg-slate-800'
|
||||
}
|
||||
`}
|
||||
>
|
||||
@@ -48,12 +47,7 @@ export default function LanguageSwitcher() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{isOpen && <div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
99
components/layout/hero-header.tsx
Normal file
99
components/layout/hero-header.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Link } from '@/i18n/navigation'
|
||||
import Image from 'next/image'
|
||||
import { ThemeToggle } from '@/components/theme-toggle'
|
||||
import { GlitchButton } from '@/components/effects/glitch-button'
|
||||
import LanguageSwitcher from './LanguageSwitcher'
|
||||
import { useLocale, useTranslations } from 'next-intl'
|
||||
|
||||
export function HeroHeader() {
|
||||
const locale = useLocale()
|
||||
const t = useTranslations('Home')
|
||||
const tNav = useTranslations('Navigation')
|
||||
|
||||
const terminalVersion = t('terminalVersion')
|
||||
const blogLabel = tNav('blog')
|
||||
const aboutLabel = tNav('about')
|
||||
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
useEffect(() => {
|
||||
const checkMobile = () => setIsMobile(window.innerWidth < 768)
|
||||
checkMobile()
|
||||
window.addEventListener('resize', checkMobile)
|
||||
return () => window.removeEventListener('resize', checkMobile)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="mb-8 border-b-2 border-slate-300 dark:border-slate-800 pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Image src="/logo.png" alt="Logo" width={32} height={32} className="opacity-80" />
|
||||
<span className="font-mono text-xs text-slate-500 uppercase tracking-widest">
|
||||
{terminalVersion}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{!isMobile && (
|
||||
<div className="flex gap-4 items-center">
|
||||
<Link
|
||||
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"
|
||||
>
|
||||
[{blogLabel}]
|
||||
</Link>
|
||||
<Link
|
||||
href="/about"
|
||||
className="font-mono text-xs text-slate-600 dark:text-slate-400 uppercase tracking-wider hover:text-cyan-600 dark:hover:text-cyan-400"
|
||||
>
|
||||
[{aboutLabel}]
|
||||
</Link>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isMobile && (
|
||||
<div>
|
||||
<GlitchButton
|
||||
variant="subtle"
|
||||
glitchColor="cyan"
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
className="font-mono text-xs uppercase tracking-wider px-3 py-2 border-2 border-slate-400 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300"
|
||||
aria-label="Toggle menu"
|
||||
aria-expanded={isMobileMenuOpen}
|
||||
>
|
||||
// {isMobileMenuOpen ? 'X' : 'MENU'}
|
||||
</GlitchButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isMobileMenuOpen && isMobile && (
|
||||
<div className="mt-4 pt-4 border-t-2 border-slate-300 dark:border-slate-800">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Link
|
||||
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 px-3 py-2 border-2 border-slate-300 dark:border-slate-700"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
[{blogLabel}]
|
||||
</Link>
|
||||
<Link
|
||||
href="/about"
|
||||
className="font-mono text-xs text-slate-600 dark:text-slate-400 uppercase tracking-wider hover:text-cyan-600 dark:hover:text-cyan-400 px-3 py-2 border-2 border-slate-300 dark:border-slate-700"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
[{aboutLabel}]
|
||||
</Link>
|
||||
<div className="flex items-center gap-4 px-4 py-2">
|
||||
<ThemeToggle />
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -24,7 +24,7 @@ Well, yes, there are. But I believe that sharing some of my opinions and experie
|
||||
|
||||
## Why self-host?
|
||||
|
||||

|
||||

|
||||
|
||||
Now, let's talk about why I chose to self-host this blog. In a nutshell, self-hosting gave me:
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
**Problem Solved:** Next.js 16 requires `NEXT_PUBLIC_*` variables available during `npm run build` for:
|
||||
|
||||
- SEO metadata (`metadataBase`)
|
||||
- Sitemap generation
|
||||
- OpenGraph URLs
|
||||
@@ -19,6 +20,7 @@ This guide documents the configuration for build-time environment variables in t
|
||||
### 1. `.gitea/workflows/main.yml`
|
||||
|
||||
**Changes:**
|
||||
|
||||
- Added step to create `.env` from Gitea secrets (after checkout)
|
||||
- Added cleanup step to remove `.env` after Docker push
|
||||
|
||||
@@ -55,6 +57,7 @@ This guide documents the configuration for build-time environment variables in t
|
||||
### 2. `Dockerfile.nextjs`
|
||||
|
||||
**Changes:**
|
||||
|
||||
- Added `COPY .env* ./` in builder stage (after copying node_modules, before copying source code)
|
||||
|
||||
**Added Section:**
|
||||
@@ -73,6 +76,7 @@ COPY .env* ./
|
||||
### 3. `.dockerignore`
|
||||
|
||||
**Changes:**
|
||||
|
||||
- Modified to allow `.env` file (created by CI/CD) while excluding other `.env*` files
|
||||
|
||||
**Updated Section:**
|
||||
@@ -85,6 +89,7 @@ COPY .env* ./
|
||||
```
|
||||
|
||||
**Explanation:**
|
||||
|
||||
- `.env*` excludes all environment files
|
||||
- `!.env` creates exception for main `.env` (from CI/CD)
|
||||
- `.env.local`, `.env.development`, `.env.production.local` remain excluded
|
||||
@@ -99,11 +104,12 @@ Navigate to: **Repository Settings → Secrets**
|
||||
|
||||
Add the following secret:
|
||||
|
||||
| Secret Name | Value | Type | Description |
|
||||
|------------|-------|------|-------------|
|
||||
| Secret Name | Value | Type | Description |
|
||||
| ---------------------- | ------------------------ | ------------------ | ------------------- |
|
||||
| `NEXT_PUBLIC_SITE_URL` | `https://yourdomain.com` | Secret or Variable | Production site URL |
|
||||
|
||||
**Notes:**
|
||||
|
||||
- Can be configured as **Secret** (masked in logs) or **Variable** (visible in logs)
|
||||
- Recommended: Use **Variable** since it's a public URL
|
||||
- For sensitive values (API keys), always use **Secret**
|
||||
@@ -113,12 +119,14 @@ Add the following secret:
|
||||
To add more build-time variables:
|
||||
|
||||
1. **Add to Gitea Secrets/Variables:**
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
|
||||
NEXT_PUBLIC_API_URL=https://api.example.com
|
||||
```
|
||||
|
||||
2. **Update workflow `.env` creation step:**
|
||||
|
||||
```yaml
|
||||
cat > .env << EOF
|
||||
NEXT_PUBLIC_SITE_URL=${{ secrets.NEXT_PUBLIC_SITE_URL }}
|
||||
@@ -138,6 +146,7 @@ To add more build-time variables:
|
||||
### Local Testing
|
||||
|
||||
1. **Create test `.env` file:**
|
||||
|
||||
```bash
|
||||
cat > .env << EOF
|
||||
NEXT_PUBLIC_SITE_URL=http://localhost:3030
|
||||
@@ -147,11 +156,13 @@ To add more build-time variables:
|
||||
```
|
||||
|
||||
2. **Build Docker image:**
|
||||
|
||||
```bash
|
||||
docker build -t mypage:test -f Dockerfile.nextjs .
|
||||
```
|
||||
|
||||
3. **Verify variable is embedded (should show "NOT FOUND" - correct behavior):**
|
||||
|
||||
```bash
|
||||
docker run --rm mypage:test node -e "console.log(process.env.NEXT_PUBLIC_SITE_URL || 'NOT FOUND')"
|
||||
```
|
||||
@@ -161,6 +172,7 @@ To add more build-time variables:
|
||||
**Why?** `NEXT_PUBLIC_*` variables are embedded in JavaScript bundle during build, NOT available as runtime environment variables.
|
||||
|
||||
4. **Test application starts:**
|
||||
|
||||
```bash
|
||||
docker run --rm -p 3030:3030 mypage:test
|
||||
```
|
||||
@@ -214,10 +226,10 @@ To add more build-time variables:
|
||||
|
||||
### 🔒 Sensitive Data Guidelines
|
||||
|
||||
| Type | Use For | Access |
|
||||
|------|---------|--------|
|
||||
| `NEXT_PUBLIC_*` | Client-side config (URLs, feature flags) | Public (embedded in JS bundle) |
|
||||
| `SECRET_*` | Server-side secrets (API keys, DB passwords) | Private (runtime only) |
|
||||
| Type | Use For | Access |
|
||||
| --------------- | -------------------------------------------- | ------------------------------ |
|
||||
| `NEXT_PUBLIC_*` | Client-side config (URLs, feature flags) | Public (embedded in JS bundle) |
|
||||
| `SECRET_*` | Server-side secrets (API keys, DB passwords) | Private (runtime only) |
|
||||
|
||||
---
|
||||
|
||||
@@ -226,10 +238,12 @@ To add more build-time variables:
|
||||
### Issue: Variables not available during build
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Next.js build errors about missing `NEXT_PUBLIC_SITE_URL`
|
||||
- Metadata/sitemap generation fails
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Verify `NEXT_PUBLIC_SITE_URL` secret exists in Gitea
|
||||
- Check workflow logs for `.env` creation step
|
||||
- Ensure `.env` file is created BEFORE Docker build
|
||||
@@ -237,9 +251,11 @@ To add more build-time variables:
|
||||
### Issue: Variables not working in application
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- URLs show as `undefined` or `null` in production
|
||||
|
||||
**Diagnosis:**
|
||||
|
||||
```bash
|
||||
# Check if variable is in bundle (should work):
|
||||
curl https://yourdomain.com | grep -o 'NEXT_PUBLIC_SITE_URL'
|
||||
@@ -249,6 +265,7 @@ docker exec mypage-prod node -e "console.log(process.env.NEXT_PUBLIC_SITE_URL)"
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Verify `.env` was copied during Docker build
|
||||
- Check Dockerfile logs for `COPY .env* ./` step
|
||||
- Rebuild with `--no-cache` if needed
|
||||
@@ -256,9 +273,11 @@ docker exec mypage-prod node -e "console.log(process.env.NEXT_PUBLIC_SITE_URL)"
|
||||
### Issue: `.env` file not found during Docker build
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Docker build warning: `COPY .env* ./` - no files matched
|
||||
|
||||
**Solution:**
|
||||
|
||||
- Check `.dockerignore` allows `.env` file
|
||||
- Verify workflow creates `.env` BEFORE Docker build
|
||||
- Check file exists: `ls -la .env` in workflow
|
||||
@@ -289,6 +308,7 @@ After deploying changes:
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
|
||||
1. Check workflow logs in Gitea Actions
|
||||
2. Review Docker build logs
|
||||
3. Verify Gitea secrets configuration
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -12,7 +12,7 @@ export default [
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||
{ argsIgnorePattern: '^_|node', varsIgnorePattern: '^_' },
|
||||
],
|
||||
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||
},
|
||||
@@ -26,6 +26,7 @@ export default [
|
||||
'dist/',
|
||||
'.cache/',
|
||||
'*.config.js',
|
||||
'next.config.analyzer.js',
|
||||
'public/',
|
||||
'coverage/',
|
||||
],
|
||||
|
||||
11
fix.js
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
|
||||
*/
|
||||
|
||||
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[] = []
|
||||
|
||||
@@ -85,7 +85,7 @@ async function copyAndRewritePath(node: ImageNode, options: Options): Promise<vo
|
||||
node.url = publicUrl + queryParams
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Stat failed, proceed with copy
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
export async function getRelatedTags(tagSlug: string, locale: string = 'en', limit = 5): Promise<TagInfo[]> {
|
||||
export async function getRelatedTags(
|
||||
tagSlug: string,
|
||||
locale: string = 'en',
|
||||
limit = 5
|
||||
): Promise<TagInfo[]> {
|
||||
const posts = await getPostsByTag(tagSlug, locale)
|
||||
const relatedTagMap = new Map<string, number>()
|
||||
|
||||
@@ -107,7 +111,9 @@ export function validateTags(tags: any): string[] {
|
||||
return validTags
|
||||
}
|
||||
|
||||
export async function getTagCloud(locale: string = 'en'): Promise<Array<TagInfo & { size: 'sm' | 'md' | 'lg' | 'xl' }>> {
|
||||
export async function getTagCloud(
|
||||
locale: string = 'en'
|
||||
): Promise<Array<TagInfo & { size: 'sm' | 'md' | 'lg' | 'xl' }>> {
|
||||
const tags = await getAllTags(locale)
|
||||
if (tags.length === 0) return []
|
||||
|
||||
|
||||
@@ -79,3 +79,7 @@ export function generateSlug(title: string): string {
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
}
|
||||
|
||||
export function cn(...inputs: (string | undefined | null | false)[]): string {
|
||||
return inputs.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
@@ -3,21 +3,21 @@
|
||||
"siteTitle": "Personal Blog",
|
||||
"siteDescription": "Thoughts on technology and development"
|
||||
},
|
||||
|
||||
"Navigation": {
|
||||
"home": "Home",
|
||||
"blog": "Blog",
|
||||
"tags": "Tags",
|
||||
"about": "About"
|
||||
},
|
||||
|
||||
"Breadcrumbs": {
|
||||
"home": "Home",
|
||||
"blog": "Blog",
|
||||
"tags": "Tags",
|
||||
"about": "About"
|
||||
"about": "About",
|
||||
"tech": "Technology",
|
||||
"design": "Design",
|
||||
"tutorial": "Tutorials"
|
||||
},
|
||||
|
||||
"Home": {
|
||||
"terminalVersion": "TERMINAL:// V2.0",
|
||||
"documentLevel": "DOCUMENT LEVEL-1 //",
|
||||
@@ -32,7 +32,6 @@
|
||||
"seePostsButton": "[SEE POSTS] >>",
|
||||
"seeAllTagsButton": "[SEE ALL TAGS] >>"
|
||||
},
|
||||
|
||||
"BlogListing": {
|
||||
"title": "Blog",
|
||||
"subtitle": "Latest articles and thoughts",
|
||||
@@ -48,7 +47,6 @@
|
||||
"prev": "< PREV",
|
||||
"next": "NEXT >"
|
||||
},
|
||||
|
||||
"BlogPost": {
|
||||
"readMore": "Read more",
|
||||
"readingTime": "{minutes} min read",
|
||||
@@ -58,7 +56,6 @@
|
||||
"relatedPosts": "Related Posts",
|
||||
"sharePost": "Share this post"
|
||||
},
|
||||
|
||||
"Tags": {
|
||||
"title": "Tags",
|
||||
"subtitle": "Browse by topic",
|
||||
@@ -67,7 +64,6 @@
|
||||
"relatedTags": "Related tags",
|
||||
"quickNav": "Quick navigation"
|
||||
},
|
||||
|
||||
"About": {
|
||||
"title": "About",
|
||||
"subtitle": "Learn more about me",
|
||||
@@ -119,13 +115,11 @@
|
||||
"techStackSelfHostingText": "Home lab, privacy-focused services, full control, Git server",
|
||||
"contactTitle": "> CONTACT"
|
||||
},
|
||||
|
||||
"NotFound": {
|
||||
"title": "Page Not Found",
|
||||
"description": "The page you're looking for doesn't exist",
|
||||
"goHome": "Go to homepage"
|
||||
},
|
||||
|
||||
"LanguageSwitcher": {
|
||||
"switchLanguage": "Switch language",
|
||||
"currentLanguage": "Current language"
|
||||
|
||||
@@ -13,15 +13,18 @@
|
||||
"home": "Acasă",
|
||||
"blog": "Blog",
|
||||
"tags": "Etichete",
|
||||
"about": "Despre"
|
||||
"about": "Despre",
|
||||
"tech": "Tehnologie",
|
||||
"design": "Design",
|
||||
"tutorial": "Tutoriale"
|
||||
},
|
||||
"Home": {
|
||||
"terminalVersion": "TERMINAL:// V2.0",
|
||||
"documentLevel": "DOCUMENT LEVEL-1 //",
|
||||
"heroTitle": "BUILD. WRITE. SHARE.",
|
||||
"heroSubtitle": "> Explore ideas",
|
||||
"checkPostsButton": "[CHECK POSTS]",
|
||||
"aboutMeButton": "[ABOUT ME]",
|
||||
"checkPostsButton": "[POSTĂRI]",
|
||||
"aboutMeButton": "[DESPRE]",
|
||||
"recentEntriesLabel": "ARCHIVE ACCESS // RECENT ENTRIES",
|
||||
"recentEntriesTitle": "> RECENT ENTRIES",
|
||||
"fileLabel": "FILE#{number} // {category}",
|
||||
|
||||
@@ -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|.*\\..*).*)'],
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import {getRequestConfig} from 'next-intl/server';
|
||||
import {routing} from './routing';
|
||||
import { getRequestConfig } from 'next-intl/server'
|
||||
import { routing } from './routing'
|
||||
|
||||
export default getRequestConfig(async ({requestLocale}) => {
|
||||
let locale = await requestLocale;
|
||||
export default getRequestConfig(async ({ requestLocale }) => {
|
||||
let locale = await requestLocale
|
||||
|
||||
if (!locale || !routing.locales.includes(locale as any)) {
|
||||
locale = routing.defaultLocale;
|
||||
locale = routing.defaultLocale
|
||||
}
|
||||
|
||||
return {
|
||||
locale,
|
||||
messages: (await import(`../../messages/${locale}.json`)).default
|
||||
};
|
||||
});
|
||||
messages: (await import(`../../messages/${locale}.json`)).default,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -23,12 +19,8 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
],
|
||||
"@/i18n/*": [
|
||||
"./src/i18n/*"
|
||||
]
|
||||
"@/*": ["./*"],
|
||||
"@/i18n/*": ["./src/i18n/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
@@ -38,7 +30,5 @@
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
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 {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
interface IntlMessages extends Messages {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user