← Όλα τα άρθρα

next-intl για ελληνικές ιστοσελίδες: πέντε gotchas

Πέντε παγίδες από δίγλωσση Next.js ιστοσελίδα: hreflang x-default, κανονικοποίηση slug, σειρά middleware, μορφές ημερομηνίας, font CLS.

next-intl για ελληνικές ιστοσελίδες: πέντε gotchas

Έχτισα το klawsfx.com ως δίγλωσση ιστοσελίδα — ελληνικά και αγγλικά — χρησιμοποιώντας το next-intl. Η βιβλιοθήκη είναι καλά τεκμηριωμένη και η εγκατάσταση λειτουργεί. Αυτό που η τεκμηρίωση δεν σε προετοιμάζει είναι το σύνολο από κρυφές παγίδες που εμφανίζονται μόνο όταν τα ελληνικά είναι το μη-προεπιλεγμένο locale και νοιάζεσαι για SEO.

Εδώ είναι τα πέντε πράγματα που μου κόστισαν πραγματικό χρόνο.

1. Το hreflang x-default δείχνει στο λάθος locale από προεπιλογή

Το hreflang λέει στη Google ποια σελίδα να εμφανίσει για ποια περιοχή. Το tag x-default είναι η εναλλακτική — η σελίδα που εμφανίζεται όταν κανένα άλλο hreflang δεν ταιριάζει με το locale του επισκέπτη. Η λογική επιλογή για έναν ελληνο-αγγλικό ιστότοπο είναι τα αγγλικά ως x-default.

Η παγίδα: αν ο ιστότοπός σου έχει σελίδες μόνο στα ελληνικά για συγκεκριμένες διαδρομές — π.χ. έχεις /el/services αλλά όχι /en/services — η βοηθητική συνάρτηση buildAlternates θα δημιουργήσει ένα hreflang x-default που δείχνει στο locale με τη μεγαλύτερη κάλυψη. Αν αυτά είναι τα ελληνικά από προεπιλογή, έχεις πει στη Google να στέλνει μη-ελληνόφωνους χρήστες σε ελληνικές σελίδες. Η πτώση στην οργανική επισκεψιμότητα από αυτό είναι πραγματική και με καθυστέρηση — χρειάζονται εβδομάδες πριν τη δεις στο Search Console.

Η λύση είναι ρητή: όταν κατασκευάζεις tags hreflang, έλεγξε τα availableLocales ανά διαδρομή. Αν τα αγγλικά είναι διαθέσιμα, χρησιμοποίησέ τα για το x-default. Αν όχι, χρησιμοποίησε όποιο locale έχει η σελίδα. Μην κάνεις ποτέ hardcode το defaultLocale ως x-default χωρίς να επαληθεύσεις ότι η διαδρομή υπάρχει σε εκείνο το locale.

function buildAlternates(pathname: string, availableLocales: string[]) {
  const entries = availableLocales.map((locale) => ({
    hrefLang: locale,
    href: `${baseUrl}/${locale}${pathname}`,
  }));

  const xDefaultLocale = availableLocales.includes('en') ? 'en' : availableLocales[0];
  entries.push({
    hrefLang: 'x-default',
    href: `${baseUrl}/${xDefaultLocale}${pathname}`,
  });

  return entries;
}

2. Τα ελληνικά γράμματα σε slugs χρειάζονται ρητή κανονικοποίηση

Τα slugs στα ελληνικά είναι είτε πλήρως ελληνικά γράμματα είτε πλήρης λατινική μεταγραφή — η ανάμειξή τους είναι πρόβλημα συντήρησης. Επέλεξε ένα και εφάρμοσέ το παντού.

Χρησιμοποιώ λατινική μεταγραφή για slugs: υπηρεσίεςypiresies. Αυτό κρατά τα URLs αναγνώσιμα σε πλαίσια κοινής χρήσης και αποφεύγει την κωδικοποίηση ποσοστού στα analytics. Αλλά η αυτόματη δημιουργία slug από ελληνικό τίτλο δεν το κάνει αυτό αυτόματα — τα περισσότερα βοηθητικά slug αφαιρούν τα διακριτικά από λατινικούς χαρακτήρες αλλά αφήνουν ελληνικά σενάρια ως έχουν ή τα παραλείπουν εντελώς, παράγοντας άδεια slugs.

Η λύση είναι ένα βήμα μεταγραφής πριν τη δημιουργία slug:

import { transliterate } from 'transliteration';
import slugify from 'slugify';

export function slugifyGreek(input: string): string {
  const latin = transliterate(input); // υπηρεσίες → ypiresies
  return slugify(latin, { lower: true, strict: true });
}

Χωρίς αυτό, ένα ελληνικό πεδίο τίτλου στον πίνακα διαχείρισης θα παράγει σιωπηλά ένα σπασμένο ή άδειο slug.

3. Το middleware του next-intl και το edge runtime του Vercel δεν συνεργάζονται καλά με την ανίχνευση locale από το Accept-Language

