[{"data":1,"prerenderedAt":680},["ShallowReactive",2],{"project-archipelago-corporate":3},{"_id":4,"date":5,"description":6,"descriptionText":647,"image":648,"images":649,"preview":652,"previewText":652,"slug":653,"tags":654,"title":678,"url":679},"d5614805-4cbe-46c3-ad41-8fab6b0c6c6a","2026",[7,18,44,52,60,70,78,86,94,102,110,118,130,151,163,183,195,207,235,247,255,263,271,279,287,295,303,311,319,327,335,343,351,359,367,375,383,399,439,447,455,471,479,487,511,519,527,539,575,587,607,619],{"_key":8,"_type":9,"children":10,"markDefs":16,"style":17},"d955a24e83a9","block",[11],{"_key":12,"_type":13,"marks":14,"text":15},"6029f3d88915","span",[],"Project Overview",[],"h2",{"_key":19,"_type":9,"children":20,"markDefs":42,"style":43},"75634399be29",[21,25,30,34,38],{"_key":22,"_type":13,"marks":23,"text":24},"c6c67911187a",[],"Archipelago International is one of Southeast Asia's largest hotel management companies, operating ",{"_key":26,"_type":13,"marks":27,"text":29},"5dd3d5c95819",[28],"strong","200+ properties",{"_key":31,"_type":13,"marks":32,"text":33},"c2d635e29484",[]," across ",{"_key":35,"_type":13,"marks":36,"text":37},"a6e74a1b9575",[28],"13+ hotel brands",{"_key":39,"_type":13,"marks":40,"text":41},"79aa17d6e377",[]," — including Aston, Harper, Quest, Neo, and favehotel. This is their primary corporate web presence: the single platform that communicates the group's brand identity, showcases its hotel portfolio, drives direct bookings, and attracts talent.",[],"normal",{"_key":45,"_type":9,"children":46,"markDefs":51,"style":43},"e7ad298c5148",[47],{"_key":48,"_type":13,"marks":49,"text":50},"11e87a2e33d8",[],"The site needed to perform at enterprise scale — fast global load times, airtight SEO, seamless booking flows, and a maintainable codebase that multiple developers could extend across many brands and services without stepping on each other.",[],{"_key":53,"_type":9,"children":54,"markDefs":59,"style":17},"09dd07da5abe",[55],{"_key":56,"_type":13,"marks":57,"text":58},"8e97d9a2be19",[],"Business Outcomes",[],{"_key":61,"_type":9,"children":62,"level":67,"listItem":68,"markDefs":69,"style":43},"6d38dd4da74a",[63],{"_key":64,"_type":13,"marks":65,"text":66},"030ecc019f2e",[],"Unified digital presence for 13+ hotel brands under a single, consistent experience",1,"bullet",[],{"_key":71,"_type":9,"children":72,"level":67,"listItem":68,"markDefs":77,"style":43},"29187fad1945",[73],{"_key":74,"_type":13,"marks":75,"text":76},"7d5c695277e5",[],"Reduced time-to-market for new brand and service pages through a scalable component architecture",[],{"_key":79,"_type":9,"children":80,"level":67,"listItem":68,"markDefs":85,"style":43},"dcff677fa6c9",[81],{"_key":82,"_type":13,"marks":83,"text":84},"16aed3f75a18",[],"Improved organic search visibility via structured Schema.org data (12+ types) and pre-rendered pages with full meta coverage",[],{"_key":87,"_type":9,"children":88,"level":67,"listItem":68,"markDefs":93,"style":43},"75f10d2b615b",[89],{"_key":90,"_type":13,"marks":91,"text":92},"aa72ba69b915",[],"Enabled direct bookings and appointment scheduling through the Integrated Booking Engine, reducing dependency on third-party OTAs",[],{"_key":95,"_type":9,"children":96,"level":67,"listItem":68,"markDefs":101,"style":43},"3e1e75bf07eb",[97],{"_key":98,"_type":13,"marks":99,"text":100},"94d75efb27a6",[],"Streamlined talent acquisition with a self-contained career portal — job search, detail pages, and multi-file applications — all without a third-party ATS frontend",[],{"_key":103,"_type":9,"children":104,"level":67,"listItem":68,"markDefs":109,"style":43},"3a901c90bc5e",[105],{"_key":106,"_type":13,"marks":107,"text":108},"9105845ca954",[],"Faster page delivery globally through AWS CloudFront + Cloudflare CDN with immutable cache headers on all static assets",[],{"_key":111,"_type":9,"children":112,"markDefs":117,"style":17},"ae40df5a1cc3",[113],{"_key":114,"_type":13,"marks":115,"text":116},"8c6c2e51a009",[],"What I Built",[],{"_key":119,"_type":9,"children":120,"markDefs":129,"style":43},"f780162acfc1",[121,125],{"_key":122,"_type":13,"marks":123,"text":124},"dc7e88a67597",[28],"Multi-Brand Discovery System",{"_key":126,"_type":13,"marks":127,"text":128},"6000d943f511",[]," A unified brand hub presenting 13+ hotel brands, each with dedicated landing pages, hotel listings, and location-based search — all powered by a single typed API service layer.",[],{"_key":131,"_type":9,"children":132,"markDefs":150,"style":43},"a5d224b2edb1",[133,137,141,146],{"_key":134,"_type":13,"marks":135,"text":136},"62438bcdd8b8",[28],"Integrated Booking Engine (IBE)",{"_key":138,"_type":13,"marks":139,"text":140},"0d263f4073fd",[]," A custom multi-step booking flow covering room search, multi-guest configuration, pricing tiers, appointment scheduling, and payment — built as an isolated component system (",{"_key":142,"_type":13,"marks":143,"text":145},"69ba55001ffa",[144],"code","components/integrated-booking-engine/",{"_key":147,"_type":13,"marks":148,"text":149},"8ac9cdb4e965",[],") that can be surfaced anywhere on the site.",[],{"_key":152,"_type":9,"children":153,"markDefs":162,"style":43},"3b36d2366af1",[154,158],{"_key":155,"_type":13,"marks":156,"text":157},"35e6cf0bd8f9",[28],"Career Portal",{"_key":159,"_type":13,"marks":160,"text":161},"c4593070bc54",[]," Full job search with filters, individual job detail pages, and a multi-file application form (CV + supporting documents) with FormData serialization and server-side submission — no third-party job board embed required.",[],{"_key":164,"_type":9,"children":165,"markDefs":182,"style":43},"ac5bd57a1f24",[166,170,174,178],{"_key":167,"_type":13,"marks":168,"text":169},"8cae4dc248ff",[28],"News & Content Hub",{"_key":171,"_type":13,"marks":172,"text":173},"c34015a217c7",[]," A paginated news listing and article detail system with PDF export capability, Schema.org ",{"_key":175,"_type":13,"marks":176,"text":177},"61fd5d107201",[144],"NewsArticle",{"_key":179,"_type":13,"marks":180,"text":181},"4ebb6e0308e3",[]," structured data, and dynamic Open Graph metadata per article.",[],{"_key":184,"_type":9,"children":185,"markDefs":194,"style":43},"f66c7b98c5cb",[186,190],{"_key":187,"_type":13,"marks":188,"text":189},"ec31c80e0f28",[28],"Adaptive Header & Navigation",{"_key":191,"_type":13,"marks":192,"text":193},"77650e5f8a1a",[]," A dual-mode header (desktop full-nav / mobile drawer) with scroll-aware transparency — transparent with a vignette on the homepage hero, opaque everywhere else. Manages dialogs (Book Now, Contact Us, Team Profiles) through a centralised dialog registry in the app store.",[],{"_key":196,"_type":9,"children":197,"markDefs":206,"style":43},"57863a1b5423",[198,202],{"_key":199,"_type":13,"marks":200,"text":201},"511bfb14ad47",[28],"HLS Video Streaming",{"_key":203,"_type":13,"marks":204,"text":205},"3c6dfd70b031",[]," Cloudflare Stream-hosted video with a custom player built on Hls.js + Vidstack, supporting adaptive bitrate streaming and fallback handling.",[],{"_key":208,"_type":9,"children":209,"markDefs":234,"style":43},"db449ce458cb",[210,214,218,222,226,230],{"_key":211,"_type":13,"marks":212,"text":213},"8f7e28143864",[28],"SEO & Structured Data Layer",{"_key":215,"_type":13,"marks":216,"text":217},"7edb85aa0f99",[]," A ",{"_key":219,"_type":13,"marks":220,"text":221},"d9a2d8551241",[144],"useSEO()",{"_key":223,"_type":13,"marks":224,"text":225},"d3c9eebe6383",[]," composable wrapping all OpenGraph, Twitter Card, and canonical tag logic. Paired with 12+ Schema.org structured data types (Organization, Hotel, Article, JobPosting, BreadcrumbList, Service, VideoObject, Event, and more) via ",{"_key":227,"_type":13,"marks":228,"text":229},"fa1f14e0508b",[144],"nuxt-schema-org",{"_key":231,"_type":13,"marks":232,"text":233},"50cc41300be3",[],". Auto-generated sitemap with per-route priorities.",[],{"_key":236,"_type":9,"children":237,"markDefs":246,"style":43},"808b5d6a9e41",[238,242],{"_key":239,"_type":13,"marks":240,"text":241},"67cce3f01852",[28],"Form & Security Layer",{"_key":243,"_type":13,"marks":244,"text":245},"e645919daa3c",[]," All public-facing forms (inquiry, career application, appointment, brochure download) are protected by Cloudflare Turnstile bot detection and managed through a Pinia form store for consistent validation and state handling.",[],{"_key":248,"_type":9,"children":249,"markDefs":254,"style":17},"2c6ba1803760",[250],{"_key":251,"_type":13,"marks":252,"text":253},"9302733f154a",[],"Stack",[],{"_key":256,"_type":9,"children":257,"level":67,"listItem":68,"markDefs":262,"style":43},"95708c1628bf",[258],{"_key":259,"_type":13,"marks":260,"text":261},"dfe665d9ec20",[],"Nuxt 3.17 (SSG)",[],{"_key":264,"_type":9,"children":265,"level":67,"listItem":68,"markDefs":270,"style":43},"edb2bf55fb8d",[266],{"_key":267,"_type":13,"marks":268,"text":269},"6bc42b8294c3",[],"Vue 3.5, TypeScript 5.5",[],{"_key":272,"_type":9,"children":273,"level":67,"listItem":68,"markDefs":278,"style":43},"843b537359d3",[274],{"_key":275,"_type":13,"marks":276,"text":277},"e01ea6d338a4",[],"Vuetify 3.7",[],{"_key":280,"_type":9,"children":281,"level":67,"listItem":68,"markDefs":286,"style":43},"babd5971c8d9",[282],{"_key":283,"_type":13,"marks":284,"text":285},"da4d47645a90",[],"Pinia 3",[],{"_key":288,"_type":9,"children":289,"level":67,"listItem":68,"markDefs":294,"style":43},"ca0377772189",[290],{"_key":291,"_type":13,"marks":292,"text":293},"51f0c843ec5a",[],"Vite 6",[],{"_key":296,"_type":9,"children":297,"level":67,"listItem":68,"markDefs":302,"style":43},"d7c64f392a88",[298],{"_key":299,"_type":13,"marks":300,"text":301},"0b0f5f840500",[],"SCSS, Vuetify theming",[],{"_key":304,"_type":9,"children":305,"level":67,"listItem":68,"markDefs":310,"style":43},"fd2413534239",[306],{"_key":307,"_type":13,"marks":308,"text":309},"88ad90716c3a",[],"@nuxt/image — WebP, responsive density variants, multi-CDN",[],{"_key":312,"_type":9,"children":313,"level":67,"listItem":68,"markDefs":318,"style":43},"03f5342364a8",[314],{"_key":315,"_type":13,"marks":316,"text":317},"de61d0eb04b7",[],"Video.js, Vidstack, Hls.js",[],{"_key":320,"_type":9,"children":321,"level":67,"listItem":68,"markDefs":326,"style":43},"92893249ea9d",[322],{"_key":323,"_type":13,"marks":324,"text":325},"be869e91c260",[],"Splide Vue, Swiper",[],{"_key":328,"_type":9,"children":329,"level":67,"listItem":68,"markDefs":334,"style":43},"66ff0043c690",[330],{"_key":331,"_type":13,"marks":332,"text":333},"888a1db61594",[],"dayjs, @vuepic/vue-datepicker",[],{"_key":336,"_type":9,"children":337,"level":67,"listItem":68,"markDefs":342,"style":43},"ff56c530ab36",[338],{"_key":339,"_type":13,"marks":340,"text":341},"738fc68141bf",[],"SEO: nuxt-schema-org, @nuxtjs/sitemap, @nuxtjs/robots",[],{"_key":344,"_type":9,"children":345,"level":67,"listItem":68,"markDefs":350,"style":43},"0e16da06ab19",[346],{"_key":347,"_type":13,"marks":348,"text":349},"eb1884fcd8e3",[],"Google Tag Manager, Google Analytics 4",[],{"_key":352,"_type":9,"children":353,"level":67,"listItem":68,"markDefs":358,"style":43},"79ebd4cc1721",[354],{"_key":355,"_type":13,"marks":356,"text":357},"199dec71b4ce",[],"Cloudflare Turnstile",[],{"_key":360,"_type":9,"children":361,"level":67,"listItem":68,"markDefs":366,"style":43},"f83aae769211",[362],{"_key":363,"_type":13,"marks":364,"text":365},"9b2f56819c56",[],"Cloudflare Stream",[],{"_key":368,"_type":9,"children":369,"level":67,"listItem":68,"markDefs":374,"style":43},"2da175f1b359",[370],{"_key":371,"_type":13,"marks":372,"text":373},"cc39e7af5459",[],"AWS CodeBuild → S3 → CloudFront",[],{"_key":376,"_type":9,"children":377,"markDefs":382,"style":17},"2b3808c7eb41",[378],{"_key":379,"_type":13,"marks":380,"text":381},"284dfa55ff35",[],"Rendering Strategy",[],{"_key":384,"_type":9,"children":385,"markDefs":398,"style":43},"71f6e8a3d298",[386,390,394],{"_key":387,"_type":13,"marks":388,"text":389},"7213bcecac89",[],"The site is fully ",{"_key":391,"_type":13,"marks":392,"text":393},"1a658c2e6855",[28],"Static Site Generated (SSG)",{"_key":395,"_type":13,"marks":396,"text":397},"a61ae381eca2",[]," — all 14+ routes are pre-rendered at build time and served as static HTML from CloudFront. This delivers sub-100ms TTFB globally with zero cold starts.",[],{"_key":400,"_type":9,"children":401,"markDefs":438,"style":43},"2c265a545e71",[402,406,410,414,418,422,426,430,434],{"_key":403,"_type":13,"marks":404,"text":405},"2e8005372d58",[],"At the component level, the project enforces a ",{"_key":407,"_type":13,"marks":408,"text":409},"58a88c107310",[28],"lazy component loading pattern",{"_key":411,"_type":13,"marks":412,"text":413},"c175cbe853cf",[],": every page file (",{"_key":415,"_type":13,"marks":416,"text":417},"a3d0ea488bd9",[144],"pages/*.vue",{"_key":419,"_type":13,"marks":420,"text":421},"ded6394f0ed2",[],") contains only SEO metadata and a ",{"_key":423,"_type":13,"marks":424,"text":425},"6f4440a80cad",[144],"resolveComponent()",{"_key":427,"_type":13,"marks":428,"text":429},"b8553e58c727",[]," call. All logic, state, and templates live in a paired component file (",{"_key":431,"_type":13,"marks":432,"text":433},"d0890f5d6a0f",[144],"components/pages/[name]/Content.vue",{"_key":435,"_type":13,"marks":436,"text":437},"8cd58e0be78f",[],"). Vite treats each of these as a separate chunk, so the browser only downloads the code it needs for the current route.",[],{"_key":440,"_type":9,"children":441,"markDefs":446,"style":43},"2840e9811048",[442],{"_key":443,"_type":13,"marks":444,"text":445},"7d9f315a37e6",[],"pages/news.vue                      ← SEO metadata + resolveComponent() only\ncomponents/pages/news/Content.vue   ← All logic, state, and template\n",[],{"_key":448,"_type":9,"children":449,"markDefs":454,"style":43},"2b671a37a669",[450],{"_key":451,"_type":13,"marks":452,"text":453},"ff44772667ff",[],"Additional build-time optimisations:",[],{"_key":456,"_type":9,"children":457,"level":67,"listItem":68,"markDefs":470,"style":43},"8efa8eeb1c90",[458,462,466],{"_key":459,"_type":13,"marks":460,"text":461},"c56fc4be3e36",[],"Manual Rollup chunk splitting per ",{"_key":463,"_type":13,"marks":464,"text":465},"d83a7bffaf47",[144],"node_modules",{"_key":467,"_type":13,"marks":468,"text":469},"803e384392a0",[]," package (video, swiper, etc. in separate chunks)",[],{"_key":472,"_type":9,"children":473,"level":67,"listItem":68,"markDefs":478,"style":43},"3c8b4b514f13",[474],{"_key":475,"_type":13,"marks":476,"text":477},"a499df368e96",[],"CSS code splitting enabled",[],{"_key":480,"_type":9,"children":481,"level":67,"listItem":68,"markDefs":486,"style":43},"af2c3c354217",[482],{"_key":483,"_type":13,"marks":484,"text":485},"8f1b89d33939",[],"Nitro asset compression",[],{"_key":488,"_type":9,"children":489,"level":67,"listItem":68,"markDefs":510,"style":43},"7a3f71ff0c11",[490,494,498,502,506],{"_key":491,"_type":13,"marks":492,"text":493},"ab6d06932507",[],"Immutable cache headers on ",{"_key":495,"_type":13,"marks":496,"text":497},"b7b982897ba2",[144],"/_nuxt/**",{"_key":499,"_type":13,"marks":500,"text":501},"4a37de615925",[]," (1 month) and ",{"_key":503,"_type":13,"marks":504,"text":505},"24f3a8e25069",[144],"/images/**",{"_key":507,"_type":13,"marks":508,"text":509},"812c41c91b88",[]," (1 week)",[],{"_key":512,"_type":9,"children":513,"level":67,"listItem":68,"markDefs":518,"style":43},"51394e669b3b",[514],{"_key":515,"_type":13,"marks":516,"text":517},"2ca5f21d9b11",[],"Resource hints with visibility-based prefetch",[],{"_key":520,"_type":9,"children":521,"markDefs":526,"style":17},"10715b583a99",[522],{"_key":523,"_type":13,"marks":524,"text":525},"64f3fafac3ec",[],"Key Engineering Challenges Solved",[],{"_key":528,"_type":9,"children":529,"markDefs":538,"style":43},"89bfa3220612",[530,534],{"_key":531,"_type":13,"marks":532,"text":533},"46b1d96304b6",[28],"1. Code splitting at page and feature boundaries",{"_key":535,"_type":13,"marks":536,"text":537},"ba7e473c17dc",[]," With 13+ brands and 14+ pages sharing a large UI component library, naive bundling would have produced a monolithic JS payload. The lazy component pattern combined with manual Rollup chunk configuration ensures each route loads only its own code, with shared vendor packages cached separately in long-lived immutable chunks.",[],{"_key":540,"_type":9,"children":541,"markDefs":574,"style":43},"d22eb868bc20",[542,546,550,554,558,562,566,570],{"_key":543,"_type":13,"marks":544,"text":545},"67cb75b3ced5",[28],"2. Type-safe API consumption across a multi-endpoint backend",{"_key":547,"_type":13,"marks":548,"text":549},"a37f5af8c613",[]," The backend exposes two separate API bases (content API and brand/hotel API). Rather than scattering raw ",{"_key":551,"_type":13,"marks":552,"text":553},"305fe8526fdd",[144],"fetch",{"_key":555,"_type":13,"marks":556,"text":557},"60f5d3e32f6d",[]," calls, all endpoints are centralised in ",{"_key":559,"_type":13,"marks":560,"text":561},"856cce408f83",[144],"services/archipelago-service.ts",{"_key":563,"_type":13,"marks":564,"text":565},"2abfe8d159ef",[]," using a generic ",{"_key":567,"_type":13,"marks":568,"text":569},"ce6d4f5bf431",[144],"fetchJSON\u003CT>()",{"_key":571,"_type":13,"marks":572,"text":573},"d49baf2a46c0",[]," wrapper. Every function is fully typed, error handling is consistent, and adding a new endpoint is a one-line change.",[],{"_key":576,"_type":9,"children":577,"markDefs":586,"style":43},"e383d013e2c3",[578,582],{"_key":579,"_type":13,"marks":580,"text":581},"513f8380eaa9",[28],"3. Scroll-aware, dialog-aware header state without layout shift",{"_key":583,"_type":13,"marks":584,"text":585},"0d0da8eb197b",[]," The header needs to be transparent on the homepage hero, opaque when scrolled, and opaque whenever any dialog is open — across all breakpoints. This was solved by tracking scroll position, dialog registry state, and current route in the Pinia app store and deriving the header style from a single computed property, eliminating competing state sources and layout shift.",[],{"_key":588,"_type":9,"children":589,"markDefs":606,"style":43},"6e8cdb8a0d3a",[590,594,598,602],{"_key":591,"_type":13,"marks":592,"text":593},"a5ad3d09717f",[28],"4. HLS video in a statically generated Vue app",{"_key":595,"_type":13,"marks":596,"text":597},"404783ceca88",[]," Cloudflare Stream delivers video as HLS, which native ",{"_key":599,"_type":13,"marks":600,"text":601},"af247e0384c6",[144],"\u003Cvideo>",{"_key":603,"_type":13,"marks":604,"text":605},"dc30bc6e37e9",[]," elements don't support universally. Hls.js was integrated with a Vidstack wrapper to handle adaptive bitrate streaming, codec detection, and graceful fallback — all lazily loaded so the HLS parser is never downloaded on pages without video.",[],{"_key":608,"_type":9,"children":609,"markDefs":618,"style":43},"0b50c36e6677",[610,614],{"_key":611,"_type":13,"marks":612,"text":613},"928d179a390b",[28],"5. Multi-file career application submissions via static frontend",{"_key":615,"_type":13,"marks":616,"text":617},"ab1e1b998a7f",[]," Career applications require CV + supporting document uploads from a static site with no backend server. FormData serialisation is handled client-side, multipart POST requests go directly to the API with Bearer token auth, and Pinia form store manages upload progress, validation state, and success/error feedback — no server middleware required.",[],{"_key":620,"_type":9,"children":621,"markDefs":646,"style":43},"fa3b553a32dd",[622,626,630,634,638,642],{"_key":623,"_type":13,"marks":624,"text":625},"fbbbc8e3e146",[28],"6. Schema.org coverage at scale without repetition",{"_key":627,"_type":13,"marks":628,"text":629},"3e0e4f1853d0",[]," Each page type (article, job posting, hotel, service) needs different structured data. A set of composables (",{"_key":631,"_type":13,"marks":632,"text":633},"4aca1dc93ce3",[144],"useBreadcrumbSchema()",{"_key":635,"_type":13,"marks":636,"text":637},"4eb0d40d80c0",[],", ",{"_key":639,"_type":13,"marks":640,"text":641},"3f94fecb016c",[144],"useContactPageSchema()",{"_key":643,"_type":13,"marks":644,"text":645},"ae7fecda5fd3",[],", etc.) encapsulate the Schema.org generation logic per type. Page files call only the composables they need, keeping structured data co-located with SEO metadata and out of component logic.",[],"Project Overview\n\nArchipelago International is one of Southeast Asia's largest hotel management companies, operating 200+ properties across 13+ hotel brands — including Aston, Harper, Quest, Neo, and favehotel. This is their primary corporate web presence: the single platform that communicates the group's brand identity, showcases its hotel portfolio, drives direct bookings, and attracts talent.\n\nThe site needed to perform at enterprise scale — fast global load times, airtight SEO, seamless booking flows, and a maintainable codebase that multiple developers could extend across many brands and services without stepping on each other.\n\nBusiness Outcomes\n\nUnified digital presence for 13+ hotel brands under a single, consistent experience\n\nReduced time-to-market for new brand and service pages through a scalable component architecture\n\nImproved organic search visibility via structured Schema.org data (12+ types) and pre-rendered pages with full meta coverage\n\nEnabled direct bookings and appointment scheduling through the Integrated Booking Engine, reducing dependency on third-party OTAs\n\nStreamlined talent acquisition with a self-contained career portal — job search, detail pages, and multi-file applications — all without a third-party ATS frontend\n\nFaster page delivery globally through AWS CloudFront + Cloudflare CDN with immutable cache headers on all static assets\n\nWhat I Built\n\nMulti-Brand Discovery System A unified brand hub presenting 13+ hotel brands, each with dedicated landing pages, hotel listings, and location-based search — all powered by a single typed API service layer.\n\nIntegrated Booking Engine (IBE) A custom multi-step booking flow covering room search, multi-guest configuration, pricing tiers, appointment scheduling, and payment — built as an isolated component system (components/integrated-booking-engine/) that can be surfaced anywhere on the site.\n\nCareer Portal Full job search with filters, individual job detail pages, and a multi-file application form (CV + supporting documents) with FormData serialization and server-side submission — no third-party job board embed required.\n\nNews & Content Hub A paginated news listing and article detail system with PDF export capability, Schema.org NewsArticle structured data, and dynamic Open Graph metadata per article.\n\nAdaptive Header & Navigation A dual-mode header (desktop full-nav / mobile drawer) with scroll-aware transparency — transparent with a vignette on the homepage hero, opaque everywhere else. Manages dialogs (Book Now, Contact Us, Team Profiles) through a centralised dialog registry in the app store.\n\nHLS Video Streaming Cloudflare Stream-hosted video with a custom player built on Hls.js + Vidstack, supporting adaptive bitrate streaming and fallback handling.\n\nSEO & Structured Data Layer A useSEO() composable wrapping all OpenGraph, Twitter Card, and canonical tag logic. Paired with 12+ Schema.org structured data types (Organization, Hotel, Article, JobPosting, BreadcrumbList, Service, VideoObject, Event, and more) via nuxt-schema-org. Auto-generated sitemap with per-route priorities.\n\nForm & Security Layer All public-facing forms (inquiry, career application, appointment, brochure download) are protected by Cloudflare Turnstile bot detection and managed through a Pinia form store for consistent validation and state handling.\n\nStack\n\nNuxt 3.17 (SSG)\n\nVue 3.5, TypeScript 5.5\n\nVuetify 3.7\n\nPinia 3\n\nVite 6\n\nSCSS, Vuetify theming\n\n@nuxt/image — WebP, responsive density variants, multi-CDN\n\nVideo.js, Vidstack, Hls.js\n\nSplide Vue, Swiper\n\ndayjs, @vuepic/vue-datepicker\n\nSEO: nuxt-schema-org, @nuxtjs/sitemap, @nuxtjs/robots\n\nGoogle Tag Manager, Google Analytics 4\n\nCloudflare Turnstile\n\nCloudflare Stream\n\nAWS CodeBuild → S3 → CloudFront\n\nRendering Strategy\n\nThe site is fully Static Site Generated (SSG) — all 14+ routes are pre-rendered at build time and served as static HTML from CloudFront. This delivers sub-100ms TTFB globally with zero cold starts.\n\nAt the component level, the project enforces a lazy component loading pattern: every page file (pages/*.vue) contains only SEO metadata and a resolveComponent() call. All logic, state, and templates live in a paired component file (components/pages/[name]/Content.vue). Vite treats each of these as a separate chunk, so the browser only downloads the code it needs for the current route.\n\npages/news.vue                      ← SEO metadata + resolveComponent() only\ncomponents/pages/news/Content.vue   ← All logic, state, and template\n\n\nAdditional build-time optimisations:\n\nManual Rollup chunk splitting per node_modules package (video, swiper, etc. in separate chunks)\n\nCSS code splitting enabled\n\nNitro asset compression\n\nImmutable cache headers on /_nuxt/** (1 month) and /images/** (1 week)\n\nResource hints with visibility-based prefetch\n\nKey Engineering Challenges Solved\n\n1. Code splitting at page and feature boundaries With 13+ brands and 14+ pages sharing a large UI component library, naive bundling would have produced a monolithic JS payload. The lazy component pattern combined with manual Rollup chunk configuration ensures each route loads only its own code, with shared vendor packages cached separately in long-lived immutable chunks.\n\n2. Type-safe API consumption across a multi-endpoint backend The backend exposes two separate API bases (content API and brand/hotel API). Rather than scattering raw fetch calls, all endpoints are centralised in services/archipelago-service.ts using a generic fetchJSON\u003CT>() wrapper. Every function is fully typed, error handling is consistent, and adding a new endpoint is a one-line change.\n\n3. Scroll-aware, dialog-aware header state without layout shift The header needs to be transparent on the homepage hero, opaque when scrolled, and opaque whenever any dialog is open — across all breakpoints. This was solved by tracking scroll position, dialog registry state, and current route in the Pinia app store and deriving the header style from a single computed property, eliminating competing state sources and layout shift.\n\n4. HLS video in a statically generated Vue app Cloudflare Stream delivers video as HLS, which native \u003Cvideo> elements don't support universally. Hls.js was integrated with a Vidstack wrapper to handle adaptive bitrate streaming, codec detection, and graceful fallback — all lazily loaded so the HLS parser is never downloaded on pages without video.\n\n5. Multi-file career application submissions via static frontend Career applications require CV + supporting document uploads from a static site with no backend server. FormData serialisation is handled client-side, multipart POST requests go directly to the API with Bearer token auth, and Pinia form store manages upload progress, validation state, and success/error feedback — no server middleware required.\n\n6. Schema.org coverage at scale without repetition Each page type (article, job posting, hotel, service) needs different structured data. A set of composables (useBreadcrumbSchema(), useContactPageSchema(), etc.) encapsulate the Schema.org generation logic per type. Page files call only the composables they need, keeping structured data co-located with SEO metadata and out of component logic.","/images/projects/archipelago/SCR-20260325-uclm.webp",[650],{"alt":651,"caption":651,"src":648},"e-commerce page image","Archipelago International is one of Southeast Asia's largest hotel management companies, operating 200+ properties across 13+ hotel brands — including Aston, Harper, Quest, Neo, and favehotel. This is their primary corporate web presence: the single platform that communicates the group's brand identity, showcases its hotel portfolio, drives direct bookings, and attracts talent.","archipelago-corporate",[655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,157],"Nuxt 3","TypeScript","Tailwind","Pinia","SSG","Vite","SCSS"," REST API"," Cloudflare CDN"," Cloudflare Stream"," Cloudflare Turnstile"," AWS S3"," AWS CloudFront"," CI/CD"," GTM GA4"," Schema.org SEO"," HLS Streaming Hls.js","Vidstack","Nuxt/image","Hospitality","Enterprise","Multi-brand","Booking Engine","Archipelago International — Corporate Website","https://archipelagohotels.com",1775998939297]