Skip to content
xscriptor-ai

xscriptor

Full design and development documentation for xscriptor.com, the Xscriptor literary portfolio (Next.js 16 App Router, static export, five locales, four book-reader engines, decryption animations). Use when building or modifying pages, components, CSS, content, i18n, book readers, or the build/deploy of xscriptor.com.

projectv2.0.0web-fullstack/portfolio/xscriptor
View source on GitHub

Xscriptor Site System

xscriptor.com is the personal literary portfolio, blog, book library and art surface of the author/artist Xscriptor (Óscar Preciado). It is a fully static, multilingual Next.js 16 App Router site whose signature interaction is text that decrypts character by character, set in EB Garamond over full-bleed art.

This skill documents the whole system — design language, architecture, routes, components, content pipeline, book readers, i18n, theming, SEO, security and deployment — so an agent can extend the site without breaking its voice or its conventions.

Repository: github.com/xscriptor-web/xscriptor.com (branch main). The Next app lives at the repository root.

Companion references

Read the one(s) relevant to the task; they are the detailed source of truth.

| Reference | Covers | |-----------|--------| | references/design-system.md | Brand intent, color tokens, typography, surfaces (glass/neumorphism/marker chips), motion language, breakpoints, a11y | | references/architecture.md | Stack, rendering model, route table, providers, i18n, theme, SEO, security, gotchas | | references/component-catalog.md | Every component: location, props, behavior, recipes | | references/content-pipeline.md | Blog article pipeline, book MDX format, poemParser, messages + WordConfig | | references/book-readers.md | The four reader engines and their parsing/pagination algorithms | | references/build-and-deploy.md | Scripts, static export, sitemap, .htaccess/_headers, hosting, validation | | references/code-structure.md | Where code goes, responsibilities, import rules, anti-patterns, debt |

When to use this skill

  • Adding or editing any page, section or route on xscriptor.com.
  • Building or refactoring components, CSS modules, or global styles.
  • Working on the book readers, blog pipeline, graph, contact/PGP page or home hero.
  • Adding articles, books, translations or UI strings.
  • Touching i18n, theming, SEO/metadata, static export or deployment config.
  • Anything that must stay visually and structurally consistent with the site.

1. Design intent (the part that matters most)

