Prod ready, heavily optimized images with automatic scripts

This commit is contained in:
AlacrisDevs
2026-03-23 10:08:41 +02:00
parent b604442c46
commit 792f14be3e
33 changed files with 210 additions and 65 deletions

105
README.md
View File

@@ -1,46 +1,83 @@
# 🎮 TIPILAN # TipiLAN
**TIPILAN** is the official web platform for the TipiLAN LAN event — built using [Next.js](https://nextjs.org) and powered by the lightning-fast [Bun](https://bun.sh) runtime. Official website for **TipiLAN** — Estonia's largest student-organised LAN event.
> ⚠️ This is a work in progress! Contributions welcome. > Work in progress. Event date: **September 11, 2026**.
--- ---
## 🚀 Features ## Tech Stack
- Event information & schedule - [Next.js 16](https://nextjs.org/) — App Router, SSG, Turbopack
- Stream - [React 19](https://react.dev/)
- Responsive dark-themed UI - [TypeScript 5](https://www.typescriptlang.org/)
- Built with Next.js, styled with Tailwind (if applicable), powered by Bun - [Tailwind CSS 4](https://tailwindcss.com/)
- [next-intl](https://next-intl.dev/) — ET / EN localisation
- [react-icons](https://react-icons.github.io/react-icons/)
Requires **Node.js ≥ 22**.
--- ---
## 📦 Tech Stack ## Quickstart
- [Next.js](https://nextjs.org/)
- [Bun](https://bun.sh/)
- [TypeScript](https://www.typescriptlang.org/)
- [React](https://reactjs.org/)
- [Tailwind CSS](https://tailwindcss.com/) *(if used)*
---
## 🛠️ Setup Instructions
📖 See [`docs/SETUP.md`](./docs/SETUP.md) for a full setup guide on:
- ✅ Windows (native)
- 🐧 Windows with WSL
- 🐧 Linux (Ubuntu, Arch, Fedora, etc.)
---
## 🧪 Scripts
```bash ```bash
bun dev # Run development server npm install
bun run build # Build for production npm run dev
bun start # Start production server ```
bun test # Run tests (if configured) Open [http://localhost:3000](http://localhost:3000) — redirects to `/et` by default.
bun run lint # Run linter (if configured)
---
## Scripts
```bash
npm run dev # Dev server (Turbopack, localhost:3000)
npm run build # Production build (outputs SSG for /et and /en)
npm start # Serve production build
npm run lint:fix # ESLint with auto-fix
```
---
## Project Structure
```
src/
app/[locale]/ # Locale-based pages (et, en)
components/teaser/ # HeroSection, CarouselSection, EndSection, Footer
components/ # LanguageSwitcher, TeaserPage
i18n/ # next-intl routing + request config
lib/ # blurPlaceholders.ts (auto-generated)
proxy.ts # next-intl routing middleware
translations/ # et.json, en.json
public/images/
backgrounds/ # Full-viewport background WebPs
heros/ # Character/hero image WebPs
flags/ # Language switcher SVGs
original/ # Original PNG sources (not served)
scripts/ # Image processing utilities
```
---
## Image Utilities
When adding or replacing images, re-run:
```bash
# Convert new PNGs → WebP (quality 85)
node scripts/convert-images.mjs
# Regenerate blur placeholders for all images
node scripts/gen-blur-placeholders.mjs
```
---
## Localisation
Translation files live in `translations/et.json` and `translations/en.json`.
Routing is handled by `src/proxy.ts``/` redirects to `/et` (default locale).

View File

@@ -3,6 +3,10 @@ import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts"); const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
const nextConfig: NextConfig = {}; const nextConfig: NextConfig = {
images: {
qualities: [75, 90],
},
};
export default withNextIntl(nextConfig); export default withNextIntl(nextConfig);

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 574 B

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

Before

Width:  |  Height:  |  Size: 1015 KiB

After

Width:  |  Height:  |  Size: 1015 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -0,0 +1,27 @@
import sharp from 'sharp';
import { readdir, unlink } from 'fs/promises';
import { join, extname, basename } from 'path';
const INPUT_DIR = 'public/images';
const QUALITY = 85;
const files = await readdir(INPUT_DIR);
const pngs = files.filter(f => extname(f).toLowerCase() === '.png');
console.log(`Converting ${pngs.length} PNG(s) to WebP at quality ${QUALITY}...\n`);
for (const file of pngs) {
const input = join(INPUT_DIR, file);
const output = join(INPUT_DIR, basename(file, '.png') + '.webp');
const { width, height, size: inSize } = await sharp(input).metadata();
await sharp(input).webp({ quality: QUALITY }).toFile(output);
const { size: outSize } = await sharp(output).metadata();
const saved = (((inSize - outSize) / inSize) * 100).toFixed(1);
console.log(`${file} (${width}x${height})`);
console.log(` ${(inSize/1024).toFixed(0)} KB → ${(outSize/1024).toFixed(0)} KB (${saved}% smaller)\n`);
}
console.log('Done.');

View File

@@ -0,0 +1,43 @@
import sharp from 'sharp';
import { readdir, writeFile } from 'fs/promises';
import { join, extname, basename } from 'path';
const INPUT_DIR = 'public/images';
const OUTPUT_FILE = 'src/lib/blurPlaceholders.ts';
const THUMB_WIDTH = 20;
async function processDir(dir, prefix = '') {
const entries = await readdir(dir, { withFileTypes: true });
const result = {};
for (const entry of entries) {
if (entry.name === 'original') continue;
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
const sub = await processDir(fullPath, prefix + entry.name + '/');
Object.assign(result, sub);
} else if (['.webp', '.png', '.jpg'].includes(extname(entry.name).toLowerCase())) {
const buffer = await sharp(fullPath)
.resize(THUMB_WIDTH)
.webp({ quality: 30 })
.toBuffer();
const b64 = `data:image/webp;base64,${buffer.toString('base64')}`;
const key = prefix + basename(entry.name, extname(entry.name));
result[key] = b64;
console.log(` ${key}${b64.length} chars`);
}
}
return result;
}
console.log('Generating blur placeholders...\n');
const placeholders = await processDir(INPUT_DIR);
const lines = Object.entries(placeholders)
.map(([k, v]) => ` "${k}": "${v}",`)
.join('\n');
const output = `// Auto-generated by scripts/gen-blur-placeholders.mjs — do not edit manually\nexport const BLUR_PLACEHOLDERS: Record<string, string> = {\n${lines}\n};\n`;
await writeFile(OUTPUT_FILE, output, 'utf8');
console.log(`\nWrote ${Object.keys(placeholders).length} placeholders to ${OUTPUT_FILE}`);

View File

@@ -1,5 +1,10 @@
import { NextIntlClientProvider } from "next-intl"; import { NextIntlClientProvider } from "next-intl";
import { setRequestLocale, getMessages } from "next-intl/server"; import { setRequestLocale, getMessages } from "next-intl/server";
import { routing } from "@/i18n/routing";
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export default async function LocaleLayout({ export default async function LocaleLayout({
children, children,

View File

@@ -28,7 +28,7 @@ export default function LanguageSwitcher() {
aria-label="Switch language" aria-label="Switch language"
> >
<Image <Image
src={`/images/flag-${locale === "et" ? "en" : "et"}.svg`} src={`/images/flags/flag-${locale === "et" ? "en" : "et"}.svg`}
alt={locale === "et" ? "Switch to English" : "Vaheta eesti keelele"} alt={locale === "et" ? "Switch to English" : "Vaheta eesti keelele"}
width={40} width={40}
height={30} height={30}

View File

@@ -4,6 +4,11 @@ import { useEffect, useState } from "react";
import Image from "next/image"; import Image from "next/image";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { SLIDES, type CarouselSlide } from "./constants"; import { SLIDES, type CarouselSlide } from "./constants";
import { BLUR_PLACEHOLDERS } from "@/lib/blurPlaceholders";
function blurKey(src: string) {
return src.replace('/images/', '').replace(/\.\w+$/, '');
}
function CarouselSlideComponent({ function CarouselSlideComponent({
slide, slide,
@@ -23,13 +28,16 @@ function CarouselSlideComponent({
src={slide.bgImage} src={slide.bgImage}
alt="" alt=""
fill fill
unoptimized
placeholder="blur"
blurDataURL={BLUR_PLACEHOLDERS[blurKey(slide.bgImage)]}
sizes="100vw" sizes="100vw"
className="object-cover pointer-events-none" className="object-cover pointer-events-none"
/> />
{/* Content — top padding accounts for the floating heading */} {/* Content — top padding accounts for the floating heading */}
<div <div
className={`relative z-[1] flex h-full gap-8 xl:gap-16 pt-[120px] px-8 xl:px-16 ${isLeft ? "items-end" : "items-end flex-row-reverse" className={`relative z-1 flex h-full gap-8 xl:gap-16 pt-[120px] px-8 xl:px-16 ${isLeft ? "items-end" : "items-end flex-row-reverse"
}`} }`}
> >
{/* Text content */} {/* Text content */}
@@ -51,11 +59,15 @@ function CarouselSlideComponent({
transform: isActive ? "translateY(0)" : "translateY(110%)", transform: isActive ? "translateY(0)" : "translateY(110%)",
}} }}
> >
{/* eslint-disable-next-line @next/next/no-img-element */} <Image
<img
src={slide.heroImage} src={slide.heroImage}
alt="" alt=""
className="absolute inset-0 w-full h-full object-cover" fill
unoptimized
placeholder="blur"
blurDataURL={BLUR_PLACEHOLDERS[blurKey(slide.heroImage)]}
sizes="(min-width: 1280px) 40vw, 35vw"
className="object-cover"
/> />
</div> </div>
</div> </div>
@@ -102,15 +114,18 @@ export default function CarouselSection({ sectionRef }: CarouselSectionProps) {
{/* Mobile: stacked slides, no hero images, no transitions */} {/* Mobile: stacked slides, no hero images, no transitions */}
<div className="lg:hidden"> <div className="lg:hidden">
{SLIDES.map((slide, i) => ( {SLIDES.map((slide, i) => (
<div key={i} className="relative w-full overflow-hidden border-t-4 border-primary-50"> <div key={i} className="relative w-full h-[60vw] min-h-[320px] overflow-hidden border-t-4 border-primary-50">
<Image <Image
src={slide.bgImage} src={slide.bgImage}
alt="" alt=""
fill fill
unoptimized
placeholder="blur"
blurDataURL={BLUR_PLACEHOLDERS[blurKey(slide.bgImage)]}
sizes="100vw" sizes="100vw"
className="object-cover pointer-events-none" className="object-cover pointer-events-none"
/> />
<div className="relative z-[1] flex flex-col gap-4 justify-end h-full p-6 pb-12"> <div className="relative z-1 flex flex-col gap-4 justify-end h-full p-6 pb-12">
<h3 className="text-h1 text-text-light text-shadow-teaser">{t(slide.titleKey)}</h3> <h3 className="text-h1 text-text-light text-shadow-teaser">{t(slide.titleKey)}</h3>
<p className="text-p-lg text-text-light max-w-[500px]">{t(slide.descKey)}</p> <p className="text-p-lg text-text-light max-w-[500px]">{t(slide.descKey)}</p>
</div> </div>

View File

@@ -3,6 +3,7 @@
import Image from "next/image"; import Image from "next/image";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useCountUp } from "@/hooks/useCountUp"; import { useCountUp } from "@/hooks/useCountUp";
import { BLUR_PLACEHOLDERS } from "@/lib/blurPlaceholders";
function formatK(n: number): string { function formatK(n: number): string {
if (n < 1000) return String(n); if (n < 1000) return String(n);
@@ -50,13 +51,16 @@ export default function EndSection() {
{/* Tickets side */} {/* Tickets side */}
<div className="relative w-1/2 overflow-hidden min-h-dvh"> <div className="relative w-1/2 overflow-hidden min-h-dvh">
<Image <Image
src="/images/tickets_teaser.png" src="/images/backgrounds/tickets_teaser.webp"
alt="" alt=""
fill fill
unoptimized
placeholder="blur"
blurDataURL={BLUR_PLACEHOLDERS["backgrounds/tickets_teaser"]}
sizes="(min-width: 1280px) 50vw, 100vw" sizes="(min-width: 1280px) 50vw, 100vw"
className="object-cover pointer-events-none" className="object-cover pointer-events-none"
/> />
<div className="relative z-[1] flex flex-col items-center justify-center h-full gap-8 xl:gap-16 2xl:gap-32 p-6 lg:p-12 xl:p-16"> <div className="relative z-1 flex flex-col items-center justify-center h-full gap-8 xl:gap-16 2xl:gap-32 p-6 lg:p-12 xl:p-16">
{/* Ticket stats */} {/* Ticket stats */}
<div className="flex gap-6 xl:gap-12 text-center text-text-light"> <div className="flex gap-6 xl:gap-12 text-center text-text-light">
<CountUpStat end={0} suffix="€" label={t("teaser.tickets.earlyVisitor")} /> <CountUpStat end={0} suffix="€" label={t("teaser.tickets.earlyVisitor")} />
@@ -80,13 +84,16 @@ export default function EndSection() {
{/* Sponsors side */} {/* Sponsors side */}
<div className="relative w-1/2 overflow-hidden min-h-dvh"> <div className="relative w-1/2 overflow-hidden min-h-dvh">
<Image <Image
src="/images/sponsors_teaser.png" src="/images/backgrounds/sponsors_teaser.webp"
alt="" alt=""
fill fill
unoptimized
placeholder="blur"
blurDataURL={BLUR_PLACEHOLDERS["backgrounds/sponsors_teaser"]}
sizes="(min-width: 1280px) 50vw, 100vw" sizes="(min-width: 1280px) 50vw, 100vw"
className="object-cover pointer-events-none" className="object-cover pointer-events-none"
/> />
<div className="relative z-[1] flex flex-col items-center justify-center h-full gap-8 xl:gap-16 2xl:gap-32 p-6 lg:p-12 xl:p-16"> <div className="relative z-1 flex flex-col items-center justify-center h-full gap-8 xl:gap-16 2xl:gap-32 p-6 lg:p-12 xl:p-16">
{/* Sponsor stats */} {/* Sponsor stats */}
<div className="flex gap-6 xl:gap-12 text-center text-text-light"> <div className="flex gap-6 xl:gap-12 text-center text-text-light">
<CountUpStat end={900} suffix="+" label={t("teaser.sponsors.visitors")} /> <CountUpStat end={900} suffix="+" label={t("teaser.sponsors.visitors")} />

View File

@@ -16,7 +16,7 @@ export default function Footer() {
const t = useTranslations(); const t = useTranslations();
return ( return (
<footer className="bg-bg-dark w-full border-t-4 border-primary-50"> <footer className="bg-bg-dark w-full border-t-4 border-primary-50 lg:border-t-0">
{/* Mobile: only social icons */} {/* Mobile: only social icons */}
<div className="flex lg:hidden items-center justify-center gap-6 py-8 px-6 flex-wrap"> <div className="flex lg:hidden items-center justify-center gap-6 py-8 px-6 flex-wrap">
{SOCIAL_LINKS.map(({ icon: Icon, href, label }) => ( {SOCIAL_LINKS.map(({ icon: Icon, href, label }) => (

View File

@@ -5,6 +5,7 @@ import { useTranslations } from "next-intl";
import LanguageSwitcher from "@/components/LanguageSwitcher"; import LanguageSwitcher from "@/components/LanguageSwitcher";
import { useCountdown } from "@/hooks/useCountdown"; import { useCountdown } from "@/hooks/useCountdown";
import { EVENT_DATE } from "./constants"; import { EVENT_DATE } from "./constants";
import { BLUR_PLACEHOLDERS } from "@/lib/blurPlaceholders";
interface HeroSectionProps { interface HeroSectionProps {
onScrollDown: () => void; onScrollDown: () => void;
@@ -19,9 +20,12 @@ export default function HeroSection({ onScrollDown }: HeroSectionProps) {
<section className="relative h-dvh w-full overflow-hidden"> <section className="relative h-dvh w-full overflow-hidden">
{/* Background */} {/* Background */}
<Image <Image
src="/images/hero_teaser.png" src="/images/backgrounds/hero_teaser.webp"
alt="" alt=""
fill fill
unoptimized
placeholder="blur"
blurDataURL={BLUR_PLACEHOLDERS["backgrounds/hero_teaser"]}
className="object-cover pointer-events-none" className="object-cover pointer-events-none"
sizes="100vw" sizes="100vw"
priority priority

View File

@@ -13,22 +13,22 @@ export const SLIDES: CarouselSlide[] = [
{ {
titleKey: "teaser.compete.title", titleKey: "teaser.compete.title",
descKey: "teaser.compete.description", descKey: "teaser.compete.description",
bgImage: "/images/compete_teaser.png", bgImage: "/images/backgrounds/compete_teaser.webp",
heroImage: "/images/compete_hero.png", heroImage: "/images/heros/compete_hero.webp",
layout: "left", layout: "left",
}, },
{ {
titleKey: "teaser.play.title", titleKey: "teaser.play.title",
descKey: "teaser.play.description", descKey: "teaser.play.description",
bgImage: "/images/play_teaser.png", bgImage: "/images/backgrounds/play_teaser.webp",
heroImage: "/images/play_hero.png", heroImage: "/images/heros/play_hero.webp",
layout: "right", layout: "right",
}, },
{ {
titleKey: "teaser.explore.title", titleKey: "teaser.explore.title",
descKey: "teaser.explore.description", descKey: "teaser.explore.description",
bgImage: "/images/explore_teaser.png", bgImage: "/images/backgrounds/explore_teaser.webp",
heroImage: "/images/explore_hero.png", heroImage: "/images/heros/explore_hero.webp",
layout: "left", layout: "left",
}, },
]; ];

View File

@@ -0,0 +1,12 @@
// Auto-generated by scripts/gen-blur-placeholders.mjs — do not edit manually
export const BLUR_PLACEHOLDERS: Record<string, string> = {
"backgrounds/compete_teaser": "data:image/webp;base64,UklGRkQAAABXRUJQVlA4IDgAAADwAgCdASoUAAsAPxFysFAsJqSisAgBgCIJZQC7AC0kAAD+7wHPoVxbdBCX00WbjgD9Z4tx1CAAAA==",
"backgrounds/explore_teaser": "data:image/webp;base64,UklGRk4AAABXRUJQVlA4IEIAAADQAwCdASoUAAsAPxFysFAsJqSisAgBgCIJZQCdACPtdR6HHnLZHwAA/uk16hD6XlyktfBMGDYcaSECd8XN57v+wAA=",
"backgrounds/hero_teaser": "data:image/webp;base64,UklGRjwAAABXRUJQVlA4IDAAAADwAgCdASoUAAsAPxFysFAsJqSisAgBgCIJZQCw7C0kAAD+7wd2LOZnmb7bPjFHAAA=",
"backgrounds/play_teaser": "data:image/webp;base64,UklGRkYAAABXRUJQVlA4IDoAAAAQAwCdASoUAAsAPxFysFAsJqSisAgBgCIJZQAAetFV2AAA/u7a/rkwOPegln3XZvJv4IXGbjFCgAAA",
"backgrounds/sponsors_teaser": "data:image/webp;base64,UklGRkoAAABXRUJQVlA4ID4AAACwAgCdASoUAAsAPxFysVAsJqSisAgBgCIJZQAAeyAA/u8NKV4mJzYIQ5TdP6QE3PTLq6L/cRRlp5DBY4AAAA==",
"backgrounds/tickets_teaser": "data:image/webp;base64,UklGRkAAAABXRUJQVlA4IDQAAADwAgCdASoUAAsAPxFysFAsJqSisAgBgCIJZQCsADDWAAD+7wOPnse+a27m6AB0gO1dloAA",
"heros/compete_hero": "data:image/webp;base64,UklGRmABAABXRUJQVlA4WAoAAAAQAAAAEwAAFAAAQUxQSLQAAAABgGrbtrJl/y4Nd/fEILkm90wjkUjQIJEZRLdKdHkAonukubsu5JP/ESJiAmTd5ZSpv2d4uM0huduWZ8tMOoHvJqnsA9YDRs0AfVIt8JBglPYJ1En533ARbeQYhGOfFH4KC24jxcNGnksagAIZZ84BbHfFLMHaaLUkR+8Lxjf8f6yR4r+we5WsROz3K/hub0yumVAo7DUUse+hqCAU1SFpCIHD0W3rsyNnaN/OSbtDheN2syRWUDgghgAAANADAJ0BKhQAFQA/EYC4VawoJSMoCAGAIglmAMiYGHsmEwuFEhEkYAD+66HVfxcO3w6PqL2pnKTu3Ng/jKAoDs8naMiWdzhP7r5h1yoBgzUT75Dx8f0hkv9pe7sM/bowsdteFBkLMihe2/KstbbBJ3JDP0lIY68rF2gLM5uQUOPBuAQ6AAAA",
"heros/explore_hero": "data:image/webp;base64,UklGRnoBAABXRUJQVlA4WAoAAAAQAAAAEwAAFAAAQUxQSMcAAAABgGPb2rHn/orVpf6r2OxS22ZpZxC2nbSZwd/GNuZgG++NnvebQkRMADRNoUkWGI794pjJUCu5YayFXDcUsEkeFDtJ/vEREbkf/DtvFiJOKd/aBcvkh8Q1l//QcqDBbtN/UXc615GmfxJudagyAU8bEue02ANMViA9VWmVALWNKAk+F3YOeeUDjDrMEYMUZxfYDSD1wsXpXnKoM18AOWzIe5MeOQwAdqo36lb+MY9R9/HpugwAvJaopLmw9koTADj5+02K5QAAAFZQOCCMAAAA8AQAnQEqFAAVAD8RfLNULCekIygKqYAiCWwAAC6K7ktWusnkjyqNZvLtrf5IXSsgAP7Y10kJQQoMowu9tcKs37WMMYK0+ODOOnN6ITImiWwI2YSiqAiB1HVtiJn751d+7RynAhi3kIDVj/gRpYzPtuFjAKK95QkeE+vkcbEHeYc8lEOzfIUG9z3UwAA=",
"heros/play_hero": "data:image/webp;base64,UklGRpgBAABXRUJQVlA4WAoAAAAQAAAAEwAAFAAAQUxQSNoAAAABgGPbtmnplG3brszKbJuhjSZUaER//KxGRbarWmDbtv3eCd5593chIiYAAECxKsVTS1EO2I15xJ+DydZSbzlFMTcUHWq4qFekgsU4RH7LlUgUIz2JQiaOMyVCpQs7UyfU94c3AdB40/dEIWdL6Y0FHXFfv7+CbjmBRl1Xp8oVPj2gMAOEUkR+nkf631Igv4bMuwoCkLIdOhIhf0z47AAAipo9+MOCh/6gvfiBzM3+OgoQiTLmKACAnyxJckz7PyhZxC8HAACrp03isuA0TsnLBki1CAlpryQPNFZQOCCYAAAAkAQAnQEqFAAVAD8RfrZVrCekoygIAYAiCWMAA+YY8jKFKQn7mN5HXGKuMKLAAPnN+XfRnezaXgxFG0vexSu324H8iTVzaqp6+GAnLm3Z0vYGqKNz9UAFLu/aVjnBei3xNpg0DHr4e2EMCNq6VGTYtaJn7sVI0C/bn2ttJ3P6x68okPFLtX69dR+UTOMhnz1zhE3rAwgAAAA=",
};

View File

@@ -1,17 +1,8 @@
import createMiddleware from 'next-intl/middleware'; import createMiddleware from 'next-intl/middleware';
import { NextRequest } from 'next/server';
import { routing } from './i18n/routing'; import { routing } from './i18n/routing';
const intlMiddleware = createMiddleware(routing); export default createMiddleware(routing);
export function proxy(request: NextRequest) {
return intlMiddleware(request);
}
export const config = { export const config = {
matcher: [ matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
'/',
'/(et|en)/:path*',
'/((?!_next|_vercel|.*\\..*).*)'
]
}; };