From 04b7fbf418e7ab2c6fea3e4f4429fb8ca4fe21d7 Mon Sep 17 00:00:00 2001 From: Mathias Oterhals Myklebust Date: Thu, 26 Sep 2024 13:50:39 +0200 Subject: [PATCH] feat(i18n): language routing introduces a new level in the Next.js dynamic routing: [lang] to specify the desired display language supported by a middleware to rewrite paths to include language code --- package-lock.json | 9 +- package.json | 2 + .../(main)/{ => [lang]}/[slug]/[id]/page.tsx | 0 src/app/(main)/{ => [lang]}/[slug]/page.tsx | 25 ++- src/app/(main)/{ => [lang]}/page.tsx | 27 ++- src/app/(main)/legal/[id]/page.tsx | 41 ----- src/components/navigation/footer/Footer.tsx | 3 +- src/middleware.ts | 47 +---- src/middlewares/languageMiddleware.ts | 169 ++++++++++++++++++ src/middlewares/redirectMiddleware.ts | 49 +++++ studio/lib/interfaces/slugTranslations.ts | 6 + studio/lib/queries/languages.ts | 2 + studio/lib/queries/redirects.ts | 2 +- studio/lib/queries/slugTranslations.ts | 27 +++ studio/schemas/schemaTypes/slug.ts | 29 +-- 15 files changed, 334 insertions(+), 104 deletions(-) rename src/app/(main)/{ => [lang]}/[slug]/[id]/page.tsx (100%) rename src/app/(main)/{ => [lang]}/[slug]/page.tsx (85%) rename src/app/(main)/{ => [lang]}/page.tsx (75%) delete mode 100644 src/app/(main)/legal/[id]/page.tsx create mode 100644 src/middlewares/languageMiddleware.ts create mode 100644 src/middlewares/redirectMiddleware.ts create mode 100644 studio/lib/interfaces/slugTranslations.ts create mode 100644 studio/lib/queries/slugTranslations.ts diff --git a/package-lock.json b/package-lock.json index a3f9fc256..ab451eb8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,8 @@ "@sanity/preview-url-secret": "^1.6.11", "@sanity/react-loader": "^1.9.15", "@sanity/vision": "^3.39.1", + "@types/negotiator": "^0.6.3", + "negotiator": "^0.6.3", "next": "^14.2.13", "next-sanity": "^7.1.4", "next-sanity-image": "^6.1.1", @@ -7888,6 +7890,12 @@ "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", "license": "MIT" }, + "node_modules/@types/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@types/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-JkXTOdKs5MF086b/pt8C3+yVp3iDUwG635L7oCH6HvJvvr6lSUU5oe/gLXnPEfYRROHjJIPgCV6cuAg8gGkntQ==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.16.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", @@ -20387,7 +20395,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" diff --git a/package.json b/package.json index 0529d71e6..dd291d392 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "@sanity/preview-url-secret": "^1.6.11", "@sanity/react-loader": "^1.9.15", "@sanity/vision": "^3.39.1", + "@types/negotiator": "^0.6.3", + "negotiator": "^0.6.3", "next": "^14.2.13", "next-sanity": "^7.1.4", "next-sanity-image": "^6.1.1", diff --git a/src/app/(main)/[slug]/[id]/page.tsx b/src/app/(main)/[lang]/[slug]/[id]/page.tsx similarity index 100% rename from src/app/(main)/[slug]/[id]/page.tsx rename to src/app/(main)/[lang]/[slug]/[id]/page.tsx diff --git a/src/app/(main)/[slug]/page.tsx b/src/app/(main)/[lang]/[slug]/page.tsx similarity index 85% rename from src/app/(main)/[slug]/page.tsx rename to src/app/(main)/[lang]/[slug]/page.tsx index fe3494a52..435c106a6 100644 --- a/src/app/(main)/[slug]/page.tsx +++ b/src/app/(main)/[lang]/[slug]/page.tsx @@ -3,6 +3,8 @@ import { Metadata } from "next"; import { Blog } from "src/blog/Blog"; import BlogPreview from "src/blog/BlogPreview"; import CustomErrorMessage from "src/blog/components/customErrorMessage/CustomErrorMessage"; +import Legal from "src/blog/components/legal/Legal"; +import LegalPreview from "src/blog/components/legal/LegalPreview"; import { homeLink } from "src/blog/components/utils/linkTypes"; import Compensations from "src/compensations/Compensations"; import CompensationsPreview from "src/compensations/CompensationsPreview"; @@ -13,9 +15,11 @@ import SectionRenderer from "src/utils/renderSection"; import { fetchSeoData, generateMetadataFromSeo } from "src/utils/seo"; import { CompanyLocation } from "studio/lib/interfaces/companyDetails"; import { CompensationsPage } from "studio/lib/interfaces/compensations"; +import { LegalDocument } from "studio/lib/interfaces/legalDocuments"; import { BlogPage, PageBuilder, Post } from "studio/lib/interfaces/pages"; import { CustomerCasePage } from "studio/lib/interfaces/specialPages"; import { COMPANY_LOCATIONS_QUERY } from "studio/lib/queries/companyDetails"; +import { LEGAL_DOCUMENTS_BY_SLUG_AND_LANG_QUERY } from "studio/lib/queries/legalDocuments"; import { BLOG_PAGE_QUERY, POSTS_QUERY, @@ -31,9 +35,7 @@ import { loadStudioQuery } from "studio/lib/store"; export const dynamic = "force-dynamic"; type Props = { - params: { - slug: string; - }; + params: { lang: string; slug: string }; }; export async function generateMetadata({ params }: Props): Promise { @@ -51,7 +53,8 @@ const Page404 = ( ); async function Page({ params }: Props) { - const { slug } = params; + const { lang, slug } = params; + const { perspective, isDraftMode } = getDraftModeInfo(); const [ @@ -60,6 +63,7 @@ async function Page({ params }: Props) { initialCompensationsPage, initialLocationsData, initialCustomerCases, + initialLegalDocument, ] = await Promise.all([ loadStudioQuery(SLUG_QUERY, { slug }, { perspective }), loadStudioQuery(BLOG_PAGE_QUERY, { slug }, { perspective }), @@ -78,6 +82,11 @@ async function Page({ params }: Props) { { slug }, { perspective }, ), + loadStudioQuery( + LEGAL_DOCUMENTS_BY_SLUG_AND_LANG_QUERY, + { slug, language: lang }, + { perspective }, + ), ]); if (initialPage.data) { @@ -145,6 +154,14 @@ async function Page({ params }: Props) { ); } + if (initialLegalDocument.data) { + return isDraftMode ? ( + + ) : ( + + ); + } + return Page404; } diff --git a/src/app/(main)/page.tsx b/src/app/(main)/[lang]/page.tsx similarity index 75% rename from src/app/(main)/page.tsx rename to src/app/(main)/[lang]/page.tsx index f22e0228b..eb1c72acc 100644 --- a/src/app/(main)/page.tsx +++ b/src/app/(main)/[lang]/page.tsx @@ -1,11 +1,16 @@ import { Metadata } from "next"; +import CustomErrorMessage from "src/blog/components/customErrorMessage/CustomErrorMessage"; import InformationSection from "src/blog/components/informationSection/InformationSection"; +import { homeLink } from "src/blog/components/utils/linkTypes"; import { getDraftModeInfo } from "src/utils/draftmode"; import SectionRenderer from "src/utils/renderSection"; import { fetchSeoData, generateMetadataFromSeo } from "src/utils/seo"; +import { client } from "studio/lib/client"; import { LinkType } from "studio/lib/interfaces/navigation"; import { PageBuilder } from "studio/lib/interfaces/pages"; +import { LanguageObject } from "studio/lib/interfaces/supportedLanguages"; +import { LANGUAGES_QUERY } from "studio/lib/queries/languages"; import { LANDING_PAGE_REF_QUERY } from "studio/lib/queries/navigation"; import { PAGE_QUERY, SEO_PAGE_QUERY } from "studio/lib/queries/page"; import { loadStudioQuery } from "studio/lib/store"; @@ -34,9 +39,29 @@ const pagesLink = { internalLink: { _ref: "studio/structure/pages" }, }; -const Home = async () => { +type Props = { + params: { lang: string; slug: string }; +}; + +const Page404 = ( + +); + +const Home = async ({ params }: Props) => { const { perspective, isDraftMode } = getDraftModeInfo(); + const language = ( + await client.fetch(LANGUAGES_QUERY) + )?.find((l) => l.id === params.lang); + + if (language === undefined) { + return Page404; + } + const { data: landingId } = await loadStudioQuery( LANDING_PAGE_REF_QUERY, {}, diff --git a/src/app/(main)/legal/[id]/page.tsx b/src/app/(main)/legal/[id]/page.tsx deleted file mode 100644 index efeda4d0f..000000000 --- a/src/app/(main)/legal/[id]/page.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import Legal from "src/blog/components/legal/Legal"; -import LegalPreview from "src/blog/components/legal/LegalPreview"; -import { getDraftModeInfo } from "src/utils/draftmode"; -import { LegalDocument } from "studio/lib/interfaces/legalDocuments"; -import { LEGAL_DOCUMENTS_BY_SLUG_AND_LANG_QUERY } from "studio/lib/queries/legalDocuments"; -import { loadStudioQuery } from "studio/lib/store"; - -export const dynamic = "force-dynamic"; - -type Props = { - params: { - id: string; - }; -}; - -// TODO: hide from SEO??? - -async function Page({ params }: Props) { - const { id } = params; - const { perspective, isDraftMode } = getDraftModeInfo(); - - const initialDocument = await loadStudioQuery( - LEGAL_DOCUMENTS_BY_SLUG_AND_LANG_QUERY, - { slug: id, language: "en" }, //TODO: replace this with selected language for the page - { perspective }, - ); - - if (!initialDocument) { - throw new Error("Page not found"); - } - - if (isDraftMode) { - return ; - } - - if (initialDocument) { - return ; - } -} - -export default Page; diff --git a/src/components/navigation/footer/Footer.tsx b/src/components/navigation/footer/Footer.tsx index 8851dd57e..2d227bd8e 100644 --- a/src/components/navigation/footer/Footer.tsx +++ b/src/components/navigation/footer/Footer.tsx @@ -52,14 +52,13 @@ const Footer = ({ {legalData?.map((legal) => { - const path = `legal/${legal.slug.current}`; const link = { _key: legal._id, _type: legal._type, linkTitle: legal.basicTitle, linkType: LinkType.Internal, internalLink: { - _ref: path, + _ref: legal.slug.current, }, }; return ( diff --git a/src/middleware.ts b/src/middleware.ts index 403d1f172..b20770112 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,47 +1,14 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextRequest } from "next/server"; -import { RedirectDestinationSlugPage } from "studio/lib/interfaces/redirect"; -import { REDIRECT_BY_SOURCE_SLUG_QUERY } from "studio/lib/queries/redirects"; - -import { readBaseUrl } from "./app/env"; -import { HTTP_STATUSES } from "./utils/http"; +import { languageMiddleware } from "./middlewares/languageMiddleware"; +import { redirectMiddleware } from "./middlewares/redirectMiddleware"; export async function middleware(request: NextRequest) { - const baseUrlResult = readBaseUrl(); - if (!baseUrlResult.ok) { - console.error( - "Failed to run middleware, missing base url:", - baseUrlResult.error, - ); - return; - } - const baseUrl = baseUrlResult.value; - const slug = request.nextUrl.pathname; - const slugQueryParam = slug.replace(/^\/+/, ""); - /* - fetching redirect data via API route to avoid token leaking to client - (middleware should run on server, but `experimental_taintUniqueValue` begs to differ...) - */ - const redirectRes = await fetch(new URL("/api/fetchData", baseUrl), { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: REDIRECT_BY_SOURCE_SLUG_QUERY, - params: { slug: slugQueryParam }, - }), - }); - if (redirectRes.ok) { - const redirect: RedirectDestinationSlugPage | null = - await redirectRes.json(); - if (redirect?.destination) { - return NextResponse.redirect( - new URL(redirect.destination, request.url), - HTTP_STATUSES.TEMPORARY_REDIRECT, - ); - } + const res = await redirectMiddleware(request); + if (res !== undefined) { + return res; } + return languageMiddleware(request); } export const config = { diff --git a/src/middlewares/languageMiddleware.ts b/src/middlewares/languageMiddleware.ts new file mode 100644 index 000000000..045aea08c --- /dev/null +++ b/src/middlewares/languageMiddleware.ts @@ -0,0 +1,169 @@ +import Negotiator from "negotiator"; +import { headers } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; + +import { client } from "studio/lib/client"; +import { SlugTranslations } from "studio/lib/interfaces/slugTranslations"; +import { LanguageObject } from "studio/lib/interfaces/supportedLanguages"; +import { + DEFAULT_LANGUAGE_QUERY, + LANGUAGES_QUERY, +} from "studio/lib/queries/languages"; +import { + SLUG_TRANSLATIONS_FROM_LANGUAGE_QUERY, + SLUG_TRANSLATIONS_TO_LANGUAGE_QUERY, +} from "studio/lib/queries/slugTranslations"; + +async function translateSlug( + slug: string, + targetLanguageId: string, + sourceLanguageId?: string, +): Promise { + if (slug.length === 0) { + return slug; + } + const slugTranslations = await client.fetch( + sourceLanguageId !== undefined + ? SLUG_TRANSLATIONS_FROM_LANGUAGE_QUERY + : SLUG_TRANSLATIONS_TO_LANGUAGE_QUERY, + { + slug, + language: sourceLanguageId ?? targetLanguageId, + }, + ); + return slugTranslations?._translations.find( + (t) => t !== null && t.language === targetLanguageId, + )?.slug; +} + +function negotiateClientLanguage( + availableLanguages: string[], +): string | undefined { + const acceptLanguage = headers().get("Accept-Language"); + if (acceptLanguage === null) return undefined; + return new Negotiator({ + // Negotiator expects lower-case header name + headers: { "accept-language": acceptLanguage }, + }).language(availableLanguages); +} + +/** + * Handles rerouting of paths based on URL language code or user's preferred language + * + * Limitations/notes: + * - no special treatment of nested paths (e.g. `/blogg` uses slug `blogg`, while `/blogg/hei` uses slug `blogg/hei`) + * - given the following valid pages, an English user will be redirected from (1) to (2), and not to (3): + * - (1) `/gift` + * - (2) `/en/poison` + * - (3) `/en/gift` + * - given the following valid pages, an English user navigating to (1) will not be redirected to (2), but receive a 404 error: + * - (1) `/gift` + * - (2) `/en/gift` + * + * @param request + */ +export async function languageMiddleware( + request: NextRequest, +): Promise { + const availableLanguages = await client.fetch( + LANGUAGES_QUERY, + ); + if (availableLanguages === null) { + console.error("No languages available, language middleware aborted."); + return; + } + const pathname = request.nextUrl.pathname; + const language = availableLanguages.find( + ({ id }) => pathname.startsWith(`/${id}/`) || pathname === `/${id}`, + ); + if (language === undefined) { + return redirectMissingLanguage(pathname, availableLanguages, request.url); + } + return redirectWithLanguage(pathname, language, request.url); +} + +/** + * Check that the slug actually exists for the given language. + * - If it exists, no changes are made. + * - Otherwise, we attempt to translate to the specified language + * - If translated, user is redirected to `/[language]/[translatedSlug]` + * - Otherwise, user is redirected to the slug with default language (`/[slug]`) + * + * @param pathname + * @param language + * @param baseUrl + */ +async function redirectWithLanguage( + pathname: string, + language: LanguageObject, + baseUrl: string, +) { + const slug = pathname.split("/").slice(2).join("/"); + const translatedSlug = await translateSlug(slug, language.id); + if (translatedSlug === undefined) { + return NextResponse.redirect(new URL(`/${slug}`, baseUrl)); + } + if (translatedSlug !== slug) { + const translatedPathname = `/${language.id}/${translatedSlug}`; + return NextResponse.redirect(new URL(translatedPathname, baseUrl)); + } + // all good, no modifications needed + return; +} + +/** + * + * Negotiate with the user to find the most preferred languages from the list of available languages + * - If the negotiated language is the same as default, no changes are made. + * - Otherwise, we attempt to translate to the negotiated language + * - If translated, user is redirected to `/[negotiatedLanguage]/[translatedSlug]` + * - Otherwise, user is redirected to the slug with default language (`/[slug]`) + * + * @param pathname + * @param availableLanguages + * @param baseUrl + */ +async function redirectMissingLanguage( + pathname: string, + availableLanguages: LanguageObject[], + baseUrl: string, +) { + // Pathname does NOT include a language code, we negotiate with the user to find + // the most preferred languages from the list of available languages + const defaultLanguageId = ( + await client.fetch(DEFAULT_LANGUAGE_QUERY) + )?.id; + if (defaultLanguageId === undefined) { + console.error( + "No default language available, language middleware aborted.", + ); + return; + } + const negotiatedLanguage = + negotiateClientLanguage( + availableLanguages.map((language) => language.id), + ) ?? defaultLanguageId; + if (negotiatedLanguage === defaultLanguageId) { + // Same as default, simply rewrite internally to include language code + return NextResponse.rewrite( + new URL(`/${negotiatedLanguage}${pathname}`, baseUrl), + ); + } + // Attempt to translate to the negotiated language + const slug = pathname.replace(/^\//, ""); + const translatedSlug = await translateSlug( + slug, + negotiatedLanguage, + defaultLanguageId, + ); + if (translatedSlug === undefined) { + // Translation not available, rewrite to default language + return NextResponse.rewrite( + new URL(`/${defaultLanguageId}${pathname}`, baseUrl), + ); + } + // Redirect with language code and translated slug + return NextResponse.redirect( + new URL(`/${negotiatedLanguage}/${translatedSlug}`, baseUrl), + ); +} diff --git a/src/middlewares/redirectMiddleware.ts b/src/middlewares/redirectMiddleware.ts new file mode 100644 index 000000000..f478e4035 --- /dev/null +++ b/src/middlewares/redirectMiddleware.ts @@ -0,0 +1,49 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { readBaseUrl } from "src/app/env"; +import { HTTP_STATUSES } from "src/utils/http"; +import { RedirectDestinationSlugPage } from "studio/lib/interfaces/redirect"; +import { REDIRECT_BY_SOURCE_SLUG_QUERY } from "studio/lib/queries/redirects"; + +export async function redirectMiddleware( + request: NextRequest, +): Promise { + const baseUrlResult = readBaseUrl(); + if (!baseUrlResult.ok) { + console.error( + "Failed to run redirect middleware, missing base url:", + baseUrlResult.error, + ); + return; + } + const baseUrl = baseUrlResult.value; + const slug = request.nextUrl.pathname; + // TODO: handle language path segment + const slugQueryParam = slug.replace(/^\/+/, ""); + /* + fetching redirect data via API route to avoid token leaking to client + (middleware should run on server, but `experimental_taintUniqueValue` begs to differ...) + */ + const redirectRes = await fetch(new URL("/api/fetchData", baseUrl), { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query: REDIRECT_BY_SOURCE_SLUG_QUERY, + params: { slug: slugQueryParam }, + }), + }); + if (redirectRes.ok) { + const redirect: RedirectDestinationSlugPage | null = + await redirectRes.json(); + if (redirect?.destination) { + console.log("Redirect", slug, "→", redirect.destination); + return NextResponse.redirect( + new URL(redirect.destination, request.url), + HTTP_STATUSES.TEMPORARY_REDIRECT, + ); + } + } + console.log("No redirect for", slug); +} diff --git a/studio/lib/interfaces/slugTranslations.ts b/studio/lib/interfaces/slugTranslations.ts new file mode 100644 index 000000000..3c68882aa --- /dev/null +++ b/studio/lib/interfaces/slugTranslations.ts @@ -0,0 +1,6 @@ +export interface SlugTranslations { + _translations: ({ + language: string; + slug: string; + } | null)[]; +} diff --git a/studio/lib/queries/languages.ts b/studio/lib/queries/languages.ts index c5d608f2f..9903a54a1 100644 --- a/studio/lib/queries/languages.ts +++ b/studio/lib/queries/languages.ts @@ -1,3 +1,5 @@ import { groq } from "next-sanity"; export const DEFAULT_LANGUAGE_QUERY = groq`*[_type == "languageSettings" && _id == "languageSettings"][0].languages[default][0]`; + +export const LANGUAGES_QUERY = groq`*[_type == "languageSettings" && _id == "languageSettings"][0].languages`; diff --git a/studio/lib/queries/redirects.ts b/studio/lib/queries/redirects.ts index 86401b246..b54619985 100644 --- a/studio/lib/queries/redirects.ts +++ b/studio/lib/queries/redirects.ts @@ -1,7 +1,7 @@ import { groq } from "next-sanity"; export const REDIRECT_BY_SOURCE_SLUG_QUERY = groq` - *[_type == "redirect" && source.current == $slug][0]{ + *[_type == "brokenLink" && source.current == $slug][0]{ "destination": select( destination.type == "reference" => destination.reference->slug.current, destination.type == "external" => destination.external diff --git a/studio/lib/queries/slugTranslations.ts b/studio/lib/queries/slugTranslations.ts new file mode 100644 index 000000000..b2e636523 --- /dev/null +++ b/studio/lib/queries/slugTranslations.ts @@ -0,0 +1,27 @@ +import { groq } from "next-sanity"; + +export const SLUG_TRANSLATIONS_FROM_LANGUAGE_QUERY = groq` + *[slug.current == $slug && language == $language][0]{ + "_translations": *[ + _type == "translation.metadata" + && references(^._id) + ].translations[].value->{ + language, + "slug": slug.current, + }, + } +`; + +export const SLUG_TRANSLATIONS_TO_LANGUAGE_QUERY = groq` + *[slug.current == $slug][0]{ + "_translations": ( + *[ + _type == "translation.metadata" + && references(^._id) + ].translations[].value->{ + language, + "slug": slug.current, + } + )[language == $language], + } +`; diff --git a/studio/schemas/schemaTypes/slug.ts b/studio/schemas/schemaTypes/slug.ts index d4b980386..4938baba1 100644 --- a/studio/schemas/schemaTypes/slug.ts +++ b/studio/schemas/schemaTypes/slug.ts @@ -1,24 +1,25 @@ import { SlugValidationContext, defineField } from "sanity"; +import { apiVersion } from "studio/env"; import { isPublished } from "studio/utils/documentUtils"; -async function isSlugUniqueAcrossAllDocuments( +function isSlugUniqueAcrossAllDocuments( slug: string, - context: SlugValidationContext, + { document, getClient }: SlugValidationContext, ) { - const { document, getClient } = context; - const client = getClient({ apiVersion: "2022-12-07" }); + if (document === undefined) { + return true; + } const id = document?._id.replace(/^drafts\./, ""); - const params = { - draft: `drafts.${id}`, - published: id, - slug, - }; - const SLUG_QUERY = - "!defined(*[!(_id in [$draft, $published]) && slug.current == $slug][0]._id)"; - - const result = await client.fetch(SLUG_QUERY, params); - return result; + return getClient({ apiVersion }).fetch( + "!defined(*[!(_id in [$draft, $published]) && slug.current == $slug && language == $language][0]._id)", + { + draft: `drafts.${id}`, + published: id, + slug, + language: document.language, + }, + ); } const SLUG_MAX_LENGTH = 200;