DEV Community

Cover image for I Built an AI Color Analysis App in 9 Languages: Next.js SEO Lessons I Wish I Knew Earlier
Belle AI
Belle AI

Posted on

I Built an AI Color Analysis App in 9 Languages: Next.js SEO Lessons I Wish I Knew Earlier

Over the last few months I've been building Belle AI, an app that takes a single selfie and tells you your seasonal color type (the 12-season system), your most flattering makeup shades, and your face shape.

It ships as an iOS app and a Next.js website. The website turned out to be where I learned the most, mostly about multilingual SEO. This post covers what I built and the mistakes that cost me the most time.

Disclosure: I'm the developer of Belle AI. The links below point to my project.

What it does

Personal color analysis is usually done by a human stylist who drapes colored fabrics under your face. It typically costs $150-400 and takes a couple of hours.

Belle AI approximates that with a vision model pipeline:

  1. You upload a selfie.
  2. The model estimates skin undertone, eye color, hair color and contrast level.
  3. It maps you to one of 12 seasons (Spring, Summer, Autumn, Winter, each with Light, Warm, Soft, Cool, Deep or Bright variants).
  4. It generates a report with a palette for clothing, makeup, hair and jewelry.

You can try it here: aicoloranalysis.io

The stack

  • Web: Next.js (App Router), React, Tailwind CSS, next-intl for i18n
  • Auth and payments: Auth.js, plus a merchant-of-record payment provider
  • Backend: Python API that talks to generative AI models
  • Mobile: native iOS app (App Store)
  • Locales: English, Chinese, Japanese, Korean, Spanish, French, German, Russian, Turkish

Lesson 1: Next.js metadata is replaced, not merged

This one bit me hard. In the App Router, if a page exports its own openGraph or twitter object, it replaces the one from your layout. It does not merge with it.

So any page that defined its own openGraph without images silently lost its share preview image. Pinterest and X showed blank cards.

The fix is boring: export shared constants and include them explicitly everywhere.

export const OG_IMAGES = [
  { url: `${SITE_URL}/opengraph-image`, width: 1200, height: 630 },
];
export const TWITTER_IMAGES = [`${SITE_URL}/opengraph-image`];
Enter fullscreen mode Exit fullscreen mode

If a page has its own opengraph-image file, skip the shared constant there.

Lesson 2: hreflang only works if it's reciprocal

My first approach was to emit hreflang alternates for every locale on every page. That was wrong.

Not every blog post exists in every language, and some thin translations are noindex. Pointing hreflang at a 404 or a noindex page breaks the reciprocal rule, and Google can ignore the whole group.

So I only list locales that actually exist and are indexable:

export function buildAlternatesForLocales(
  locale: string,
  path: string,
  availableLocales: readonly string[],
) {
  const languages: Record<string, string> = {};
  for (const l of availableLocales) {
    languages[l] =
      l === routing.defaultLocale
        ? `${SITE_URL}${path}`
        : `${SITE_URL}/${l}${path}`;
  }
  if (availableLocales.includes(routing.defaultLocale)) {
    languages["x-default"] = `${SITE_URL}${path}`;
  }
  return { canonical: canonicalUrl(locale, path), languages };
}
Enter fullscreen mode Exit fullscreen mode

Lesson 3: Pick one canonical host and enforce it

example.com, www.example.com, and example.com/en/ can all serve the same page. I standardized on one host and made the default locale prefix-free. Everything else, including sitemaps, canonicals, hreflang and outbound links, uses that single form.

It sounds trivial, but it removed a whole class of duplicate-content problems.

Lesson 4: RSS auto-discovery is per page

Another Next.js metadata gotcha is that alternates on a page overrides the layout's alternates. If you only declare the RSS link in the layout, most pages won't expose it in <head>.

I moved RSS discovery into the same helper that builds canonicals and hreflang, so every page gets it automatically, and each language points to its own feed.

Lesson 5: Track attribution from day one

Every outbound link I post gets UTM parameters (utm_source, utm_medium, utm_campaign). Without this, you can't tell which channel brings signups or revenue. Adding it later means losing months of data.

What's next

  • More tools: undertone test, face shape quiz, hair color preview
  • Better handling of lighting differences in selfies (the hardest accuracy problem so far)
  • More languages

Try it and tell me what you think

I'd love feedback from other devs, especially on:

  1. How you handle lighting variance in photo-based ML features
  2. Your multilingual SEO setup in Next.js
  3. Anything that looks broken on the site

Thanks for reading!

Top comments (0)