Files

13 KiB
Raw Permalink Blame History

Architecture Patterns

Project: Portfolio Killian' Dalcin — Nuxt 4 SSR Researched: 2026-04-10 Confidence: HIGH (verified against actual codebase + Nuxt 4 docs patterns)


Is the Current Architecture Sound?

Yes — the foundation is solid. The core SSR pipeline is correctly implemented:

  • ssr: true + compatibilityVersion: 4 — full SSR, not hybrid
  • @nuxtjs/i18n with prefix_except_default — SEO-correct URL scheme (FR at /, EN at /en/)
  • @nuxtjs/color-mode with storage: 'cookie' — theme class applied server-side, no flash
  • useSeoMeta() with reactive () => t('...') callbacks — meta resolves server-side per locale
  • useLocaleHead() in app.vue — injects hreflang alternates on every page automatically
  • Data layer (app/data/*.ts + useProjects()) is clean: static IDs, translated fields via i18n keys, reactive recomputation on locale change

Three real problems exist in the current implementation (not architecture flaws — just execution gaps):

  1. og:image hardcoded to https://killiandalcin.fr/og-image.png on all 6 pages, including project detail where project.image is already available
  2. JSON-LD only on homepage — other pages have no structured data; jobTitle still says "Developpeur Full Stack Freelance" instead of positioning Hytale
  3. ogUrl missinguseSeoMeta() calls don't include ogUrl, so the canonical URL is absent from Open Graph, though <link rel="canonical"> is provided by @nuxtjs/i18n

The existing layer structure is correct. No refactoring needed. Extensions follow the same pattern:

Pages (app/pages/)
  hytale.vue                          ← new page, same pattern as fiverr.vue
  project/[id].vue                    ← add dynamic og:image (project.image already there)

Composables (app/composables/)
  useSeoMeta → per-page calls         ← add ogUrl to every page
  useJsonLd.ts                        ← new: centralize JSON-LD generation

Data (app/data/)
  hytale.ts                           ← new: pricing tiers, service cards, tech highlights
  site.ts                             ← update jobTitle to Hytale positioning

Locales (app/locales/fr.json, en.json)
  seo.hytale.*                        ← new SEO keys
  hytale.*                            ← new page content keys

Component Boundaries for the Hytale Page

Component Responsibility Communicates With
pages/hytale.vue Page assembly, SEO, JSON-LD HytaleHeroSection, HytalePricingGrid, HytaleServiceCards, FAQSection, CTASection
sections/HytaleHeroSection.vue Hero — "Hytale Plugin Developer" headline, early access badge useI18n()
sections/HytalePricingGrid.vue 3-column pricing table (Simple / Complex / Sur-mesure + Maintenance) app/data/hytale.ts via props
sections/HytaleServiceCards.vue What's included per service, tech stack used app/data/hytale.ts via props
Reuse FAQSection.vue Hytale-specific FAQs data/faq.ts (add hytaleFAQs export)
Reuse CTASection.vue Call to action to contact / Fiverr props

Follow the fiverr.vue structural pattern exactly — it already does service cards correctly. The Hytale page is a thematic variant, not a new pattern.


og:image Hardcoding Fix

Problem: All pages including project/[id].vue use the same static og-image.png. The project detail page already has project.value?.image available but ignores it.

Fix: per-page og:image strategy

// pages/project/[id].vue — already has project data, just use it
useSeoMeta({
  // ...
  ogImage: () => project.value?.image
    ? `https://killiandalcin.fr${project.value.image}`
    : 'https://killiandalcin.fr/og-image.png',
})

For all other pages, create dedicated OG images rather than sharing one. The naming convention:

Page File Dimensions
Default / fallback /public/og/og-default.png 1200×630
Hytale /public/og/og-hytale.png 1200×630
Fiverr /public/og/og-fiverr.png 1200×630
Projects /public/og/og-projects.png 1200×630

Each page's useSeoMeta() references its own file. This is the simplest, most reliable approach — no server-side image generation required, works perfectly with SSR, zero dependencies.

Do not use @vercel/og or nuxt-og-image. The portfolio is Docker-deployed, not Vercel. nuxt-og-image adds a Satori/Chromium dependency and requires additional config for non-Vercel deployments. Static pre-made images are sufficient for a portfolio and have zero runtime cost.


Canonical URL Strategy with prefix_except_default

Current situation: @nuxtjs/i18n with prefix_except_default + baseUrl: 'https://killiandalcin.fr' automatically generates:

<!-- On / (FR default, no prefix) -->
<link rel="canonical" href="https://killiandalcin.fr/" />
<link rel="alternate" hreflang="fr" href="https://killiandalcin.fr/" />
<link rel="alternate" hreflang="en" href="https://killiandalcin.fr/en/" />
<link rel="alternate" hreflang="x-default" href="https://killiandalcin.fr/" />

<!-- On /en/ -->
<link rel="canonical" href="https://killiandalcin.fr/en/" />
<link rel="alternate" hreflang="fr" href="https://killiandalcin.fr/" />
<link rel="alternate" hreflang="en" href="https://killiandalcin.fr/en/" />

This is correct — useLocaleHead() in app.vue handles all of this automatically. No manual canonical management needed.

The one gap is ogUrl in Open Graph. Add it to every page's useSeoMeta():

// Pattern for every page
const { locale } = useI18n()
const localePath = useLocalePath()

useSeoMeta({
  // ... existing fields ...
  ogUrl: () => `https://killiandalcin.fr${localePath('/hytale')}`,
})

useLocalePath() resolves the correct prefixed path for the current locale (/hytale for FR, /en/hytale for EN), making ogUrl SSR-safe and locale-correct.

For the sitemap: @nuxtjs/sitemap already reads from @nuxtjs/i18n configuration and generates hreflang entries automatically. No manual sitemap management needed for the Hytale page — it appears automatically when pages/hytale.vue is created.


JSON-LD Structured Data Patterns

What to Use Per Page

Page Schema Types Priority
/ (homepage) Person + WebSite + ProfessionalService Exists, needs update
/hytale Service (×3 tiers) + SoftwareApplication New
/projects ItemList of SoftwareSourceCode Nice to have
/project/[id] SoftwareSourceCode or CreativeWork Nice to have
/fiverr Offer per service Nice to have
/contact ContactPage Low value

Centralize with a Composable

The current pattern inlines JSON-LD in each page's useHead(). This works but leads to duplication of Person data across pages. Centralize reusable schemas:

// app/composables/useJsonLd.ts
export function usePersonSchema() {
  return {
    '@type': 'Person',
    name: "Killian' DAL-CIN",
    url: 'https://killiandalcin.fr',
    jobTitle: 'Hytale Plugin Developer & Full Stack Developer',
    email: 'contact@killiandalcin.fr',
    sameAs: [
      'https://linkedin.com/in/killian-dal-cin',
      'https://www.fiverr.com/users/mr_kayjaydee',
      'https://gitea.kamisama.ovh/kayjaydee',
    ],
  }
}

export function useWebSiteSchema() {
  return {
    '@type': 'WebSite',
    name: "Killian' DAL-CIN",
    url: 'https://killiandalcin.fr',
    potentialAction: {
      '@type': 'SearchAction',
      target: 'https://killiandalcin.fr/projects?q={search_term_string}',
      'query-input': 'required name=search_term_string',
    },
  }
}

export function useHytaleServiceSchemas() {
  return [
    {
      '@type': 'Service',
      name: 'Hytale Plugin Development — Simple',
      provider: { '@type': 'Person', name: "Killian' DAL-CIN" },
      serviceType: 'Software Development',
      description: 'Basic Hytale plugin: single mechanic, standard features',
      offers: { '@type': 'Offer', priceCurrency: 'USD', price: '150' },
    },
    {
      '@type': 'Service',
      name: 'Hytale Plugin Development — Complex',
      provider: { '@type': 'Person', name: "Killian' DAL-CIN" },
      serviceType: 'Software Development',
      description: 'Advanced Hytale plugin with custom systems, multiplayer, persistence',
      offers: { '@type': 'Offer', priceCurrency: 'USD', price: '400' },
    },
    {
      '@type': 'Service',
      name: 'Hytale Plugin Maintenance',
      provider: { '@type': 'Person', name: "Killian' DAL-CIN" },
      serviceType: 'Software Maintenance',
      description: 'Monthly plugin maintenance: update compatibility after Hytale patches',
      offers: { '@type': 'Offer', priceCurrency: 'USD', priceSpecification: { '@type': 'UnitPriceSpecification', price: '50', unitCode: 'MON' } },
    },
  ]
}

Each page then composes what it needs:

// pages/index.vue
const { usePersonSchema, useWebSiteSchema } = useJsonLd()
useHead({
  script: [{
    type: 'application/ld+json',
    innerHTML: JSON.stringify({
      '@context': 'https://schema.org',
      '@graph': [usePersonSchema(), useWebSiteSchema()],
    }),
  }],
})

// pages/hytale.vue
const { usePersonSchema, useHytaleServiceSchemas } = useJsonLd()
useHead({
  script: [{
    type: 'application/ld+json',
    innerHTML: JSON.stringify({
      '@context': 'https://schema.org',
      '@graph': [usePersonSchema(), ...useHytaleServiceSchemas()],
    }),
  }],
})

SoftwareApplication for Hytale Plugins

For the Hytale page, SoftwareApplication is the most SEO-relevant schema for plugin demos or featured work:

{
  "@type": "SoftwareApplication",
  "name": "Hytale Plugin — [Plugin Name]",
  "applicationCategory": "GameApplication",
  "operatingSystem": "Hytale",
  "author": { "@type": "Person", "name": "Killian' DAL-CIN" },
  "offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" }
}

Use SoftwareApplication only when there are real plugin demos or releasable plugins. Use placeholder data with clearly marked demo content for now.


Hytale Page: Pricing Grid Pattern

The most effective pricing grid for this use case is a 3-tier table with a highlighted middle tier. Nuxt UI v3 provides everything needed without custom components:

<!-- Structure recommendation  implement with UCard inside a CSS grid -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
  <!-- Tier: Simple Plugin -->
  <UCard>...</UCard>

  <!-- Tier: Complex Plugin  highlighted -->
  <UCard class="ring-2 ring-brand-500 scale-105">
    <template #header>
      <UBadge>Most Popular</UBadge>
    </template>
    ...
  </UCard>

  <!-- Tier: Sur-mesure -->
  <UCard>...</UCard>
</div>

Pricing data belongs in app/data/hytale.ts (not siteConfig) because it is Hytale-specific content, not site-wide configuration. Translatable labels live in locale files; prices stay in the data file (they are locale-independent).


Anti-Patterns to Avoid

Anti-Pattern 1: Duplicating Person schema across pages as raw objects

What: Copy-pasting the full Person object in every page file Why bad: When Killian's jobTitle changes to "Hytale Plugin Developer", every page needs updating manually Instead: useJsonLd.ts composable as shown above — single source of truth

Anti-Pattern 2: Using localStorage for any SSR state

What: Storing locale or theme in localStorage Why bad: Causes hydration mismatch — server renders with default, client re-renders after mount Instead: Cookie-only (already correctly implemented)

Anti-Pattern 3: Static ogUrl strings

What: ogUrl: 'https://killiandalcin.fr/hytale' hardcoded Why bad: EN version at /en/hytale gets wrong ogUrl, confusing social crawlers Instead: ogUrl: () => \https://killiandalcin.fr${localePath('/hytale')}``

Anti-Pattern 4: Translating prices

What: Putting price strings like "$150" or "150€" in locale files Why bad: Prices change independently of language; mixes content types Instead: Prices in data file, currency/format computed if needed

Anti-Pattern 5: nuxt-og-image for a Docker-SSR deployment

What: Using Satori-based dynamic OG image generation Why bad: Adds Chromium/Satori dependency, complex config for non-Vercel targets, overhead per request Instead: Static pre-made OG images per page in /public/og/


Scalability Considerations

This is a static-content portfolio. Scalability is not a concern. The architecture is appropriate for:

  • ~10 pages
  • ~20 projects
  • 2 locales
  • No user accounts, no dynamic data beyond the contact form

The only scaling vector is content volume (more projects, more services). The current data layer (app/data/) handles this cleanly — add entries to arrays, add i18n keys, done.


Sources

  • Nuxt 4 docs: ssr: true, compatibilityVersion: 4 — verified against current nuxt.config.ts
  • @nuxtjs/i18n v9 docs: prefix_except_default, useLocaleHead(), useLocalePath() — HIGH confidence
  • Schema.org: Service, SoftwareApplication, Person, WebSite — HIGH confidence
  • @nuxtjs/sitemap v6: auto i18n integration — HIGH confidence (verified module is installed)
  • Pattern for useJsonLd.ts composable: derived from existing codebase conventions (composable-per-concern)
  • og:image static file strategy: MEDIUM confidence (sufficient for use case, no dynamic content needed)