The site must read like a printed literary edition that has learned to decrypt itself. Concretely:

  • Typography-first. EB Garamond carries everything. No display font, no icon font. Words are the interface.
  • Marker aesthetic. Text over imagery sits on soft color-mix chips so it stays legible.
  • One accent per theme. Deep violet (#5b2e8d) in light, warm gold (#ffe884) in dark. Everything else is background/text/border.
  • Atmospheric art. Full-viewport photographs and pre-rendered background frames behind a dark overlay; the art is the stage, never the subject.
  • Motion reveals, it does not decorate. Decryption, blur-to-focus, staggered fades, a scroll-drawn connecting line.
  • Literary + cryptographic tension. Poetic prose next to PGP fingerprints and descrambling text is the brand, not a gimmick.
  • Both themes first-class; accessible by default; static by default.

If a proposed UI is clever but reads as generic SaaS, reject it. Choose clarity and voice over novelty.


2. Tech stack at a glance

| Layer | Choice | |-------|--------| | Framework | Next.js 16.3 App Router, output: "export", trailingSlash: true | | Language | TypeScript 5 (strict), React 19.2, some JS | | Styling | Tailwind CSS 4.3 (PostCSS) + CSS Modules | | Font | Self-hosted EB Garamond (Regular + Italic, woff2) | | Motion | framer-motion 12 | | Smooth scroll | lenis 1.3 | | Content | Markdown/MDX + gray-matter + remark/rehype (GFM, KaTeX, highlight) | | Components | @xscriptor/xcomponents (npm) + local copies | | Backgrounds | @xscriptor/xbackgrounds (XParticles) | | Sitemap | next-sitemap on postbuild | | Hosting | Static (Apache .htaccess, or _headers platforms) |

No server runtime, no API routes, no PHP, no env vars. Details in architecture.md §1.


3. Rendering model in one minute

  • Everything is pre-rendered. page.tsx files are Server Components; book pages read MDX from disk at build time and pass raw text to client readers.
  • Interactive code is "use client", including the "use client" barrel at src/app/components/xcomponents/index.ts. Server Components must import reusable components only via that barrel.
  • Two locales layers: the root layout hardcodes es (so / is Spanish); the [locale] layout mounts a nested I18nProvider with the requested locale.
  • Providers wrap the app in the root layout: LenisProvider (smooth scroll), TransitionProvider (glass navbar + language dock + route overlay), then separators, main, footer.
  • Theming uses data-theme on <html> + localStorage["theme"], applied before first paint by a beforeInteractive script and toggled by the navbar logo.

Full detail: architecture.md §2–§7.


4. Route map (summary)

All pages live under [locale] (es, en, de, it, fr; es fallback):

| Path | What | |------|------| | / and /[locale] | Home hero (ASCII intro → zigzag phrases) | | /[locale]/blog | Article listing (search + category pills, center-blur cards) | | /[locale]/blog/[...slug] | Article (markdown → KaTeX/highlight → decrypt) | | /[locale]/obras | Book gallery (center-blur cards + graph card) | | /[locale]/obras/{boulevard,asintota,cielos-de-alquitran,colaterales,primavera-en-el-desierto,la-danza-de-las-amapolas} | Book readers | | /[locale]/obras/grafo | Canvas poem graph | | /[locale]/contacto | PGP public-key card + social grid | | /[locale]/info | Bio, press cards, timeline | | /[locale]/links | Link list | | /[locale]/terminos-y-condiciones | Terms/privacy/cookies | | 404 | src/app/not-found.tsx |

Exact files and data sources: architecture.md §3.


5. The design system (essentials)

Full token tables and rules in design-system.md. The non-negotiables:

Colors — use the variables, never raw hex

Light: --bg #ffffff, --text #000000, --accent #5b2e8d, --accent-text #ffffff, --border rgba(0,0,0,.1), --foreground #171717, --text-muted #6b7280, --primary #4328a8, --primary-hover #7c3aed, --success #10b981.

Dark (:root[data-theme="dark"], :root.dark): --bg #0a0a0a, --text #ffffff, --accent #ffe884, --accent-text #000000, --border rgba(255,255,255,.1), --foreground #ededed, --text-muted #9ca3af, --primary #fbbf24, --primary-hover #f59e0b, --success #34d399.

Only sanctioned raw hexes: the book word palette (c1–c5) and the graph’s 8-color palette.

Type

EB Garamond everywhere. Global centered defaults: h1 400/clamp(16px,12vw,32px), h2 300/clamp(24px,8vw,26px), h3 600/clamp(20px,9vw,26px), p 400/clamp(18px,2.5vw,20px) with side padding clamp(1rem,8vw,4rem). .article-content resets prose to left-aligned, line-height 1.8. Monospace is only for code/keys/meta.

Marker chip (the signature atom)

background: color-mix(in srgb, var(--bg) 88%, transparent);
padding: 0.1em 0.35em; border-radius: 0.2em;
box-decoration-break: clone; -webkit-box-decoration-break: clone;

Glass

color-mix(in srgb, var(--bg) 70%, transparent) (light) / 15% (dark) + backdrop-filter: blur(18–20px) + 1px solid var(--border).

Neumorphism

Only PublicKeyCard, via --neumorph-bg and :root.light / :root.dark.

Motion

Subtle, brief, purposeful; reveal not decorate. Respect prefers-reduced-motion (global rule + component guards). Decrypt speed default 50 ms (article body 10 ms). See design-system.md §6.


6. The book reader engines (choose deliberately)

Four engines share the block/section/index/4-5-per-page model but differ:

| Engine | Books | Signature | |--------|-------|-----------| | XBookReader (npm) | Boulevard | simplest paginated reader | | XCompleteBook (npm, via barrel) | Asintota, Amapolas | section/index pages + renderPoem override | | XBookColors (local) | Cielos de Alquitrán, Primavera, Amapolas page | rotating full-bleed backgrounds + per-word colors/decrypt | | BookReaderPoems (local) | Colaterales | index-driven poem reordering + titles |

Poem words become WordConfig[] with hashed underline/button/blur1/ blur2/normal types plus a safety fallback so blurred words are always revealable. Deep dive, parsing algorithms and pitfalls: book-readers.md.


7. Content & i18n essentials

  • Blog: src/app/content/articulos/{locale}/**/*.md; parsed by src/app/lib/articles.ts (frontmatter → markdown → GFM/KaTeX/highlight → HTML; duplicate H1/author stripped; soft lines → <br>; tables wrapped; readingTime = ceil(words/200)); es fallback; research/ excluded from listings.
  • Books: src/app/content/<book>/{locale}.mdx, read as raw text (cielos-de-alquitran ES file is cielos-de-alquitran.mdx).
  • UI strings: messages/{locale}.json, namespaced; consume with useT(namespace) (client) or getMsg (server); add keys to all five locales.
  • HomePhrases holds WordConfig[] arrays consumed by XHomeColors.
  • Graph: src/lib/poemParser.ts aggregates all books + flat blog articles.
  • Full contracts and checklists: content-pipeline.md.

8. Components (quick index)

Barrel import surface: import { ... } from "@/app/components/xcomponents".

  • npm: XNavbar, XFooter, XSeparator, XZigZagLayout, XBookReader, XBookReaderIllus, XInteractivePhrase, XDecryptedText, XCompleteBook, XBookFullDecrypt, XContactForm, XNewsletter, XSocialContact.
  • local reusable: XGlassNavbar, XZigZagLayoutVideo, XHomeColors, XBookColors, BookReaderPoems, XMinimalFooter.
  • app-level: ClientComponentHome, AsciiLoadingAnimation, LenisProvider, transitionProvider, ConditionalSeparator, XFooterComponent, PublicKeyCard, SocialGrid, ParticlesBackground, BlogListClient, ObrasClientPage, XBlogDecrypt, XTextDecrypt, GrafoPoetico, reader wrappers (AsintotaBook, CielosDeAlquitranBook, AmapolasBook), navbarIcons.

Props, behavior and recipes: component-catalog.md.

XContactForm, XNewsletter and XSocialContact are re-exported but not rendered by any page today; there is no newsletter backend. Don’t assume they are wired up.


9. Build & deployment

npm install
npm run dev            # http://localhost:3000
npx tsc --noEmit       # authoritative type check (next lint is flaky on Next 16)
npm run build          # next build → out/ + postbuild next-sitemap
  • Static export to out/; robots.txt + sitemaps are generated.
  • Security headers live in public/.htaccess (Apache: HSTS, CSP, nosniff, frame/referrer/permissions, sensitive-file block, cache, deflate) and public/_headers (subset for _headers platforms). 'unsafe-inline' is required by the static export.
  • public/x-public.asc is fetched at runtime by PublicKeyCard.
  • Background frame counts must match each reader’s TOTAL_BG (asintota 72, amapolas 31, cielos 30).

Details and failure modes: build-and-deploy.md.


10. Conventions & guardrails

  1. Use tokens, not raw colors (except the documented palettes).
  2. Text over art goes on a marker chip.
  3. Import reusable components via the "use client" barrel. Never import @xscriptor/xcomponents directly in a Server Component.
  4. Never fork npm components locally — update @xscriptor/xcomponents and bump the version. Re-apply the patches in docs/xcomponents-fixes.md if you upgrade.
  5. Pages are thin server components; interactivity lives in client components.
  6. Copy goes in messages/*.json for all five locales (plus the documented in-component dictionaries where they exist).
  7. Add keys to every locale and, for content, add the same slug across locale folders.
  8. Respect prefers-reduced-motion in any new animation.
  9. Use clamp() for fluid padding; avoid bare vw.
  10. No runtime/server features (route handlers, middleware, revalidate).
  11. Validate before finalizing: npx tsc --noEmit then npm run build.
  12. Don’t commit secrets (there are none by design).

Known gotchas (see architecture.md §11 / code-structure.md §6)

  • Locale list is duplicated in ~9 files; the footer and 404 only handle en|es|de and fall back to Spanish for it/fr.
  • Two UI-text systems (messages vs in-component dictionaries) must be kept in sync.
  • A local unreferenced xcompletebook/XCompleteBook.tsx duplicate exists.
  • gsap and react-intersection-observer are installed but unused in app code.
  • Global a:hover { color:#ff4141 } fights accent links.
  • .htaccess blocks direct .json/.md serving (fine for this build).

11. Editing recipe (agent checklist)

  1. Locate the route entry and its client component; read the matching reference (component-catalog.md, book-readers.md, …).
  2. Plan against the design system: tokens, marker chips, motion budget.
  3. Implement at the right layer (route vs reusable vs lib vs content vs messages).
  4. Localize every string across the five locales; add content slugs in all five folders.
  5. Verify light/dark, <768px, and reduced-motion.
  6. Type-check with npx tsc --noEmit, then build with npm run build.
  7. Report what changed and any structural debt you encountered.

12. Output expectations

A correct change to this site is:

  • structurally consistent with the existing layers and import rules,
  • visually quiet, typography-driven, on the marker/glass system,
  • correct in light and dark themes and on mobile,
  • localized in all five languages,
  • accessible (sr-only text for decrypt animations, focus states, reduced motion),
  • static-export-safe,
  • and easy for a human to maintain afterwards.

If a solution is clever but harder to keep consistent with the above, reject it.

References (7)

architecture

Xscriptor Architecture

Technical map of xscriptor.com: stack, rendering model, route table, providers, i18n, theming, SEO and security. Read alongside code-structure.md (where code goes) and component-catalog.md (what each component does).

Repository: github.com/xscriptor-web/xscriptor.com (branch main, single history). The Next.js app lives at the repository root.


1. Tech stack

| Layer | Choice | Notes | |-------|--------|-------| | Framework | Next.js 16.3.x (App Router) | output: "export", trailingSlash: true | | Language | TypeScript 5 + some JS | strict: true, path alias @/* → ./src/* | | UI runtime | React 19.2 | Server Components by default | | Styling | Tailwind CSS 4.3 (PostCSS plugin) + CSS Modules | global CSS via @import "tailwindcss" | | Fonts | EB Garamond (self-hosted woff2) | Regular + Italic | | Motion | framer-motion 12 | route transition, modals, spans | | Scroll | lenis 1.3 | smooth scrolling (LenisProvider) | | Content | Markdown/MDX + gray-matter + remark/rehype | GFM, KaTeX, highlight | | Markdown stack | remark-parse, remark-gfm, remark-math, remark-rehype, rehype-katex, rehype-highlight, rehype-stringify, unified, katex | in src/app/lib/articles.ts | | Components | @xscriptor/xcomponents 0.2.3 (npm) + local copies | see component-catalog.md | | Backgrounds | @xscriptor/xbackgrounds 0.1.0 | XParticles | | QR | qrcode 1.5 | PGP fingerprint QR | | Sitemap | next-sitemap 4.2 | runs on postbuild | | Hosting | Static host (Apache .htaccess, plus _headers) | no server runtime |

react-intersection-observer and gsap are declared as dependencies. The app code uses raw IntersectionObserver and requestAnimationFrame almost everywhere; treat those two packages as candidates for removal unless a component explicitly imports them (grep before deleting).

tailwind.config.* does not exist. Tailwind v4 is configured entirely via postcss.config.mjs (plugins: ["@tailwindcss/postcss"]) and the @import "tailwindcss" line in globals.css. @theme is not used — theming is done with plain CSS custom properties.


2. Rendering model

  • Fully static export. next.config.ts:

    const nextConfig: NextConfig = {
      output: "export",
      trailingSlash: true,
      images: { unoptimized: true },
    };
    

    There is no Node runtime in production. Every route must be pre-renderable.

  • Server/Client boundary.

    • page.tsx files are Server Components. Book pages read MDX from disk at build time (fs.readFileSync) and pass raw text to a client reader.
    • Interactive components are marked "use client" (readers, navbar, home, contact, info, blog list, transitions, etc.).
    • Root and [locale] layouts are server components that mount client providers (I18nProvider, TransitionProvider, LenisProvider).
  • The "use client" barrel. Local reusable components are re-exported from src/app/components/xcomponents/index.ts, which starts with "use client". This is the boundary server components import from (see §6).

  • generateStaticParams. The [locale] layout returns the five locales; book/article routes additionally enumerate their own params.

  • Data at build time. src/app/lib/articles.ts and src/lib/poemParser.ts use fs, so they can only run inside Server Components / build.


3. Route map

All user-facing pages live under the dynamic [locale] segment (no separate non-localized route tree exists in this repo).

| Route | File | Rendering | Data source | |-------|------|-----------|-------------| | / | src/app/page.tsx | Static (locale = es) | delegates to ClientComponentHome | | /[locale] | [locale]/page.tsx | Static per locale | ClientComponentHome | | /[locale]/blog | [locale]/blog/page.tsx | Static per locale | getSortedArticles(locale) → BlogListClient | | /[locale]/blog/[...slug] | [locale]/blog/[...slug]/page.tsx | Static per locale + slug | getArticleData(slug, locale) → XBlogDecrypt | | /[locale]/contacto | [locale]/contacto/page.tsx | Static | ContactoClientPage → PublicKeyCard | | /[locale]/info | [locale]/info/page.tsx | Static | InfoClientPage (press cards, timeline) | | /[locale]/links | [locale]/links/page.tsx | Static (server component) | inline LINKS + LinksPage messages | | /[locale]/obras | [locale]/obras/page.tsx | Static | ObrasClientPage (book gallery) | | /[locale]/obras/boulevard | .../boulevard/page.tsx | Static | MDX boulevard/{locale}.mdx → npm XBookReader | | /[locale]/obras/asintota | .../asintota/page.tsx | Static | MDX asintota/{locale}.mdx → AsintotaBook → npm XCompleteBook | | /[locale]/obras/cielos-de-alquitran | .../cielos-de-alquitran/page.tsx | Static | MDX cielos-de-alquitran/{locale\|cielos-de-alquitran}.mdx → local XBookColors | | /[locale]/obras/colaterales | .../colaterales/page.tsx | Static | MDX → local BookReaderPoems | | /[locale]/obras/primavera-en-el-desierto | .../primavera-en-el-desierto/page.tsx | Static | MDX → local XBookColors | | /[locale]/obras/la-danza-de-las-amapolas | .../la-danza-de-las-amapolas/page.tsx | Static | MDX → local XBookColors (page) / AmapolasBook → npm XCompleteBook with renderPoem | | /[locale]/obras/grafo | .../obras/grafo/page.tsx | Static | getAllPoems(locale) → GrafoPoetico | | /[locale]/terminos-y-condiciones | .../terminos-y-condiciones/page.tsx | Static | TermsClientPage | | * (404) | src/app/not-found.tsx | Client | usePathname() → locale-specific copy |

There is no top-level /blog, /obras, /contacto, etc. Any legacy reference to app/blog/page.tsx routes is stale. src/app/blog/ and src/app/obras/ currently hold only CSS modules consumed by the [locale] pages; src/app/contacto/ and src/app/info/ hold only their *.module.css.

Hero sequences

  • Home (ClientComponentHome): a timed ASCII intro overlay (AsciiLoadingAnimation, ~1.2 s minimum, frames auto-advance every 35 ms, hard cap 4 s), then two XZigZagLayoutVideo blocks — a left-start block with six XHomeColors phrases and a right-start block with eight more. Background images alternate per block; a scroll-drawn SVG line connects the markers.
  • Obras / Blog galleries: full-viewport stacked sections that blur/dim the non-centered item, based on a scroll listener computing the section nearest the viewport center (activeIndex).
  • Readers: full-viewport background frames plus a paginated content column.

4. Layout shell & providers

src/app/layout.tsx (server) is the root:

<html lang="es" suppressHydrationWarning>
  <head>
    <Script id="theme-init" strategy="beforeInteractive">
      {`(function(){try{var t=localStorage.getItem("theme");if(t==="dark"){document.documentElement.setAttribute("data-theme","dark")}}catch(e){}})();`}
    </Script>
  </head>
  <body className="min-h-screen flex flex-col bg-(--bg) text-(--text) overflow-x-hidden">
    <a href="#main-content" className="skip-to-content">Saltar al contenido principal</a>
    <I18nProvider locale="es" messages={esMessages}>
      <LenisProvider />
      <TransitionProvider />
      <ConditionalSeparator />
      <main id="main-content" className="px-4 sm:px-6 lg:px-8 pt-16">{children}</main>
      <ConditionalSeparator />
      <SiteXFooter />
    </I18nProvider>
  </body>
</html>

Key points:

  • Root hardcodes Spanish (so / is Spanish). The [locale] layout mounts a nested I18nProvider with the requested locale; the inner one wins for that subtree.
  • import "@xscriptor/xcomponents/styles.css" is loaded in the root layout — the npm package's component styles ship there.
  • Global chrome order: smooth-scroll provider → route-transition/navbar/language dock → conditional separator → main → separator → footer.
  • main has pt-16 to clear the fixed navbar and horizontal responsive padding.
  • No <head> font preload tags; fonts are declared in globals.css.

[locale]/layout.tsx:

  • generateStaticParams() → ["es","en","de","it","fr"].
  • generateMetadata() builds <link rel="alternate"> hreflang tags and a canonical URL (https://xscriptor.com for es, .../{locale} otherwise).
  • Picks messages from the five imported JSON files, validates the locale (es fallback), mounts I18nProvider + UpdateHtmlLang.

5. i18n system

Five locales: es, en, de, it, fr. Spanish is the fallback.

Message store

messages/{locale}.json — 926 lines each, identical key shape. Namespaces:

Layout, Navbar, Footer, HomePage, BlogPage, ObrasPage, LiteraturaPage, Books, ContactPage, ContactForm, Newsletter, BookReader, InfoPage, LinksPage, TermsPage, HomePhrases, UnderConstruction.

HomePhrases is special: it holds WordConfig[] arrays (not strings) consumed by XHomeColors (see content-pipeline.md).

Client runtime — src/app/i18n-provider.tsx

  • I18nProvider({ locale, messages }) stores { locale, messages } in React Context.
  • useLocale() → current Locale, defaults to "es" when outside a provider.
  • useT(namespace?) → t(key, params?):
    • resolves dot-paths (resolvePath) against the namespace,
    • interpolates {param} tokens,
    • returns the key itself if a lookup misses,
    • has t.raw<T>(key) for arrays/objects (used for pressCards, timeline, HomePhrases).
  • The Locale type is a strict union; adding a locale requires updating it here and in every duplicate locale list across the app (see gotchas in §9).

Server helper — src/app/lib/i18n-utils.ts

getMsg(messages, "Namespace.key") — a non-hook dot-path resolver used by Server Components for generateMetadata/metadata. It returns the key on miss (no interpolation).

Content localization

  • Blog articles: src/app/content/articulos/{locale}/**/*.md, with es fallback handled in articles.ts.
  • Books: src/app/content/{book}/{locale}.mdx (exception: cielos-de-alquitran ES file is cielos-de-alquitran.mdx — see poemParser.ts/page code).
  • Pages read their locale via params and fall back to es when a file or locale is missing.

Nav chrome i18n

TransitionProvider keeps its own per-locale label dictionaries (LOCALE_KEYS, NAV_LABELS, langLabels) rather than reading messages/. This is a deliberate (but duplicated) convenience — see component-catalog.md and the gotchas below.


6. Component system overview

Two tiers:

  1. @xscriptor/xcomponents (npm) — the published library (styles.css, content types). Imported through the local barrel.
  2. Local components — either bespoke (XGlassNavbar, XBookColors, XHomeColors, BookReaderPoems, XZigZagLayoutVideo, XMinimalFooter) or app-level (XBlogDecrypt, XTextDecrypt, GrafoPoetico, PublicKeyCard, SocialGrid, ParticlesBackground, AsciiLoadingAnimation, ClientComponentHome, LenisProvider, TransitionProvider, ConditionalSeparator, XFooterComponent, navbar icons).

The barrel src/app/components/xcomponents/index.ts is the import surface:

"use client";
export { XNavbar } from "@xscriptor/xcomponents";
export { XGlassNavbar } from "./xglassnavbar";
export { XZigZagLayoutVideo } from "./xzigzagvideo";
export { XFooter, XSeparator, XZigZagLayout } from "@xscriptor/xcomponents";
export { XBookReader, XBookReaderIllus, XInteractivePhrase, XDecryptedText, XCompleteBook, XBookFullDecrypt } from "@xscriptor/xcomponents";
export { default as XMinimalFooter } from "./xminimalfooter/XMinimalFooter";
export { XBookColors } from "./xbookcolors";
export { XHomeColors } from "./xhomecolors";
export { BookReaderPoems } from "./BookReaderPoems";
export { XContactForm, XNewsletter, XSocialContact } from "@xscriptor/xcomponents";

Why the barrel exists: the npm bundle's chunk-*.mjs does not preserve the "use client" directive, so Server Components that fs.readFileSync (book pages) would crash with useState is not a function when importing client-hook components directly. The barrel re-adds the boundary.

Install docs and patches:

  • docs/STRUCTURE.md — high-level structure (Spanish).
  • docs/xcomponents-fixes.md — required reading before upgrading @xscriptor/xcomponents; documents the two local XGlassNavbar patches (active-link detection, theme persistence) and why they exist.

7. Theme system

  • Attribute data-theme on <html>; values "dark" (attribute present) or light (attribute absent). :root.dark / :root.light classes coexist for component CSS that keys off classes.
  • Persisted in localStorage under the key theme ("dark" | "light").
  • Before first paint: a next/script beforeInteractive snippet in the root layout reads localStorage and sets data-theme="dark" to avoid a flash.
  • Toggle: the navbar logo (center) is the theme button (XGlassNavbar logoAsThemeToggle), with sun/moon icons from navbarIcons.tsx. Mobile has a dedicated theme row in the overlay menu.
  • Hydration-safe sync: the navbar renders the same default on server and client, reads localStorage in a post-hydration useEffect, and gates persistence behind a themeReady flag so the default light value never overwrites a stored "dark". (<html suppressHydrationWarning> absorbs the attribute difference.)
  • ParticlesBackground mirrors data-theme to dark/light classes on <html> so PublicKeyCard's :root.light/:root.dark neumorphism responds.
  • Components that need live theme reads use a MutationObserver on documentElement filtered to data-theme (useDetectTheme in XHomeColors, XBookColors, GrafoPoetico).
  • Global theme transition: background/color 0.3s ease on html, body.

8. SEO & metadata

  • Root metadata provides metadataBase: https://xscriptor.com, a title template %s | Xscriptor, description, OpenGraph (es_ES, 1200×1200 /xscriptor-signature.png), Twitter summary_large_image, robots index,follow.
  • Each page exports generateMetadata (or metadata) and sets a per-page openGraph.locale via the same inline ternary es_ES/en_US/de_DE/it_IT/fr_FR.
  • [locale]/layout.tsx adds canonical + hreflang alternates for the five locales.
  • next-sitemap.config.js: siteUrl: https://xscriptor.com, generateRobotsTxt: true, sitemapSize: 10000, exclude: ['/api/*', '/blog/post'] (the excludes are legacy but harmless).
  • robots.txt and sitemap.xml/sitemap-0.xml are generated by the postbuild step — never hand-author them in public/.
  • Icons: src/app/favicon.ico and src/app/icon.svg use Next's file conventions. public/xscriptor-favicon.png and xscriptor-signature.* are static assets referenced by metadata/UI.

9. Security & hosting headers

Two header sources ship in public/:

  • public/.htaccess (Apache — primary production target):
    • Static-export rewrite: clean URLs → $1.html, unknown paths → index.html.
    • Headers: HSTS (max-age=31536000; includeSubDomains; preload), X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, Referrer-Policy: strict-origin-when-cross-origin, Permissions-Policy (camera/microphone/geolocation/interest-cohort all ()), and a CSP: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; media-src 'self'; connect-src 'self'; frame-ancestors 'self'; form-action 'self'.
    • Blocks direct access to .env|.git|.json|.lock|.md|.mdx files.
    • Cache-Control via mod_expires (media/fonts 1 year, css/js 1 month) and mod_deflate.
  • public/_headers (Netlify/Cloudflare-Pages style): a subset (nosniff, X-Frame-Options: DENY, referrer policy, permissions policy).

'unsafe-inline' in the CSP is required by Next.js static export. A hash/nonce CSP would require server rendering, which this site does not have.

There is no PHP endpoint, no API route, and no newsletter backend in the current repository. public/x-public.asc is a static PGP public key fetched by fetch("/x-public.asc").


10. Environment variables

None. The project runs with zero env configuration: all URLs are hardcoded (https://xscriptor.com), all data is on disk, all external contact channels are static links (mailto:, Telegram, WhatsApp, social).


11. Known architectural gotchas

  1. Duplicated locale lists. The five-locale set is re-declared in i18n-provider.tsx, articles.ts, poemParser.ts, [locale]/layout.tsx, transitionProvider.tsx, ConditionalSeparator.tsx, not-found.tsx, XFooterComponent.tsx, and GrafoPoetico label maps. Adding/removing a locale is a multi-file change.
  2. Inconsistent locale regex. TransitionProvider and ConditionalSeparator match all five locales; XFooterComponent and not-found.tsx use /^\/(en|es|de)/ and therefore fall back to es for Italian and French paths. Fix both when touching footer/404 locale detection.
  3. Two UI text systems. messages/*.json (namespaced, provider-based) and hardcoded per-locale dictionaries inside components (TransitionProvider, GrafoPoetico, reader section names, not-found). Keep both in sync.
  4. Unused local duplicate. src/app/components/xcomponents/xcompletebook/XCompleteBook.tsx is a local implementation, but the barrel exports the npm XCompleteBook. The local file is currently unreferenced — confirm before editing or deleting.
  5. gsap / react-intersection-observer are installed but not imported in app code; candidates for dependency cleanup.
  6. .htaccess blocks .json/.md serving. Does not affect the built site (data is inlined into JS/HTML), but do not plan to serve raw content files.
  7. Path aliases. Use @/app/... and @/lib/... (mapped to ./src/*). Relative imports with deep ../../../../messages/... also appear in pages — both styles exist; prefer @/.
  8. next lint is unreliable on Next 16; validate types with npx tsc --noEmit (see build-and-deploy.md).
book-readers

Xscriptor Book Reader Engines

The site has four different book-rendering engines. They share concepts (blank-line blocks, section titles, index, 4/5-item pagination, full-bleed backgrounds, decrypting words) but differ in parsing detail and interaction. Pick the engine deliberately; do not rewrite one into another.

| Engine | Origin | Used by | Signature | |--------|--------|---------|-----------| | XBookReader | npm @xscriptor/xcomponents | Boulevard | simplest paginated reader | | XCompleteBook | npm @xscriptor/xcomponents | Asintota, La danza de las amapolas | section/index pages + renderPoem override | | XBookColors | local xcomponents/xbookcolors | Cielos de Alquitrán, Primavera | per-block background rotation + per-word colors/decrypt | | BookReaderPoems | local xcomponents/BookReaderPoems | Colaterales | index-driven poem reordering + titles |

There is also an unused local copy at xcomponents/xcompletebook/XCompleteBook.tsx. The barrel exports the npm XCompleteBook, so the local file is dead code today. Verify before editing.


1. Shared content model

All engines consume the raw MDX string and split it into blocks on blank lines (2+ or 3+ newlines depending on the engine). Blocks become typed items:

type PageItem = {
  type: "section_title" | "content" | "poem_block" | "image" | "index";
  content: string;
  title?: string;        // poem_block (BookReaderPoems)
  entries?: IndexEntry[];// index
  alt?: string; src?: string; // image
};
  • Section title — first line matches the engine’s SECTION_NAMES[locale] (case-insensitive). Each becomes its own page.
  • Image — block exactly ![alt](src).
  • Index — trailing block beginning with the locale index header (Índice/Index/Inhalt/Indice/Index). Stripped from the body and rendered as a dedicated page; entry lines containing ....123 or - 123 are poem entries, others are section names.
  • Content / poem — everything else.

Pagination grouping (identical in all engines)

function groupIntoPages(items, withCover) {
  const pages = [];
  if (withCover) pages.push([]);            // page 0 = cover
  let page = [];
  for (const item of items) {
    if (item.type === "section_title" || item.type === "index") {
      if (page.length) { pages.push(page); page = []; }
      pages.push([item]);                    // section/index always alone
      continue;
    }
    page.push(item);
    const maxPerPage = pages.length % 2 === 0 ? 4 : 5;  // alternate 4 / 5
    if (page.length >= maxPerPage) { pages.push(page); page = []; }
  }
  if (page.length) pages.push(page);
  return pages;
}

Cover page (currentPage === 0 && coverImage) shows just the cover image; section pages show a centered title; index pages show the index list; content pages render a zigzag of blocks. Navigation scrolls the reader into view with scrollIntoView({ behavior: "smooth", block: "start" }).

Default labels (per locale)

DEFAULT_LABELS[locale] = { prev, next, pageOf: "Página {current} de {total}", index } (Previous/Next, Zurück/Weiter, Precedente/Successivo, Précédent/Suivant). {current} and {total} are manually split around the page-number input, not interpolated by a formatter. BookReaderPoems instead pulls labels from useT("BookReader").

Word parsing (for decrypting poem text)

Every engine that renders poems converts each poem into WordConfig[]:

// split into lines → words → chunks of (1 + word.length % 3) words
const hash = chunkText.length + chunkIndex * 3 + lineIndex * 5;
if (hash % 7  === 0) type = "underline";
else if (hash % 11 === 0) type = "button";
else if (hash % 13 === 0) type = "blur1";
else if (hash % 17 === 0) type = "blur2";
italic = hash % 4 === 0;
bold   = hash % 9 === 0;
breakAfter = last chunk in line;

Then a safety pass guarantees at least one underline, button, blur1, blur2 exist (otherwise blurred words could never be revealed). For very short poems (≤3 chunks) the assignment is explicit:

| chunks | distribution | |--------|--------------| | 1 | underline | | 2 | underline + blur1 | | 3 | underline + blur1 + button |

XBookColors (XBookFullDecrypt) and XCompleteBook implement this safety pass; BookReaderPoems overrides the last normal as button if none exists.


2. XBookColors (local) — deep dive

File: src/app/components/xcomponents/xbookcolors/XBookColors.tsx (+ .module.css).

Props

{
  rawText: string;
  coverImage?: string;
  backgroundImage?: string;
  backgroundVideo?: string;
  bgConfig?: { basePath: string; total: number; digits?: number; format?: string };
  locale?: "es"|"en"|"de"|"it"|"fr";
  overlayColor?: string;                 // default rgba(0,0,0,0.45)
  labels?: { prev?; next?; pageOf?; index? };
  onPageChange?: (page: number) => void;
  sectionNames?: Record<Locale, string[]>;
}

Backgrounds

resolvedBg = bgConfig ? ${basePath}${pad((bgIndex % total) + 1, digits ?? 2)}.${format ?? "webp"} : backgroundImage.

bgIndex is set to the current page on navigation, so backgrounds rotate in lockstep with pages. The wrapper fixed-bleeds a <video> or <img> plus a bgOverlay tint.

Rendering

  • Cover page, section-title page, index page exactly as the shared model.
  • Content pages: XZigZagLayout (startSide="left", gap 6, offset clamp(1rem,4vw,4rem), textAlign="side", showLine, lineColor = wordHex[BLOCK_COLORS[currentPage % 5]], lineThickness 0.5) with each item either an image block or a XBookFullDecrypt poem block.
  • Pagination: SVG chevrons, split pageOf template + numeric input (digits-only, Enter/blur to jump, resets out-of-range).

Theme + colors

useDetectTheme (MutationObserver on data-theme) selects DARK_WORD_HEX / LIGHT_WORD_HEX and DARK_COLORS / LIGHT_COLORS (sets --c0…--c7), applied as inline style on the wrapper. XBookFullDecrypt cycles WORD_COLORS every 2 chunks and applies color + --c-word inline; blur is applied via inline styles (filter: blur(10px); opacity:.3) because CSS-class swapping did not reliably win the cascade (documented in xbookcolors/README.md).

Wrappers that rotate backgrounds

CielosDeAlquitranBook and AmapolasBook are client wrappers that keep their own bgIndex, compute the next frame, and preload it with new Image() before passing backgroundImage to XCompleteBook. Note the page routes for Cielos/Primavera/Amapolas use XBookColors directly with bgConfig when a background set exists (Amapolas page uses XBookColors + bgConfig; the AmapolasBook wrapper is a separate XCompleteBook variant — inspect the route before editing to know which is live).


3. XCompleteBook (npm, via barrel) — deep dive

File: npm @xscriptor/xcomponents; local duplicate at xcomponents/xcompletebook/XCompleteBook.tsx mirrors the logic.

Props

{
  rawText: string;
  coverImage?: string;
  backgroundImage?: string;
  backgroundVideo?: string;
  locale?: "es"|"en"|"de"|"it"|"fr";
  overlayColor?: string;
  labels?: { prev?; next?; pageOf?; index? };
  onPageChange?: (page: number) => void;
  sectionNames?: string[];                       // for THIS book, current locale
  renderPoem?: (content: string) => ReactNode;   // replaces XInteractivePhrase
}

Differences from XBookColors

  • Content pages use XZigZagLayout with lineColor="var(--accent)", lineThickness 0.2, and each poem is rendered by XInteractivePhrase (npm) unless renderPoem is supplied.
  • Default section names are geometry-themed (CURVA, PLANO CARTESIANO, RECTA, I…V); AmapolasBook overrides them with the poppy sections.
  • No per-block background rotation; the wrapper passes a single rotating backgroundImage.

Usage

  • Asintota: AsintotaBook rotates 72 backgrounds (bgConfig equivalent) and passes per-locale labels; renderPoem not set (uses XInteractivePhrase).
  • Amapolas wrapper: passes sectionNames and renderPoem={(c) => <XBookFullDecrypt content={c} />} to get the colored per-word decrypt. (The Amapolas route currently uses XBookColors with bgConfig instead — confirm which is active before changing.)

4. BookReaderPoems (local) — deep dive

File: src/app/components/xcomponents/BookReaderPoems.tsx (+ .module.css).

Used by Colaterales. Its distinguishing feature is that it treats the index as a table of contents that dictates poem order.

Parsing pipeline

  1. parseIndex(rawText, locale):
    • finds the trailing index header block,
    • walks its lines: known section names start a section; lines with ....123 are poem titles (normalized whitespace, lowercased as keys);
    • returns { knownTitles: Map<key, displayTitle>, sectionTitles: Set, orderedTitles: Map<section, key[]> }.
  2. parseContent:
    • removes the index from the body, splits into blank-line blocks;
    • section-title and image blocks as usual;
    • inside a poem section, a line matching a known title starts a new poem_block; remaining lines become its content;
    • outside sections, a single all-caps line becomes an empty poem_block.
  3. Merge pass: a poem_block with empty content followed by a content block is merged into one poem.
  4. reorderPoems: for each section, sorts its poem_blocks by the index order, re-attaching any non-poem blocks to the poem that preceded them (or moving them to the section start if they preceded all poems).
  5. Index page re-appended as a PageItem.

Render

Content pages use XZigZagLayout (accent line, thickness 0.2) and render each poem as <h3 class=poemTitle> + XInteractivePhrase. poemWrapper p forces line-height: 2 !important. Pagination mirrors the shared pattern but uses useT("BookReader"). The container is a plain column (min-height: 80vh) — no full-bleed background.

Section names (Colaterales) per locale: TACTO/TRANSFUSIONES/PERSPICACIA/ INTERSECCIÓN/INCERTIDUMBRE (translated for all five). Index headers here use Inhaltsverzeichnis for German (different from the other engines’ Inhalt) — a known inconsistency.


5. XBookReader (npm) — Boulevard

[locale]/obras/boulevard/page.tsx renders the npm XBookReader directly:

<XBookReader
  rawText={rawText}
  coverImage="/images/colecciones/libros/boulevard.webp"
/>

Minimal wrapper (main.min-h-screen.py-16.px-4.md:px-8.max-w-7xl.mx-auto). No background rotation, no section overrides. Boulevard content files: src/app/content/boulevard/{locale}.mdx.


6. Choosing / adding an engine

  • Plain paginated poems, no art → XBookReader.
  • Section pages + index + a single background, generic word interaction → XCompleteBook.
  • Rotating full-bleed backgrounds and/or per-word color + decrypt → XBookColors (+ a wrapper if you want preloading).
  • Poems whose reading order is defined by an index / titles → BookReaderPoems.

When adding a book:

  1. Author the five MDX files.
  2. Add cover + background frames to public/images/obras/<book>/.
  3. Create [locale]/obras/<book>/page.tsx using the file-resolution pattern from content-pipeline.md.
  4. If backgrounds rotate, add a client wrapper (copy AsintotaBook) with the correct TOTAL_BG.
  5. Register the book in ObrasClientPage and (optionally) poemParser.
  6. Translate section-name tables and Books.* strings into all locales.

7. Known reader pitfalls

  • Background frame counts must match TOTAL_BG / bgConfig.total, else the reader requests a missing bgNN.webp (blank frame).
  • Section-name tables are per-engine and duplicated across locales; adding a section only in one locale makes it render as plain content in the others.
  • Index parsing differences (German Inhalt vs Inhaltsverzeichnis) mean the same book content cannot be moved between engines without adjusting the index header.
  • groupIntoPages alternates 4/5 items based on the current pages.length parity, so inserting/removing a section page shifts pagination for everything after it — expected, not a bug.
  • Short poems rely on the explicit type-assignment fallback; do not remove it or blurred-only poems become unrevealable.
  • Theme is read from data-theme only (no system preference); reader colors update via MutationObserver when the navbar toggles the theme.
build-and-deploy

Xscriptor Build & Deployment

Everything needed to build, validate and ship xscriptor.com, plus the static hosting contract. The site is a 100% static export — there is no server runtime, no PHP and no API.


1. Scripts (package.json)

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "postbuild": "next-sitemap"
  }
}
  • npm run dev — local dev server on http://localhost:3000.

  • npm run build — production build. next build writes the static site to out/, then postbuild runs next-sitemap (npm automatically runs postbuild after build).

  • npm run start — serves the production build locally (not used in production; the site is static).

  • npm run lint — next lint. On Next 16 this can be unreliable; the project’s actual validation is TypeScript:

    npx tsc --noEmit
    

Requirements: Node 18+ and npm. No environment variables are required.


2. Next.js configuration

next.config.ts:

const nextConfig: NextConfig = {
  output: "export",        // static HTML export to out/
  trailingSlash: true,     // every route emitted as /path/index.html
  images: { unoptimized: true },  // no Image Optimization API at runtime
};

export default nextConfig;

Consequences:

  • Every route must be statically renderable. Do not add route handlers, middleware, revalidate, server actions, or anything that needs a request.
  • images.unoptimized means next/image acts like a plain <img> (used in InfoClientPage for the signature). Remote images must be plain <img>.
  • trailingSlash: true is why .htaccess rewrites /path/ → /path.html.
  • postcss.config.mjs registers @tailwindcss/postcss; Tailwind v4 has no tailwind.config.* — theme tokens are plain CSS variables in globals.css.

TypeScript (tsconfig.json)

  • strict: true, target ES2020, moduleResolution: "bundler", jsx: "react-jsx", resolveJsonModule: true (Top-level JSON imports such as ascii-frames.json and the message files rely on this).
  • Path alias: "@/*": ["./src/*"] → use @/app/..., @/lib/....
  • exclude: ["node_modules", "web"].

3. Build output layout

next build
  → out/                       # deployable static site
       index.html              # /
       es/index.html           # /[locale] for each locale
       es/blog/index.html
       es/blog/<slug...>/index.html
       es/obras/<book>/index.html
       ...
       _next/static/...        # JS/CSS chunks
       images/, fonts/, *.png, x-public.asc
       404.html
       robots.txt              # generated by next-sitemap (postbuild)
       sitemap.xml
       sitemap-0.xml
       .htaccess               # copied from public/
next-sitemap (postbuild)

Rules:

  • Files in public/ are copied verbatim to out/ at the same paths.
  • robots.txt, sitemap.xml and sitemap-0.xml are generated; never add source copies in public/ or next-sitemap may skip/override them.
  • To change .htaccess, edit public/.htaccess and rebuild (it is copied).
  • To change the domain in the sitemap, edit next-sitemap.config.js siteUrl.

4. Sitemap & robots — next-sitemap.config.js

module.exports = {
  siteUrl: 'https://xscriptor.com',
  generateRobotsTxt: true,
  sitemapSize: 10000,
  exclude: ['/api/*', '/blog/post'],  // legacy paths; safe to trim
};
  • postbuild regenerates robots.txt (allows crawlers, points to the sitemap) and the sitemaps.
  • The exclude entries are historical (no API or /blog/post route exists). Removing them is safe but low-value.
  • Per-page priorities/changefreq are not customized.

5. Security headers

Two header files are shipped because the site can be hosted on either Apache (primary, via .htaccess) or a _headers-aware platform.

public/.htaccess (Apache)

  • Static rewrite for trailingSlash output:
    RewriteEngine On
    RewriteRule ^index\.html$ - [L]
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^(.*)/$ $1.html [L]
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^ /index.html [L]
    
  • Headers (via mod_headers):
    • Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
    • X-Content-Type-Options: nosniff
    • X-Frame-Options: SAMEORIGIN
    • Referrer-Policy: strict-origin-when-cross-origin
    • Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=()
    • Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; media-src 'self'; connect-src 'self'; frame-ancestors 'self'; form-action 'self'
  • Sensitive file blocking: <FilesMatch "\.(env|git|json|lock|md|mdx)$"> Require all denied</FilesMatch>.
  • Cache-Control (mod_expires): webp/jpg/png/svg and woff2 → 1 year; css/js and mp4 → 1 month.
  • Compression (mod_deflate): html, css, plain, xml, js, json, svg.

'unsafe-inline' in script-src/style-src is required by the Next.js static export (inline theme script, inline styles). A nonce/hash CSP is not possible without SSR. If you add a third-party script, you must extend the CSP explicitly.

public/_headers (Netlify/Cloudflare Pages style)

/*
  X-Content-Type-Options: nosniff
  X-Frame-Options: DENY
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: geolocation=(), microphone=(), camera=()

Note the intentional differences from .htaccess (DENY vs SAMEORIGIN, no HSTS/CSP here). Keep both in mind: which one applies depends on the host.


6. Hosting

  • Target: static hosting (Apache/Hostinger being the primary environment given .htaccess). Any static host works if it honors trailingSlash routes (Netlify/Cloudflare/Vercel static, GitHub Pages with rewrites, etc.).
  • Deploy = upload the contents of out/ (including .htaccess).
  • No build step on the server; no Node process.
  • public/x-public.asc is fetched at runtime by PublicKeyCard — ensure it is present in the deployed output (it is copied automatically).
  • public/images/** is large (pre-rendered book backgrounds: 72+31+30 frames). Consider CDN/caching; the .htaccess already sets 1-year caching for images.

7. Validation checklist before shipping

  1. npx tsc --noEmit — type check (authoritative; next lint may be flaky).
  2. npm run build — must succeed and generate out/ with robots.txt and sitemap.xml.
  3. Spot-check out/es/index.html, one book route, one article route.
  4. Confirm .htaccess and x-public.asc exist in out/.
  5. Verify both themes and <768px on the changed pages.
  6. Verify prefers-reduced-motion behavior if you touched animation.
  7. Never commit secrets; there are none in this repo by design.

Common build failures

| Symptom | Cause / fix | |---------|-------------| | useState is not a function during export | A Server Component imported a client component directly from @xscriptor/xcomponents. Import via the "use client" barrel instead. | | Missing page during export | A route lacks generateStaticParams or reads a file that is not present for some locale. | | Blank book frame | Background frame count ≠ TOTAL_BG/bgConfig.total. | | Sitemap missing | postbuild did not run; run npx next-sitemap after next build. | | Type error on JSON import | resolveJsonModule disabled or import path wrong. | | Stale sitemap/robots | A robots.txt was added to public/; remove it and rebuild. |

code-structure

Xscriptor Code Structure

Where code goes, what each layer is responsible for, and the rules that keep the site consistent. Canonical placement rules — the current tree is the source of truth; this document explains the intent behind it.


1. Current tree

xscriptor.com/
├── src/
│   ├── app/
│   │   ├── layout.tsx                  # root shell: metadata, theme script, providers, footer
│   │   ├── page.tsx                    # "/" (Spanish home)
│   │   ├── globals.css                 # Tailwind v4 import + tokens + global element rules
│   │   ├── i18n-provider.tsx           # I18nProvider, useLocale, useT (+ t.raw)
│   │   ├── not-found.tsx               # client 404
│   │   ├── favicon.ico, icon.svg       # file-convention icons
│   │   │
│   │   ├── [locale]/
│   │   │   ├── layout.tsx              # per-locale provider, hreflang, <html lang>
│   │   │   ├── page.tsx                # localized home
│   │   │   ├── UpdateHtmlLang.tsx
│   │   │   ├── blog/
│   │   │   │   ├── page.tsx            # listing (server) → BlogListClient
│   │   │   │   └── [...slug]/page.tsx  # article (server) → XBlogDecrypt
│   │   │   ├── contacto/
│   │   │   │   ├── page.tsx
│   │   │   │   └── ContactoClientPage.tsx
│   │   │   ├── info/
│   │   │   │   ├── page.tsx
│   │   │   │   └── InfoClientPage.tsx
│   │   │   ├── links/page.tsx
│   │   │   ├── obras/
│   │   │   │   ├── page.tsx
│   │   │   │   ├── ObrasClientPage.tsx
│   │   │   │   ├── boulevard/page.tsx
│   │   │   │   ├── asintota/{page.tsx, AsintotaBook.tsx}
│   │   │   │   ├── cielos-de-alquitran/{page.tsx, CielosDeAlquitranBook.tsx}
│   │   │   │   ├── colaterales/page.tsx
│   │   │   │   ├── primavera-en-el-desierto/page.tsx
│   │   │   │   ├── la-danza-de-las-amapolas/{page.tsx, AmapolasBook.tsx}
│   │   │   │   └── grafo/{page.tsx, GrafoPage.module.css}
│   │   │   └── terminos-y-condiciones/{page.tsx, TermsClientPage.tsx}
│   │   │
│   │   ├── components/
│   │   │   ├── clientcomponenthome.tsx         # home hero
│   │   │   ├── ClientComponentHome.module.css
│   │   │   ├── AsciiLoadingAnimation.tsx
│   │   │   ├── ascii-frames.json               # precomputed intro frames
│   │   │   ├── LenisProvider.tsx
│   │   │   ├── transitionProvider.tsx          # navbar + language dock + route overlay
│   │   │   ├── blog/BlogListClient.tsx
│   │   │   ├── icons/navbar/navbarIcons.tsx
│   │   │   ├── layout/ConditionalSeparator.tsx
│   │   │   ├── layout/footer/XFooterComponent.tsx
│   │   │   ├── publickey/{PublicKeyCard.tsx, PublicKeyCard.module.css}
│   │   │   ├── socialgrid/SocialGrid.tsx
│   │   │   ├── xbackgrounds/ParticlesBackground.tsx
│   │   │   ├── Xtexts/XTextDecrypt/{XTextDecrypt.tsx, index.ts}
│   │   │   └── xcomponents/                    # see below
│   │   │
│   │   ├── blog/
│   │   │   ├── BlogListPage.module.css         # CSS for BlogListClient
│   │   │   └── [slug]/ArticlePage.module.css   # CSS for the article page
│   │   ├── contacto/ContactPage.module.css
│   │   ├── info/InfoPage.module.css
│   │   ├── obras/ObrasPage.module.css
│   │   │
│   │   ├── content/                            # authored content (not code-gen)
│   │   │   ├── articulos/{locale}/**/*.md
│   │   │   └── <book>/{locale}.mdx
│   │   │
│   │   └── lib/{articles.ts, i18n-utils.ts}
│   └── lib/poemParser.ts
│
├── messages/{es,en,de,it,fr}.json
├── public/                                     # static assets + .htaccess + _headers
├── docs/{STRUCTURE.md, xcomponents-fixes.md}
├── next.config.ts, next-sitemap.config.js, postcss.config.mjs
├── eslint.config.mjs, package.json, tsconfig.json, README.md, CONTRIBUTING.md, CONTACT.md
└── LICENSE

src/app/components/xcomponents/ (local reusable UI)

xcomponents/
  index.ts                     # "use client" barrel (the import surface)
  xglassnavbar/{XGlassNavbar.tsx, XGlassNavbar.module.css}
  xzigzagvideo/{XZigZagLayoutVideo.tsx, .module.css}
  xhomecolors/{XHomeColors.tsx, .module.css}
  xbookcolors/{XBookColors.tsx, .module.css, README.md}
  xcompletebook/{XCompleteBook.tsx, .module.css}   # NOTE: currently unreferenced
  xminimalfooter/XMinimalFooter.tsx
  BookReaderPoems.tsx + BookReaderPoems.module.css
  grafo/{GrafoPoetico.tsx, GrafoPoetico.module.css}
  xblogdecrypt/{XBlogDecrypt.tsx, README.md, index.ts}

Naming quirk: most CSS modules live in non-route sibling folders (src/app/blog/, src/app/obras/, src/app/contacto/, src/app/info/) while the route pages live under [locale]/. Components import them with the absolute alias, e.g. import styles from "@/app/blog/BlogListPage.module.css". Follow this pattern rather than moving CSS next to the [locale] page.


2. Layer responsibilities

Route entry (page.tsx) — Server Component

  • Export metadata or generateMetadata (SEO + Open Graph locale).
  • For localized routes, await params to get the locale.
  • Fetch build-time data (getSortedArticles, getArticleData, getAllPoems, fs.readFileSync for books).
  • Delegate all interactivity to a client component (*ClientPage, readers, BlogListClient) — do not put hooks in the page.
  • Keep the page thin; layout lives in CSS modules or the client component.

layout.tsx

  • Root: global metadata, theme <Script>, I18nProvider(es), LenisProvider, TransitionProvider, separators, main, footer, skip link.
  • [locale]: validate locale, inject the correct messages, UpdateHtmlLang, emit hreflang alternates.
  • Do not add page-specific content to layouts.

Client page component (*ClientPage.tsx)

  • Owns local UI state and useT-based copy.
  • Composes reusable components; keeps JSX declarative.
  • Often pairs with ParticlesBackground (contact/info).

Reusable UI

  • Site-specific reusable blocks → src/app/components/<area>/ with a colocated *.module.css, exported through the barrel if server components need them.
  • Cross-project library components → @xscriptor/xcomponents (update the package + bump version; never fork).
  • Animation primitives (XTextDecrypt, XBlogDecrypt) stay in the app when they depend on app-specific content shape.

Data / parsing

  • src/app/lib/articles.ts — blog markdown pipeline (build-time only).
  • src/app/lib/i18n-utils.ts — getMsg for server components.
  • src/lib/poemParser.ts — aggregate poems for the graph (build-time only).
  • Any new fs-using parser: keep it in a lib/ module, call it only from server components.

Content

  • src/app/content/articulos/{locale}/ — blog markdown.
  • src/app/content/<book>/{locale}.mdx — book text.
  • Author content by hand; never generate it at runtime.

Messages

  • messages/{locale}.json — UI strings, namespaced.
  • Add keys to all five locales simultaneously.

CSS

  • Global tokens, element resets, .article-content, utilities, keyframes → globals.css only.
  • Route/section layout → the route’s *.module.css.
  • Component internals → the component folder’s *.module.css.
  • Prefer Tailwind utilities for spacing/layout in simple JSX; use CSS modules for anything stateful or repeated. Do not mix heavy Tailwind noise into a component that already has a module.

3. Import rules

  • Use the path aliases: @/app/..., @/lib/....
  • Import reusable components from the barrel: import { XBookColors } from "@/app/components/xcomponents";
  • Never import @xscriptor/xcomponents directly from a Server Component — go through the "use client" barrel (see architecture.md §6).
  • The barrel must stay "use client". If you add a component that needs client hooks, add its re-export line to index.ts.

4. Ideal rules for future changes

  • Route-only logic stays near the route; shared logic moves to lib/.
  • Reusable UI moves to xcomponents/ and, if browser-only, through the barrel.
  • Structured content moves to content/; interface copy moves to messages/.
  • Give a component its own module.css the moment it develops a visual language.
  • Prefer TypeScript for all new files; migrate stray .jsx/.js when touched.
  • Add new locales to every locale list (or better, centralize them — see §6).
  • Use clamp() for fluid sizing; cap wide-screen padding.
  • Verify both themes and <768px after any visual change.
  • Run npx tsc --noEmit then npm run build before finalizing.

5. Anti-patterns

  • Large inline data arrays inside page.tsx (put them in a const beside the route or in lib/).
  • Multiple unrelated widgets sharing one CSS module.
  • Hardcoded colors instead of var(--bg|--text|--accent|--border) (the book / graph palettes are the only exceptions).
  • Hardcoded user-facing text that never reaches messages/*.json (or, for the documented per-component dictionaries, at least translated in place).
  • Copying npm components into xcomponents/ instead of using the barrel.
  • Editing the npm @xscriptor/xcomponents package inside node_modules.
  • Importing @xscriptor/xcomponents directly in a Server Component.
  • Using vw for padding without clamp().
  • Adding a key to only one locale’s message file.
  • Hand-authoring robots.txt/sitemap.xml in public/ (next-sitemap owns them).
  • Adding server-only APIs (route.ts, middleware, revalidate) — export is static.
  • Using dangerouslySetInnerHTML on untrusted input (XBlogDecrypt/Grafo inject generated HTML into trusted, in-repo content only).

6. Known structural debt (fix opportunistically)

  1. Locale list duplication across ~9 files. A single src/lib/locales.ts exporting LOCALES, Locale, FALLBACK_LOCALE, INDEX_HEADERS, OG_LOCALE would remove most of it.
  2. Two locale regexes — XFooterComponent and not-found.tsx only handle en|es|de, so Italian/French fall back to Spanish. Align them with transitionProvider.
  3. Inline nav/reader dictionaries in components vs messages/. Migrate TransitionProvider labels and reader section names into messages when convenient.
  4. Unreferenced local XCompleteBook.tsx — delete or wire it up.
  5. Unused deps (gsap, react-intersection-observer) — confirm and prune.
  6. a:hover { color:#ff4141 } global override fights accent links; scope it.
  7. InfoPage 15vw padding should become clamp() for small screens.

7. Preferred editing strategy for AI agents

  1. Read the route entry (page.tsx) and its client component.
  2. Read the relevant CSS module and the data file it depends on.
  3. If a reusable block is needed, add it under components/ and, if it must be server-importable, to the barrel.
  4. Keep new copy in messages/*.json for all five locales (plus per-component tables where the codebase already does so).
  5. Respect the design system (design-system.md): tokens, marker chips, motion budget, reduced motion.
  6. Verify light/dark and mobile.
  7. npx tsc --noEmit → npm run build.
  8. Never commit secrets; never add a runtime server dependency.
component-catalog

Xscriptor Component Catalog

Every component in the project, what it does, its props/API, and where it lives. Use this before writing anything new: most needs are already covered by an existing component.

Legacy note: older docs list XContactForm, XNewsletter and XSocialContact as actively used. They are re-exported from the barrel (npm) but no page currently renders them — the contact page uses PublicKeyCard + SocialGrid. The newsletter backend does not exist. Do not assume they are wired up.


Part A — Barrel imports (src/app/components/xcomponents/)

index.ts ("use client") is the only sanctioned import surface for reusable components. Never import @xscriptor/xcomponents directly from a Server Component.

From @xscriptor/xcomponents (npm)

| Component | Purpose | |-----------|---------| | XNavbar | Legacy navbar (superseded by local XGlassNavbar) | | XFooter | Legacy footer primitive | | XSeparator | Decorative separator; supports orientation, variant (dashed), hasX, color, xColor, thickness, gap, isFaded, xBg, className | | XZigZagLayout | Alternating left/right content stack with an optional scroll-drawn SVG connector. Props: startSide, gap, offset, textAlign (inherit\|side\|left\|right), showLine, lineColor, lineThickness, className | | XBookReader | Paginated book reader (used by Boulevard) | | XBookReaderIllus | Illustrated book reader variant | | XInteractivePhrase | Renders WordConfig[] with interactive word types + per-word decrypt; prop as selects the tag; used by readers and XZigZagLayoutVideo | | XDecryptedText | Text-decrypt primitive | | XCompleteBook | Full book engine: cover, section titles, index, zigzag pages, pagination, optional background image/video, optional renderPoem override, sectionNames, onPageChange, labels | | XBookFullDecrypt | Poem renderer variant (per-word decrypt) exported by the npm package | | XContactForm / XNewsletter / XSocialContact | Re-exported but unused in current pages |

Local components

XGlassNavbar — xcomponents/xglassnavbar/XGlassNavbar.tsx

Apple-style floating glass navbar. The center logo doubles as the theme toggle. Desktop shows linksLeft/linksRight; below 768px it collapses to a fixed glass hamburger + full-screen overlay menu with a theme row.

Key props: linksLeft, linksRight ({ url, title, external?, showExternalIcon? }), logo, logoAsThemeToggle, onLogoClick, themeIcons: { toDark, toLight }, defaultTheme, storageKey (default "theme"), linkColor, linkHoverColor, linkActiveColor, iconColor, iconHoverColor, iconSize, hamburgerBarWidth, hamburgerBarThickness, cssVars, labels (labelOpen, labelClose, labelDark, labelLight, navLabel, menuLabel, linkLabelPrefix), themeToggleAriaLabel, themeToggleTitle, className.

Behavior worth knowing:

  • Hydration-safe theme sync with a themeReady gate (see architecture.md §7).
  • isActive(url): exact match, then startsWith(url + "/") with guards that a bare "/" or "/xx" locale prefix never matches everything (patched locally — see docs/xcomponents-fixes.md).
  • Locks body scroll via body.menu-open while the overlay is open; closes on route change and Escape.
  • Theme icon reveal on logo hover is pure CSS (width/opacity transition).
  • The mobile theme row is hidden when themeIcons is not provided.

XZigZagLayoutVideo — xcomponents/xzigzagvideo/XZigZagLayoutVideo.tsx

XZigZagLayout with an optional full-bleed background videoSrc/imageSrc and an overlay. Measures each child's center point with getBoundingClientRect, builds an SVG polyline, and animates strokeDashoffset with scroll progress (clamped [0,1], start at viewport mid). Adds fadeInUp on items when the wrapper enters the viewport (IntersectionObserver, threshold 0.15). Props: startSide, gap, offset, textAlign, showLine, lineColor, lineThickness, videoSrc, imageSrc, overlayColor, plus div props. Renders no background wrapper when neither videoSrc nor imageSrc is given.

XHomeColors — xcomponents/xhomecolors/XHomeColors.tsx

Renders a WordConfig[] phrase (tag selects h1..span) with the marker chip background, cycling book colors every third chunk. Word types: underline (toggles blur group 1), button (toggles blur group 2), blur1/blur2 (blur until their toggle is on), normal. Supports italic, bold, breakAfter; an empty text renders a line break. Focusable/clickable word types get role="button" + Enter/Space handling. Text is rendered through an inline per-char decrypt that animates on first view. Reads the theme via MutationObserver.

XBookColors — xcomponents/xbookcolors/XBookColors.tsx

Local reader used by Cielos de Alquitrán, Primavera and La danza de las amapolas (page-level). Parses raw MDX into section_title | image | content items + a trailing index, groups into pages alternating 4/5 items, renders each content page through XZigZagLayout + XBookFullDecrypt (private), and supports backgrounds from bgConfig (basePath, total, digits, format), backgroundImage or backgroundVideo with overlayColor. Also accepts coverImage, locale, labels, sectionNames, onPageChange. See book-readers.md for the parsing algorithm. Its README documents the resolved bugs: blur applied via inline styles, explicit short-poem type assignment, sr-only duplication, Roman-numeral sections, sequential decrypt, theme detection without system fallback, color cycling.

BookReaderPoems — xcomponents/BookReaderPoems.tsx

Index-driven reader used by Colaterales. Reads the trailing “Índice/Index/…” block to build a title→order map, identifies section titles and poem titles, reorders poems to match the index, merges trailing title-only blocks with the following content block, then paginates with the standard 4/5 scheme. Section names and index headers are per-locale tables. Uses useT("BookReader") for prev/next/pageOf/index/coverAlt. See book-readers.md.

XMinimalFooter — xcomponents/xminimalfooter/XMinimalFooter.tsx

Props copyright, links: { label, href }[]. Renders a centered, low-opacity footer line with · separators.

GrafoPoetico — xcomponents/grafo/GrafoPoetico.tsx (+ GrafoPage.module.css in the route)

Canvas force-directed graph of all poems. Nodes come from poemParser (getAllPoems), colored by bookIndex; edges connect each node to the next two in the same book. Features: custom physics (repulsion/centering/edge/alpha), pan (drag background), zoom (wheel/pinch, clamp 0.12–4), node drag, hover highlight + radial glow, connected-node highlight, live search with match highlighting and a result count, a legend, a tooltip, and a framer-motion modal showing the full poem (markdown bold/italic converted to HTML via formatLine). Per-locale book labels and UI strings are inline tables. Default 8-color palette, overridable. The page wraps it in a fixed full-screen container.

XBlogDecrypt — xcomponents/xblogdecrypt/XBlogDecrypt.tsx

Renders article HTML with a decrypt animation on every non-LaTeX text node. Props: contentHtml (required), title, image, author, date, isoDate, categories, keywords, readingTime, speed (default 50, article page passes 10), revealColor (default var(--accent, #e3342f)), scrambleColor (default #b8a8ff), startDelay (default 450 ms). Renders title → meta → pills → image → body. The body lives in a React.memo child (XBlogBody) so React never re-applies dangerouslySetInnerHTML and wipes the hand-injected span.xblog-text wrappers. A single shared IntersectionObserver reveals text on scroll after startDelay (so it plays after the route-transition overlay clears). Skips PRE/CODE/.katex. Idempotent and StrictMode-safe. The README in xblogdecrypt/ is the authoritative explanation.

XTextDecrypt — components/Xtexts/XTextDecrypt/XTextDecrypt.tsx

General-purpose decrypt text (HTMLMotionProps<'span'>). Props: text, speed, maxIterations, sequential, revealDirection (start|end|center), useOriginalCharsOnly, characters, className, encryptedClassName, parentClassName, animateOn (view|hover), delay. Exposes a .sr-only copy plus an aria-hidden animated copy. Used by PublicKeyCard for the PGP fingerprint.


Part B — App-level components (src/app/components/, not in the barrel)

ClientComponentHome — clientcomponenthome.tsx (+ ClientComponentHome.module.css)

The home page hero. Shows an AsciiLoadingAnimation overlay for at least 1.2 s, then two XZigZagLayoutVideo blocks. It reads 14 WordConfig[] arrays from useT("HomePhrases") and renders them as XHomeColors with mixed tags (h1, h2, p). Right block has startSide="right". Background images: /images/blog/conocerlamorfologiadeloindeterminado.webp (block 1) and /images/colecciones/arte/arte006.webp (block 2). Line uses var(--accent), thickness 0.3.

AsciiLoadingAnimation — AsciiLoadingAnimation.tsx

Full-screen <pre> ASCII intro. Loads precomputed frames from ascii-frames.json (top-level JSON import), advances one frame every 35 ms, fades out after the last frame or after a 4 s hard cap, then calls onDone. Font size is min(100vw / 150, 100vh / 84), monospace, user-select: none. aria-busy + aria-live="polite".

LenisProvider — LenisProvider.tsx

Mounts Lenis smooth scroll (duration 1.2, exponential easing, anchors.offset 80, autoRaf). Disabled under prefers-reduced-motion. Renders null.

transitionProvider — transitionProvider.tsx

The dominant client chrome component:

  1. Renders XGlassNavbar with locale-aware links:
    • left: Home /, Obras /obras, Info /info
    • right: Contacto /contacto, Blog /blog, external https://xscriptor.io (</> dev link).
  2. Renders a fixed bottom-center language dock (ES/DE/EN/IT/FR pills, aria-current, switchLocale() which rewrites the path preserving the rest of the route).
  3. Renders the route-transition overlay: two black rounded panels (border-radius: 100px) collapsing from 100vh to 0 (top immediately, bottom delayed 0.2 s) plus a fading path label, keyed by pathname inside AnimatePresence mode="wait".

Locale/label maps (LOCALE_KEYS, NAV_LABELS, langLabels) are inline, per-locale. Icons come from icons/navbar/navbarIcons.tsx.

ConditionalSeparator — layout/ConditionalSeparator.tsx

Renders an XSeparator (dashed, accent, hasX, faded, my-8) except on a hardcoded list of paths: home, obras, blog, grafo, contacto (all locales), plus any path containing asintota/asíntota/cielos-de-alquitran/ la-danza-de-las-amapolas. Rendered both above and below main.

XFooterComponent (default export SiteXFooter) — layout/footer/XFooterComponent.tsx

Client component: derives locale from usePathname() (note: only en/es/de), picks footer strings from the message JSONs via getMsg, and renders XMinimalFooter with the current year and links to terms + contact.

PublicKeyCard — publickey/PublicKeyCard.tsx (+ .module.css)

Neumorphic PGP card. Fetches /x-public.asc and renders it in a <pre>. Generates a QR of the hardcoded fingerprint 43086B71054295FF252949AD3F03BDE89BE5176F (qrcode, EC level M, 168px), shows .asc download, a per-char decrypt of the formatted fingerprint via XTextDecrypt, a copy-to-clipboard button with a copied confirmation state, and the SocialGrid. Labels come from useT("ContactPage"). Theme via :root.light/:root.dark neumorphic shadows.

SocialGrid — socialgrid/SocialGrid.tsx

Five hand-drawn SVG icon links (GitHub, Telegram, Instagram, WhatsApp, Email) in a circular “badge” style with an XCorner mark. Hover via inline styles using color-mix on var(--foreground).

ParticlesBackground — xbackgrounds/ParticlesBackground.tsx

Wraps XParticles from @xscriptor/xbackgrounds (particleCount 1200, trailOpacity 0.07, speed 0.8, light 255,255,255, dark 10,10,10). Mirrors data-theme to dark/light classes on <html>. Used on Contacto and Info.

BlogListClient — blog/BlogListClient.tsx

Blog index body. Fixed glass filter bar with a search input and category pills (derived from article categories, single-select toggle). Renders full-viewport stacked article cards; the card nearest viewport center is articleActive (others blurred/dimmed). Each card shows image (or accent gradient), title, excerpt, up to 2 category pills, localized date, and reading time. Uses useT('BlogPage') and useLocale(); date locale mapping de-DE/it-IT/fr-FR/en-US/es-ES. Links to /{locale}/blog/{slug}.

ObrasClientPage — [locale]/obras/ObrasClientPage.tsx

Book gallery. Hardcoded BOOKS array (slug/key/img) plus a final graph card. Same center-detection + blur-on-inactive behavior as the blog list. Uses useT() for Books.* titles/descriptions and ObrasPage.*, links to /{locale}/obras/{slug}.

Reader wrappers ([locale]/obras/*/*.tsx)

  • AsintotaBook, CielosDeAlquitranBook, AmapolasBook: thin client wrappers that own a page→background rotation (TOTAL_BG 72 / 30 / 31), preload the next background with new Image(), and pass labels (and, for Amapolas, sectionNames + renderPoem={<XBookFullDecrypt/>}) into the reader.
  • Boulevard page inlines the npm XBookReader directly.

Icons — icons/navbar/navbarIcons.tsx

SunIcon and MoonIcon (stroke, currentColor, size/strokeWidth/title props, role="img" + aria-hidden/aria-label).

not-found.tsx

Client 404: a dashed book SVG in accent color, big 404, localized message and “back home” link. Locale is derived from usePathname() with /^\/(en|es|de|it|fr)/ (unlike XFooterComponent, which only matches en|es|de).

UpdateHtmlLang

Tiny client effect that sets document.documentElement.lang = locale.


Part C — Component recipes

“Animated word / phrase”

Use XHomeColors (home) or XInteractivePhrase / XBookFullDecrypt (books). Words are WordConfig[] (see content-pipeline.md).

“Text that decrypts on scroll”

Wrap with XTextDecrypt (animateOn="view") or, for whole HTML articles, XBlogDecrypt.

“Full-bleed art section with text”

XZigZagLayoutVideo with imageSrc/videoSrc + overlayColor, children as XHomeColors/marker chips.

“Paginated book”

Pick the engine from book-readers.md; never hand-roll pagination.

“New reusable UI block”

  1. If it belongs to the shared library, add it to @xscriptor/xcomponents and bump the version — do not fork it locally.
  2. If it is site-specific, add it under src/app/components/... with a CSS module, and — if it must be importable from Server Components — export it through the "use client" barrel.
  3. Honor reduced motion and both themes.
content-pipeline

Xscriptor Content Pipeline

How content is authored, stored, parsed and localized:

  • Blog articles — Markdown → HTML at build time (articles.ts).
  • Books — MDX per locale → raw text → one of four reader engines.
  • Poem index (graph) — all books aggregated from disk (poemParser.ts).
  • UI strings / home phrases — messages/{locale}.json + I18nProvider.

1. Directory layout

src/app/content/
  articulos/
    es/**/*.md          # Spanish articles (source of truth)
    en/**/*.md          # English translations (same slugs)
    de/**/*.md  it/**/*.md  fr/**/*.md
  <book>/
    es.mdx  en.mdx  de.mdx  it.mdx  fr.mdx
  cielos-de-alquitran/
    cielos-de-alquitran.mdx   # ES file uses this special name
    en.mdx  de.mdx  it.mdx  fr.mdx
messages/
  es.json  en.json  de.json  it.json  fr.json

Books present: boulevard, asintota, primavera-en-el-desierto, la-danza-de-las-amapolas, colaterales, cielos-de-alquitran.

Blog articles are grouped in subfolders per topic (e.g. chat-control/, kyber/, obscura/) plus flat .md files. The research/ subfolder inside a topic is excluded from the blog listing but still reachable (see §2).

Public imagery (covers + reader backgrounds):

public/images/blog/*.webp|jpg
public/images/obras/<book>/<cover>
public/images/obras/<book>/<bookbook>/bgNN.webp   # reader backgrounds
public/images/obras/grafo/background.webp
public/images/colecciones/{arte,libros}/*.webp

Background counts: asintota/asintotabook = 72, amapolasbook = 31, cielosdealquitranbook = 30.


2. Blog articles — src/app/lib/articles.ts

Build-time, fs-based, locale-aware. These functions run only in Server Components / build (params/metadata). JSON/disk paths are relative to process.cwd().

Constants

const articlesBaseDir = path.join(process.cwd(), "src/app/content/articulos");
const FALLBACK_LOCALE = "es";
const SUPPORTED_LOCALES = ["es", "en", "de", "it", "fr"];

Frontmatter contract

---
title: "Article Title"
date: "2024-01-01"          # ISO; sort key (desc)
description: "Short summary"
categories: ["chat-control"] # primary taxonomy; drives listing + filters
tags: ["..."]                # fallback for keywords
keywords: ["..."]            # preferred pills on the article page
image: "/images/blog/x.webp"
author: "Xscriptor"
excerpt: "Optional custom excerpt"
---

Functions

| Function | Behavior | |----------|----------| | getSortedArticles(locale = es) | Lists top-level .md files and one level of non-research subdirectories; parses each; sorts by date desc. Falls back to es if the locale dir is missing. | | getAllArticleSlugs(locale = es) | Union of locale slugs + es fallback slugs (set), returned as { params: { slug } }. Nested slugs keep folder/file. | | getArticleData(slug, locale = es) | Reads {locale}/slug.md, falling back to es. Runs the full markdown→HTML pipeline. Returns null if absent. | | getArticlesByCategory(category, locale = es) | Case-insensitive category filter over getSortedArticles. |

ArticleMetadata

{ slug, title, description?, date, categories: string[],
  tags?, keywords?, excerpt?, readingTime: number, image?,
  author?, contentHtml? }
  • readingTime = ceil(words / 200).
  • excerpt falls back to description, then to the first 150 chars of body with headings stripped, suffixed "...".
  • toStringArray normalizes scalar/array frontmatter.
  • date defaults to today if missing.

Markdown → HTML pipeline (in getArticleData)

matter(content)                              // gray-matter frontmatter
→ stripDuplicateLeadMeta(body, title)        // remove duplicate H1 + "Publicado por" block
→ unified()
    .use(remarkParse)
    .use(remarkGfm)
    .use(remarkMath)
    .use(remarkRehype)
    .use(rehypeKatex)
    .use(rehypeHighlight)
    .use(rehypeStringify)

Post-processing on the resulting HTML:

  1. Soft-break → <br>. Converts single \n inside p, h1-6, blockquote, li, div (skips anything containing katex or pre/code), because remark-rehype does not emit <br> for soft breaks.
  2. Table wrapping. Wraps every <table…</table> in <div class="table-wrapper"> for horizontal scroll.

stripDuplicateLeadMeta removes a leading # H1 (duplicate of the page title) and an immediately following **Publicado por …** paragraph. This is why article bodies never show the title/author twice.

Consumer

  • Listing: [locale]/blog/page.tsx → getSortedArticles(locale) → BlogListClient.
  • Article: [locale]/blog/[...slug]/page.tsx:
    • generateStaticParams builds { locale, slug: slug.split('/') } for all five locales × getAllArticleSlugs() (ES slugs).
    • generateMetadata calls getArticleData.
    • Page renders XBlogDecrypt with de-duplicated categories/keywords, localized date (de-DE/it-IT/fr-FR/en-US/es-ES), reading-time label from BlogPage.readingTime, and speed={10}.
    • [...slug] is a catch-all, so nested/folder slugs work unchanged.

next-sitemap.config.js excludes /blog/post (a legacy route that no longer exists). Removing the exclude is safe.


3. Book content (MDX as plain text)

Book pages read the MDX as a raw string (fs.readFileSync) and hand it to a client reader. No MDX runtime, no frontmatter parsing for books. The readers parse the string themselves with blank-line-separated blocks.

File resolution pattern (pages)

const filePath = path.join(process.cwd(), `src/app/content/${book}/${locale}.mdx`);
const fallbackPath = path.join(process.cwd(), "src/app/content/${book}/es.mdx");
const rawText = fs.readFileSync(fs.existsSync(filePath) ? filePath : fallbackPath, "utf-8");

Exception: cielos-de-alquitran ES file is cielos-de-alquitran.mdx, handled explicitly by both poemParser.ts (LOCALE_FILES) and the page.

Structural syntax understood by the readers

  • Blank line(s) separate blocks. Readers split on \n{2,} (XBookColors / XCompleteBook) or \n{3,}/\n{2,} (poemParser / BookReaderPoems).
  • Section title: a block whose first line matches a known section name (all-caps tables per book/locale). Becomes a dedicated page.
  • Image: a whole block of the form ![alt](src).
  • Poem / content block: anything else.
  • Index: a trailing block starting with the locale’s index header (Índice/Index/Inhalt/Indice/Index) followed by entries. Entries with ....123 or - 123 are poem entries; other lines are section names. The index is stripped from the body and rendered as its own page; BookReaderPoems also uses it to reorder poems.

Section-name tables (per book/locale)

| Engine | Sections (ES) | |--------|---------------| | XBookColors default | LUCIÉRNAGAS, UNIVERSO, CONSTELACIONES, INSTANTES, AUTOPSIA, I, II, III, IV, V | | XCompleteBook default | CURVA, PLANO CARTESIANO, RECTA, I, II, III, IV, V | | BookReaderPoems (Colaterales) | TACTO, TRANSFUSIONES, PERSPICACIA, INTERSECCIÓN, INCERTIDUMBRE | | AmapolasBook override | LUCIÉRNAGAS, UNIVERSO, CONSTELACIONES, INSTANTES, AUTOPSIA |

All books also carry translated section names in de/it/fr/en.

These tables live inside the components, not in messages/. When a new book is added with custom sections, pass sectionNames (XCompleteBook / XBookColors) or extend SECTION_NAMES (BookReaderPoems).


4. Poem index for the graph — src/lib/poemParser.ts

getAllPoems(locale) builds the node set for GrafoPoetico.

  • Iterates BOOKS = [boulevard, asintota, primavera-en-el-desierto, la-danza-de-las-amapolas, colaterales, cielos-de-alquitran]; bookIndex (0–5) drives graph node color. Blog blocks get book: "blog", bookIndex: 6.
  • For each book, resolves the locale file (special-casing cielos), falling back to es.mdx.
  • isVerseBlock: a block is verse if it has ≥2 lines and average line length < 60 chars.
  • PROSE_BOOKS = new Set(["colaterales"]) allows prose blocks (capped at 600 chars via PROSE_CONTENT_MAX); other books skip non-verse blocks.
  • Titles: first short line if plausible, else first ~5 words + ….
  • Blog contributions: reads the locale’s flat .md files (not nested), strips images/headings, splits on \n{2,} blocks >30 chars, and titles them "{articleTitle} — {firstLine}" or "{articleTitle} ({n})".

PoemData = { id, title, content, book, bookIndex }.


5. UI strings and HomePhrases

messages/{locale}.json is the interface string store (see architecture.md §5 for namespaces). Two consumption patterns:

  • Client components: const t = useT("BlogPage"); t("readingTime", { minutes }).
  • Server components: getMsg(msgs, "BlogPage.metadataTitle") (no params).

WordConfig (used by HomePhrases and poem rendering)

type WordConfig = {
  text: string;                      // empty string = hard line break
  type?: "underline" | "button" | "blur1" | "blur2" | "normal";
  bold?: boolean;
  italic?: boolean;
  breakAfter?: boolean;
};

Consumed as t.raw<WordConfig[]>("phraseN"). HomePhrases holds phrase1 … phrase14; ClientComponentHome distributes them across two zigzag blocks.

Semantics of type:

  • underline — clickable; toggles the blur1 group.
  • button — clickable; toggles the blur2 group.
  • blur1 / blur2 — start blurred, unblur when their group is toggled.
  • normal — plain text.
  • breakAfter / empty text — force a line break.

When authoring HomePhrases, always include at least one underline and one button in a phrase that uses blur1/blur2, otherwise blurred words can never be revealed. The generated poem pipelines apply a similar safety fallback (see xbookcolors/README.md).


6. Adding content — checklists

Add a blog article

  1. Create src/app/content/articulos/es/<slug>.md with the frontmatter contract. Nested slugs → put it in a topic folder.
  2. Add the translation(s) with the same slug path under en/de/it/fr/.
  3. Ensure a cover image exists at the referenced image path under public/.
  4. Optionally add categories/keywords; the listing derives its filters from categories.
  5. npm run build (validates generateStaticParams and the pipeline).

Add or update a book

  1. Drop src/app/content/<book>/<locale>.mdx (all five locales; es required).
  2. Add cover + background frames under public/images/obras/<book>/.
  3. Pick an engine (see book-readers.md), add the route page under [locale]/obras/<book>/, and — for rotating backgrounds — a thin wrapper.
  4. Register the book in ObrasClientPage's BOOKS array (title/description keys Books.<key> / Books.<key>Desc in all message files).
  5. If it has custom sections, add the per-locale section-name tables.
  6. If the graph should include it, append to BOOKS in poemParser.ts and add its BOOK_LABELS translations in GrafoPoetico.

Add a UI string

Add the key to all five messages/*.json files under the right namespace, then consume via useT(namespace) (client) or getMsg (server).

design-system

Xscriptor Design System

Canonical reference for the visual language of xscriptor.com. Everything here is extracted from the live source: src/app/globals.css (tokens + global rules), the CSS-module files, and the inline styles inside components.


1. Design intent

Xscriptor is a literary portfolio: an author/poet/artist presenting books, articles and a contact surface. The visual language is deliberately:

  • Typography-first. EB Garamond carries the entire experience. There is no display font, no icon font, no decorative typeface. Words are the interface.
  • Marker / highlighter aesthetic. Runs of text are placed on soft background "chips" (color-mix(in srgb, var(--bg) 88%, transparent)) so that text stays legible over photographs and full-bleed art.
  • Restrained dual palette. One accent per theme: deep violet in light mode, warm gold in dark mode. Everything else is background, text and border.
  • Atmospheric imagery. Full-viewport photographs and pre-rendered background frames sit behind content with a dark overlay.
  • Literary / cryptographic tension. The site mixes emotional, poetic copy with PGP keys, fingerprints and decryption-style animations (text descrambles character by character). This is a signature motif, not a gimmick.
  • Motion as reveal. Decryption, staggered fades and blur-to-focus are used to reveal content. Nothing bounces or plays for its own sake.
  • Both themes are first-class. Light mode is the marketing default; dark mode is the reading default for book pages and is fully supported everywhere.

When adding UI, ask: does this read like a printed literary edition that has learned to decrypt itself? If not, it is off-brand.


2. Color tokens

All colors are CSS custom properties declared in src/app/globals.css. Use the variables, never raw hex values (the only sanctioned raw hexes are the book word-color palette in §5).

Light theme (default, :root)

| Token | Value | Use | |-------|-------|-----| | --bg | #ffffff | Page background | | --text | #000000 | Primary text | | --accent | #5b2e8d | Links, active nav, borders, pills, decrypt reveal | | --accent-text | #ffffff | Text on accent-filled surfaces | | --border | rgba(0, 0, 0, 0.1) | Hairlines, card borders, dividers | | --foreground | #171717 | Near-black used by neumorphic card + social icons | | --text-muted | #6b7280 | Secondary/meta text | | --primary | #4328a8 | Deep violet (select controls, PGP card) | | --primary-hover | #7c3aed | Hover state for --primary | | --success | #10b981 | Status dot (PGP key loaded) |

Dark theme (:root[data-theme="dark"], :root.dark)

| Token | Value | Use | |-------|-------|-----| | --bg | #0a0a0a | Page background | | --text | #ffffff | Primary text | | --accent | #ffe884 | Warm gold: links, active nav, borders | | --accent-text | #000000 | Text on gold surfaces | | --border | rgba(255, 255, 255, 0.1) | Hairlines | | --foreground | #ededed | Off-white | | --text-muted | #9ca3af | Secondary text | | --primary | #fbbf24 | Amber | | --primary-hover | #f59e0b | Amber hover | | --success | #34d399 | Status dot |

Both selectors are required: [data-theme="dark"] (set by the navbar toggle) and .dark (set on <html> by ParticlesBackground for components that key off a class). PublicKeyCard CSS relies on :root.light / :root.dark.

Semantic color rules

  • Links default to var(--accent). Global a:hover forces #ff4141 (a hard red) — this is a legacy rule; prefer component-scoped hover (opacity or accent) for new work.
  • Syntax highlighting inside .article-content has two full palettes: a light “One Light”-style set and a dark set scoped under [data-theme="dark"].
  • KaTeX gets extra left/right breathing room via .article-content .katex.

3. Typography

Font family

@font-face { font-family: "EB Garamond"; src: url("/fonts/EBGaramond-Regular.woff2") ...; font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: "EB Garamond"; src: url("/fonts/EBGaramond-Italic.woff2") ...; font-weight: 400; font-style: italic; font-display: swap; }
  • html, body { font-family: "EB Garamond", serif; }
  • Only woff2 files exist in public/fonts/ (Regular + Italic). The @font-face also lists .ttf sources that are not present — harmless (the woff2 loads first), but do not rely on the ttf.
  • Monospace appears only for technical metadata: PGP fingerprint / key, .asc download link, code blocks ('Fira Code', 'Monaco', monospace), and the ui-monospace used by the ASCII intro. It is never used for body copy.

Global element scale (in globals.css)

These apply site-wide and are centered by default:

| Element | Weight | Size | Margins | |---------|--------|------|---------| | h1 | 400 | clamp(16px, 12vw, 32px) | 1vw top/bottom | | h2 | 300 | clamp(24px, 8vw, 26px) | 1vw | | h3 | 600 | clamp(20px, 9vw, 26px) | 1vw | | p | 400 | clamp(18px, 2.5vw, 20px) | 2vw + side padding clamp(1rem, 8vw, 4rem) |

Notes:

  • The h1 clamp is unusual: 12vw means it is small on phones and caps at 32px. Do not “fix” it globally — page/components override it where a hero is needed (ObrasPage title, section titles use their own clamps).
  • Paragraphs carry large horizontal padding so prose never spans full-bleed.

Article typography override

.article-content (used by XBlogDecrypt) cancels the centered prose:

  • p → left aligned, no side padding, clamp(16px, 2vw, 18px), line-height 1.8.
  • h1–h4 → left aligned; h2 gets a bottom border, h3 smaller.
  • Lists, blockquotes (accent left border), inline/fenced code, images, tables, hr, links (underlined) all get dedicated rules.
  • Tables are wrapped in .table-wrapper (added by articles.ts) for horizontal scroll on mobile; the table gets min-width: 600px.

Body-text line-height conventions

  • Global paragraphs inherit the browser default; literary body copy in readers uses explicit line-height between 1.7 and 2 (e.g. BookReaderPoems sets line-height: 2 !important on poem paragraphs).
  • Display/marker chips use line-height: 1.8 so multi-line highlighted runs do not collide.

4. Surfaces, borders and effects

Glassmorphism (.glass, navbar, filter bars, modals)

background: color-mix(in srgb, var(--bg) 70%, transparent); /* light */
backdrop-filter: blur(18px);
border: 1px solid var(--border);
/* dark variant */
background: color-mix(in srgb, var(--bg) 15%, transparent);

Used by: XGlassNavbar (blur 20px, radius 9999px, layered shadows), blog filter bar (blur 18px, radius pill), graph tooltip/legend/modal (blur 12–30px), GrafoPoetico.

Marker / highlighter chip

The single most repeated visual atom:

background: color-mix(in srgb, var(--bg) 88%, transparent);
padding: 0.1em 0.35em;
border-radius: 0.2em;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;

Used for: home/book words (XHomeColors, XBookColors), section titles, index entries, poem titles, book titles/descriptions, page indicators. It keeps text readable over any background. Blog-list cards use a darker variant (color-mix(in srgb, #000 55%, transparent) with white text) because they sit over imagery.

Neumorphism (PublicKeyCard only)

The PGP card is the one place with a soft-UI treatment. It uses a local --neumorph-bg token (#e9e9e9 light / #16181d dark) plus hand-written dual-shadow recipes for raised and inset surfaces (card, QR frame, fingerprint chip, buttons, key block). Theme is selected by :root.light / :root.dark.

Shadows

  • Cover images / inline images: 0 10px 25px rgba(0,0,0,0.2), radius 12px.
  • Navbar: two stacked shadows (0 4px 6px -1px + 0 8px 32px), stronger in dark.
  • Modals: 0 8px 48px rgba(0,0,0,0.4).
  • Book cards (Obras/Blog): overlay rgba(0,0,0,0.35)–0.4 over the photo.

Radii

  • Pills / nav / language dock: 9999px.
  • Cards / cover / modal: 12px–16px (PublicKeyCard up to 2rem).
  • Marker chips: 0.2em; small controls 4px–8px.

5. Book word-color palette

The interactive poem readers cycle a fixed palette across word chunks. This is the only place raw hex values are allowed (they are applied inline as color and --c-word).

| Key | Dark theme | Light theme | |-----|-----------|-------------| | c1 | #fc618d (pink) | #8f002a (wine) | | c2 | #7eddc9 (teal) | #007052 (green) | | c3 | #8eeca3 (green) | #265731 (deep green) | | c4 | #fce566 (yellow) | #7a6e14 (olive) | | c5 | #b8a8ff (lavender) | #5a4e9e (violet) |

c0 is the surface color (#0a0a0a dark / #ffffff light) and is used as the foreground when a button word is hovered (accent fill).

Assignment rule (WORD_COLORS, COLOR_INTERVAL = 2): every third chunk gets a color key, the two in between are null and inherit var(--text). Dark/light maps are selected by a MutationObserver on document.documentElement's data-theme attribute (useDetectTheme). There is no prefers-color-scheme fallback: data-theme !== "dark" means light.

GrafoPoetico uses its own 8-color graph palette: #ff6b6b, #ffd93d, #6bcb77, #4d96ff, #ff6bff, #ff9f43, #22d3ee, #54a0ff, overridable via color1…color8 props.


6. Motion & animation language

| Effect | Where | Spec | |--------|-------|------| | Route transition | transitionProvider.tsx | Two black border-radius:100px panels collapse 100vh → 0 (top delay 0, bottom delay 0.2s, total ~0.4s) + a fading path label | | Page/reader fade | XCompleteBook, XBookColors, BookReaderPoems | fadeSlideIn: translateY(20px)→0, opacity 0→1, 0.5s–0.8s ease-out | | Zigzag stagger | XZigZagLayoutVideo | fadeInUp 0.5s per item, animationDelay = index * 0.12s, triggered by IntersectionObserver | | Scroll line draw | XZigZagLayout / XZigZagLayoutVideo | SVG path strokeDashoffset tied to scroll progress | | Text decrypt | XTextDecrypt, XBlogDecrypt, X*Colors | Character-by-character reveal, scrambled tail in scrambleColor; default speed 50 ms (blog body uses 10 ms) | | Blur reveal | XBookColors/XHomeColors blur words | filter: blur(10px) opacity .3 → blur(0) over 0.7s cubic-bezier(0.4,0,0.2,1) | | Smooth scroll | LenisProvider | duration: 1.2, exponential ease, anchor offset 80, autoRaf | | Graph physics | GrafoPoetico | Custom canvas spring: repulsion 1200α, centering .002α, edge .003α, alpha decay ×0.992, live requestAnimationFrame | | Card hover (info) | InfoPage.module.css | translateY(-5px) + stronger shadow | | Button/pill hover | everywhere | color swap, background fill, opacity |

Reduced motion

globals.css globally neutralizes animation/transition durations under @media (prefers-reduced-motion: reduce). Components also guard individually:

  • LenisProvider disables smooth scroll entirely.
  • XZigZagLayoutVideo hides the background video and disables itemReveal.
  • XCompleteBook/XBookColors disable bookContent animation.
  • XGlassNavbar disables nav/hamburger/overlay transitions.
  • Blog/Obras blur-on-inactive is disabled and content shown at full opacity.

New animated components must honor prefers-reduced-motion, either via the global rule or a component-level guard.


7. Responsive breakpoints

The site uses Tailwind's default breakpoints where utilities are used, and hand-written media queries elsewhere. Observed working set: 640px, 768px, 1024px, 1200px (max content width).

Key behavior:

  • XGlassNavbar: desktop link sections (12rem → 14rem at lg); below 768px, link sections are hidden and a fixed glass hamburger + full-screen overlay menu appear.
  • Full-bleed sections use the “breakout” trick: width: 100vw; margin-left: calc(-50vw + 50%); (Obras/blog cards, zigzag/video wrappers). Never use raw vw padding without clamp().
  • Book readers stack a readerContainer (min-height: 100vh, justify-content: space-between) so pagination pins to the bottom.
  • InfoPage bio/timeline use padding: 0 15vw on desktop (a candidate for a clamp() cleanup — see anti-patterns).

8. Accessibility rules already in the codebase

Preserve these patterns in new work:

  • Skip link: .skip-to-content in globals.css + <a href="#main-content"> in the root layout; it slides in on :focus.
  • .sr-only utility (defined globally, also expected by npm components) backs every decryption animation: visible text is aria-hidden="true", and a hidden .sr-only span carries the real text for screen readers.
  • Focus-visible outlines (2px solid var(--accent)) on navbar logo, hamburger and mobile theme button.
  • Navbar exposes aria-label, aria-expanded, aria-controls; the language dock uses aria-current="true" for the active locale.
  • <html lang> is corrected per locale by UpdateHtmlLang.
  • html { scroll-padding-top: 5rem } keeps anchored content clear of the navbar.
  • suppressHydrationWarning on <html> is intentional (theme attribute set before hydration).

9. Design-system checklist for new UI

  1. Use var(--bg), var(--text), var(--accent), var(--border) — never raw hex except the documented book/graph palettes.
  2. Set text on a marker chip when it floats over imagery.
  3. Prefer clamp() for fluid type and spacing; cap wide-screen padding.
  4. Verify both themes (toggle is the navbar logo).
  5. Verify <768px (navbar collapses) and reduced-motion.
  6. Keep animation subtle and purposeful; reveal, don't decorate.
  7. Reuse the existing components (see component-catalog.md) before writing new ones.