The Meritage Collection
Multi-site system with Nuxt Layers, built with FINE
Pacific Hospitality Group's Meritage Collection spans six distinct sites, each with its own branding and content. The multi-site system is built using FINE's open-source CMS FAE and Nuxt. The original plan was to build one resort site and duplicate it for each property as they were ready to be launched. Since these sites would continue to evolve after launch, duplicating them would risk deviation in the codebases and additional effort to maintain consistency between the sites. Instead, I recommended a shared architecture using Nuxt Layers.
The core layer provides each site with components, pages, and GraphQL managed in a single repository. Each site extends this foundation while still allowing for per-property overrides and feature additions. GitHub Actions are used to coordinate deploys from the core layer and its environments to the individual property sites on Netlify. The system has continued to evolve with new integrations, pages, and components added to the core layer, which become automatically available to all properties without any additional work within the individual property repositories.
┌─────────────┐
│ CDN ├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
└──────▲──────┘
│ ┌─────────────────┐ ┌────────▼────────┐
│ │ Property Repos │ │ Property Sites │
│ │ │ │ │
│ │ ┌─────────────┐ │ │ ┌─────────────┐ │
┌──────┴──────┐ │ │ Property 1 │ │ │ │ Property 1 │ │
│ CMS ├ ─ ─ ┐ │ └──────┬──────┘ │ │ └──────┬──────┘ │
└──────┬──────┘ │ │ │ │
│ │ │ │ │ ┌─────────────┐ │ │ │
│ ┌─────▶ ├────▶ CI/CD ├───▶ │
│ │ │ │ │ └──────▲──────┘ │ │ │
┌──────▼──────┐ │ │ │ │ │
│ Core Layer ├─────┘ │ │ │ │ │ │ │
└──────┬──────┘ │ │ │ │
│ ┌──────┴──────┐ │ │ │ ┌──────┴──────┐ │
│ │ │ Property n │ │ │ │ Property n │ │
│ └─────────────┘ │ │ │ └─────────────┘ │
│ └─────────────────┘ └─────────────────┘
│
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
Each property site is built with a minimal Nuxt boilerplate and extends the core layer. The core layer can be referenced from a local directory for development, or from a specific branch of the GitHub repository for different environments.
// Local development
CORE_DIR=../core-layer/layers/core
// Production
CORE_DIR=github:org/core-layer#main
// Staging
CORE_DIR=github:org/core-layer#staging
// nuxt.config.ts
...
extends: [
[
process.env.CORE_DIR,
{
auth: process.env.GIT_AUTH,
giget: { force: true, forceClean: true, dir: './layers/core' },
},
],
]Styling is managed with a set of CSS custom properties that can be configured for each property. Individual components and pages can also be overridden by each property site by creating a file with the same directory structure as the core layer. Each property site can also register its own routes, plugins, and modules.

The core layer also contains all of the shared GraphQL queries, mutations, and wiring to components. Many pages of the site are driven by a flexible content model that allows content editors to create pages with a variety of components. The content is exposed in the GraphQL API as a union type that can be mapped to individual components in the front-end. A simple naming convention of matching component filenames to the GraphQL __typename allows automatic mapping of content to components. Types for all queries and fragments are generated with GraphQL Codegen and components are created to implement each flexible component type from the GraphQL API.
fragment flexComponentFields on FlexComponent {
id
instance {
__typename
... on OfferPromoComponent {
__typename
offers {
...offerPromoFields
}
}
# ...etc
}
}
<template>
<section class="flex-components">
<template v-for="(item, i) in props.items">
<component
v-if="isValidComponent(item.instance?.__typename)"
:is="item.instance?.__typename"
:instance="item.instance"
:key="item.id"
></component>
</template>
</section>
</template>
<script lang="ts" setup>
import type { FlexComponentFieldsFragment } from '#gql'
const props = defineProps<{
items: FlexComponentFieldsFragment[]
}>()
function isValidComponent(type: string | undefined): boolean {
if (!type) return false
let maybeComponent = resolveComponent(type)
return typeof maybeComponent !== 'string'
}
</script>FAE's GraphQL API returns images as objects with multiple asset URLs for building a responsive source set. The Vue components' props are specified to allow both FAE images or strings, so a helper is used to recursively convert any instance of the FAE image fragment type to also allow strings within the TypeScript space.
fragment faeImageFields on FaeImage {
id
alt
caption
assetUrl
assetUrlXs
assetUrlSm
assetUrlMd
assetUrlLg
assetUrlXl
assetUrlXxl
width
height
}
declare type FaeImage = import('#gql').FaeImageFieldsFragment | string
// Recursively converts all instances of FaeImageFieldsFragment to FaeImage in an object type
declare type MapFaeImages<T extends object> = {
[K in keyof T]: T[K] extends import('#gql').FaeImageFieldsFragment | null
? FaeImage | null
: T[K] extends import('#gql').FaeImageFieldsFragment
? FaeImage
: T[K] extends Array<object> | null
? MapFaeImages<NonNullable<T[K]>[number]>[] | null
: T[K] extends Array<object>
? MapFaeImages<NonNullable<T[K]>[number]>[]
: T[K] extends object | null
? MapFaeImages<T[K]>
: T[K] extends object
? MapFaeImages<T[K]>
: T[K]
}Then, when creating components to be mapped to the flex component types, the component props can be defined with another helper type to automatically map the generated GraphQL type. The core layer makes extensive use of GQL fragments and their generated types to define the props within components.
declare type ContentComponentProp<
T extends NonNullable<import('#gql').FlexComponentFieldsFragment['instance']>['__typename']
> = MapFaeImages<
Optional<
Extract<
import('#gql').FlexComponentFieldsFragment['instance'],
{
__typename: T
}
>,
'__typename'
>
>
<!-- ~/layers/core/components/OfferPromo.vue -->
<script setup lang="ts">
import type { OfferPromoFieldsFragment } from '#gql'
export type OfferPromoItem = MapFaeImages<OfferPromoFieldsFragment>
const props = defineProps<{
items: OfferPromoItem[]
}>()
</script>
<!-- ~/layers/core/components/flex/OfferPromoComponent.vue -->
<template>
<OfferPromo
v-if="useNotEmptyArray(props.instance.offers)"
:items="props.instance.offers"
/>
</template>
<script setup lang="ts">
const props = defineProps<{
instance: ContentComponentProp<'OfferPromoComponent'>
}>()
</script>Full type safety is maintained across all components and pages, and individual property sites can implement their own versions of a flex component by referencing the same GraphQL type from the core layer. New components can be added to the core layer and are automatically available to all property sites without any additional configuration.
The Meritage Collection's master brand site Stay Golden runs on the same core layer. It implements its own new components and overrides to show content from across all of the properties while still allowing access to the full library of components and pages from the core layer.