Η ανίχνευση locale του next-intl βασίζεται στο header αίτησης Accept-Language. Στο edge runtime του Vercel, αυτό λειτουργεί ως επί το πλείστον — αλλά όταν έχεις ένα προσαρμοσμένο middleware που επίσης επιθεωρεί headers (CSP nonces, έλεγχοι ταυτοποίησης, ανακατευθύνσεις), η σειρά εκτέλεσης του middleware έχει σημασία και είναι εύκολο να πάει στραβά.

Το σύμπτωμα: οι χρήστες καταλήγουν στο λάθος locale παρά τη σαφή προτίμηση του browser τους. Ελέγχεις τα response headers και το Set-Cookie για το cookie locale φαίνεται σωστό, αλλά η σελίδα εξακολουθεί να αποδίδεται στη λάθος γλώσσα.

Η αιτία είναι σχεδόν πάντα ένα middleware που επιστρέφει νωρίς (με ανακατεύθυνση ή απόκριση) πριν το middleware locale του next-intl έχει την ευκαιρία να ορίσει το cookie του. Η λύση: βεβαιώσου ότι ο περιτύλιξης createMiddleware του next-intl εκτελείται πρώτος, και μόνο τότε στρωματοποίησε τη δική σου λογική.

// middleware.ts
import createIntlMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';

const intlMiddleware = createIntlMiddleware(routing);

export default function middleware(request: NextRequest) {
  const intlResponse = intlMiddleware(request);
  // Εφάρμοσε τα δικά σου headers στο intlResponse, όχι σε νέο Response
  intlResponse.headers.set('X-Custom-Header', 'value');
  return intlResponse;
}

4. Μορφοποίηση ημερομηνίας και αριθμών στο ελληνικό locale

Η Ελλάδα χρησιμοποιεί ημερομηνίες ΗΗ/ΜΜ/ΕΕΕΕ, κόμμα ως διαχωριστικό δεκαδικών, και τελεία ως διαχωριστικό χιλιάδων — το αντίθετο της αμερικανικής σύμβασης. Το next-intl χειρίζεται αυτό μέσω του API Intl, αλλά μόνο αν χρησιμοποιείς πραγματικά το useFormatter() και περνάς σωστά το locale.

Το σφάλμα που συνάντησα: χρησιμοποιούσα το new Date().toLocaleDateString() απευθείας σε ένα component, το οποίο λαμβάνει το locale του διακομιστή κατά τη χρόνο αποτύπωσης, όχι το locale του χρήστη από το context του next-intl. Στην παραγωγή στο Vercel, το locale του διακομιστή είναι en-US, οπότε οι Έλληνες χρήστες έβλεπαν 6/2/2026 αντί για 2/6/2026.

// Λάθος — χρησιμοποιεί το locale του διακομιστή
const dateStr = new Date(post.date).toLocaleDateString();

// Σωστό — χρησιμοποιεί το locale του next-intl
const format = useFormatter();
const dateStr = format.dateTime(new Date(post.date), { dateStyle: 'long' });

5. Ελληνικές γραμματοσειρές και η αύξηση CLS

Η φόρτωση μιας γραμματοσειράς που υποστηρίζει ελληνικά (ελληνικό subset της Inter, ή μια αποκλειστικά ελληνική γραμματοσειρά) απαιτεί ρητά αίτημα για το ελληνικό subset χαρακτήρων. Χωρίς αυτό, το next/font του Next.js κατεβάζει ένα subset μόνο για λατινικά και ο browser επιστρέφει στη γραμματοσειρά συστήματος για ελληνικούς χαρακτήρες, προκαλώντας ορατό φλας και αύξηση CLS.

// app/fonts.ts
import { Inter } from 'next/font/google';

export const inter = Inter({
  subsets: ['latin', 'greek'], // και τα δύο απαιτούνται
  variable: '--font-inter',
  display: 'swap',
});

Το display: 'swap' είναι ένας συμβιβασμός: αποτρέπει αόρατο κείμενο αλλά επιτρέπει αλλαγή διάταξης αν η γραμματοσειρά φορτωθεί αργά. Για σελίδες με πολύ ελληνικό περιεχόμενο, το display: 'optional' αξίζει να εξεταστεί — χρησιμοποιεί την εναλλακτική γραμματοσειρά αν η προσαρμοσμένη δεν φορτωθεί μέσα σε ένα μικρό παράθυρο, κάτι που είναι καλύτερο για το CLS σε αργές συνδέσεις.

Μια μετα-παρατήρηση

Τα περισσότερα από τα παραπάνω ζητήματα δεν εμφανίζονται κατά την ανάπτυξη επειδή το μηχάνημά σου έχει το δικό σου locale, ο browser σου είναι σωστά ρυθμισμένος, και το middleware σου τρέχει ενάντια σε μία μόνο αίτηση σε απομόνωση. Εμφανίζονται μόνο στην παραγωγή, συχνά ως σταδιακές πτώσεις επισκεψιμότητας παρά σκληρά σφάλματα. Η δημιουργία ενός περιβάλλοντος staging που προσομοιώνει διαφορετικά headers Accept-Language αξίζει τον χρόνο εγκατάστασης.