# Taco

URL: https://sola.ysz.life/projects/taco

> A dog's website, built as a reusable template

- Type: Personal project
- Period: Apr 2025
- Live: https://takitwo.vercel.app
- Source: https://github.com/lyfe691/taco
- Stack: Next.js, TypeScript, Tailwind CSS, Motion, i18n, shadcn/ui, Radix UI, Vercel

Taco is my brother's Pomeranian, and this is his website. I set it up so the next small site of its kind can start from the same code and only swap in new colours and new text.

## A website for my brother's dog, built to be reused

Taco is my brother's Pomeranian, and [takitwo.vercel.app](https://takitwo.vercel.app) is his website. I built it as a base for the next small site of this kind. The colours live in theme classes and every interface string in one JSON file per language, so the next site means new content and new values in those files while the components stay as they are.

![The Taco home page switching themes from the palette menu in the header: from Green to Cherry, Sky, Gotham and finally Night](https://sola.ysz.life/projects/taco/themes.webp)
*Switching themes from the header.*
[Video](https://sola.ysz.life/projects/taco/themes.mp4)

### Colour themes

The palette button in the header opens the theme menu. Picking a theme recolours every page at once, from the links and buttons to the soft circles behind the photo, and the site remembers the choice for the next visit. Gotham is black and white, Night is navy with a blue accent, and every new visitor starts in Green.

![The about page in the Gotham theme: on the left a large photo of Taco in the grass, a hand holding a dandelion out to him, and on the right a short description with cards for age, breed, personality and favourite food, all white on black](https://sola.ysz.life/projects/taco/about-gotham.webp)
*The about page in Gotham.*

### Languages

The site is written in English, Spanish and Japanese. On a first visit it picks the language that matches the browser and falls back to English, and the language button in the header switches it at any time. A choice made there is saved and wins on every later visit.

![The language menu opens on the home page, 日本語 is picked, the navigation, headline and buttons switch to Japanese, and the page scrolls down to the about section, also in Japanese](https://sola.ysz.life/projects/taco/language.webp)
*Switching the site to Japanese.*
[Video](https://sola.ysz.life/projects/taco/language.mp4)

### Moving between pages

The header stays in place from page to page, and the line under the active link slides across to the new one while the next page fades in. The about page gathers Taco's details: his age, breed, personality and favourite food.

![The site in the Sky theme: clicking About slides the underline from Home to About as the about page fades in, then clicking Home slides it back](https://sola.ysz.life/projects/taco/nav.webp)
*The underline follows the active page.*
[Video](https://sola.ysz.life/projects/taco/nav.mp4)

### The blog

Every post has its own page with a title, the date and author, a cover photo and the story below it. The text styles follow the theme, so in Gotham and Night a post reads as light text on a dark page.

![A blog post titled Taco's Beach Day in the Night theme: a back link, the date and author, a wide photo of Taco, and the first paragraphs in light text on navy](https://sola.ysz.life/projects/taco/post-night.webp)
*A post in the Night theme.*

### On a phone

On a phone the links move into a menu that slides in from the side. The language and theme buttons sit at the bottom of it and work the same way as on a computer.

![On a phone, the menu slides in from the right; the theme button switches the site to Cherry, then the language button switches it to Japanese](https://sola.ysz.life/projects/taco/menu.webp)
*The phone menu, with language and theme at the bottom.*
[Video](https://sola.ysz.life/projects/taco/menu.mp4)

### How it's built

Taco is a Next.js app on the App Router, written in TypeScript. The components come from shadcn/ui, which builds on Radix, and are styled with Tailwind CSS. Framer Motion runs the page and navigation animation, and the site is deployed on Vercel.

#### Themes are sets of CSS variables

shadcn/ui colours everything through tokens: classes like `bg-primary` and `border-border`. The Tailwind config maps each token to a CSS variable, so `primary` becomes `hsl(var(--primary))`, and a theme is a class that gives those variables different HSL values. Green sits on `:root`, and the other themes follow the same pattern as Sky, shown here abridged:

`app/globals.css`

```css
.theme-sky {
  --background: 210 50% 98%;
  --foreground: 210 50% 16%;
  --primary: 210 80% 50%;
  --primary-foreground: 355 100% 100%;
  --secondary: 210 33% 96%;
  --muted-foreground: 210 5% 45%;
  --border: 210 20% 90%;
  --ring: 210 80% 50%;
}
```

Because the pages colour themselves through the tokens, a new theme is one more class and an entry in the theme menu. Night is the class `dark`, the one Tailwind's `dark:` variant looks for. Gotham is dark as well but has a class of its own, so the prose styles for blog posts name both `.dark` and `.theme-gotham`.

#### No flash of the wrong theme

[next-themes](https://github.com/pacocoursey/next-themes) puts the chosen theme's class on `<html>` and keeps it in `localStorage`. The class has to be in place before the first paint, or a returning visitor who picked Night would see the page flash green first. The library handles this with a small inline script that reads the stored theme and sets the class before React hydrates.

That is why `<html>` carries `suppressHydrationWarning`: its class in the browser differs from the server's HTML on purpose. The theme button has the same problem on a smaller scale. It shows the current theme's icon, a flower for Cherry and a moon for Night, and since the server can't know the theme, it renders the default palette icon until it has mounted.

#### The English file is the type

The interface text lives in `lib/i18n/en.json`, `es.json` and `ja.json` as nested objects, and components look strings up by dot path: `t('home.aboutSection.title')` walks down whichever tree is active. The map that holds the language files is typed as `typeof en`, so a Spanish or Japanese file that is missing a key shows up as a type error in the editor.

On a visit, the provider first checks `localStorage` for a language picked earlier. If there is none, it asks the browser:

`lib/i18n/index.ts`

```ts
import { defaultLanguage, languages } from './languages';
import en from './en.json';
import es from './es.json';
import ja from './ja.json';
export type TranslationJSON = typeof en;
type TranslationsType = { [key: string]: TranslationJSON };
const translations: TranslationsType = { en, es, ja };
const fallbackLanguageCode = 'en';
export function detectUserLanguage(): string {
  if (typeof window !== 'undefined' && typeof navigator !== 'undefined') {
    const browserLang = navigator.language || navigator.languages[0];
    const langCode = browserLang.split('-')[0];
    return languages.some(lang => lang.code === langCode)
      ? langCode
      : fallbackLanguageCode;
  }
  return fallbackLanguageCode;
}
```

`navigator.language` returns a tag like `es-MX`. The function keeps the part before the hyphen and uses it if the site has that language, English otherwise. All of this runs in the browser after the page has loaded, which keeps every page static: the HTML is prerendered once and the URLs are the same in every language.

Adding a language takes three steps:

1. A JSON file with every string, in the same shape as `en.json`.
2. An import that puts it in the map, where the type checks it against English.
3. An entry in `languages.ts` with a code, a display name and a flag.

The language menu is built from that list, so it needs no change of its own.

![The Taco home page in the Cherry theme: the headline Meet Taco in pink beside a round photo of Taco with a dandelion held out to him](https://sola.ysz.life/projects/taco/home-cherry.webp)
*The home page in Cherry.*

![The home page on a phone in the Cherry theme: the headline, two buttons and the round photo stacked in one column](https://sola.ysz.life/projects/taco/home-cherry-mobile.webp)
*Home on a phone.*

![The blog post Taco's Beach Day on a phone in the Night theme, with its photo and the first paragraphs](https://sola.ysz.life/projects/taco/post-night-mobile.webp)
*A post on a phone.*
