{"id":16,"date":"2026-06-06T21:48:32","date_gmt":"2026-06-06T21:48:32","guid":{"rendered":"https:\/\/softwareproduction.eu\/wordpress\/?p=16"},"modified":"2026-06-07T01:21:54","modified_gmt":"2026-06-07T01:21:54","slug":"nuxt-and-contentful-cms-why-its-hard-and-how-it-can-work","status":"publish","type":"post","link":"https:\/\/softwareproduction.eu\/?p=16","title":{"rendered":"Nuxt and Contentful CMS \u2014 Why It&#8217;s Hard and How It Can Work"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><em>Ninth in a series about migrating from legacy architectures to a modern Nuxt 4 stack.<\/em><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Fundamental Tension<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nuxt is a <strong>file-based router<\/strong>. You create <code>pages\/products\/index.vue<\/code>, and the framework serves it at <code>\/products<\/code>. Routes are known at build time. The directory structure <em>is<\/em> the URL structure.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Contentful \u2014 a headless CMS \u2014 is the opposite. Content editors define the URL structure at runtime. They create pages, set slugs, rearrange the information architecture \u2014 and the application must serve whatever path they invent, without a code deployment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is not a bug \u2014 it is a mismatch between two design philosophies. Any team that integrates a headless CMS with Nuxt, Next.js, or any file-based router runs into it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So why use Nuxt at all? Because the CMS owns the content, not the application. A production web application still needs SSR, authentication, form validation, tracking, server middleware, and dozens of other concerns that only a full application framework covers. The routing mismatch is one problem to solve \u2014 not a reason to abandon the framework that solves everything else.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Solution: A Single Catch-All Route<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">One file: <code>pages\/[...id].vue<\/code>. Nuxt&#8217;s catch-all route matches every path not handled by a more specific route file.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n    subgraph Nuxt_Route_Resolution\n        A[\/Request: \/about\/] --&gt;|exists| B[pages\/about.vue]\n        C[\/Request: \/service\/faq\/] --&gt;|exists| D[pages\/service\/faq.vue]\n        E[\/Request: \/anything\/else\/] --&gt;|no specific file| F[\"pages\/...id.vue&lt;br\/&gt;catch-all\"]\n    end<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The catch-all route does one thing: it queries Contentful for a page entry matching the current URL path. If Contentful returns a page, the component renders it. If not, it returns a 404.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n    A[\"Browser requests&lt;br\/&gt;\/products\/premium-plan\"] --&gt; B[\"pages\/...id.vue\"]\n    B --&gt; C[\"Extract path&lt;br\/&gt;'\/products\/premium-plan'\"]\n    C --&gt; D[\"GraphQL query&lt;br\/&gt;pageByPath(path)\"]\n    D --&gt; E{Contentful page found?}\n    E -- No --&gt; F[Return 404]\n    E -- Yes --&gt; G[\"Contentful returns Page entry&lt;br\/&gt;type, title, sections\"]\n    G --&gt; H[\"Render \u2039app-page :page='page' \/\u203a\"]\n    H --&gt; I[\"Iterate sections\"]\n    I --&gt; J[\"\u2039component :is=#quot;resolve(section.type)#quot; \/\u203a&lt;br\/&gt;for each section\"]<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The CMS owns the URL structure; the application owns the rendering. Neither constrains the other.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Code Generation: From Content Model to Running Components<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A CMS page is not a single component \u2014 it is a <strong>list of content sections<\/strong>, each with a type. The application maps each content type to a Vue component and renders them dynamically. Doing this wiring by hand for dozens of content types does not scale. A code generator reads the Contentful content model schema via GraphQL introspection and produces everything needed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For every content type, the generator outputs:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>A GraphQL query<\/strong> that requests all scalar fields for that type, so the application always fetches exactly what the Contentful schema defines.<\/li>\n<li><strong>A Vue component<\/strong> that receives the Contentful entry as a strongly typed prop, with full TypeScript support for every field out of the box.<\/li>\n<\/ol>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n    subgraph Contentful\n        P[\"Page: 'Homepage'\"] --&gt; S1[HeroSection]\n        P --&gt; S2[TeaserGrid]\n        P --&gt; S3[ProductComparison]\n        P --&gt; S4[TestimonialCarousel]\n        P --&gt; S5[FAQAccordion]\n        P --&gt; S6[ContactForm]\n    end\n\n    subgraph Generated_Vue_Components\n        C1[\"\u2039HeroSection :model='...' \/\u203a\"]\n        C2[\"\u2039TeaserGrid :model='...' \/\u203a\"]\n        C3[\"\u2039ProductComparison :model='...' \/\u203a\"]\n        C4[\"\u2039TestimonialCarousel :model='...' \/\u203a\"]\n        C5[\"\u2039FAQAccordion :model='...' \/\u203a\"]\n        C6[\"\u2039ContactForm :model='...' \/\u203a\"]\n    end\n\n    S1 --&gt; C1\n    S2 --&gt; C2\n    S3 --&gt; C3\n    S4 --&gt; C4\n    S5 --&gt; C5\n    S6 --&gt; C6<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">When content editors add a new content type in Contentful, the build pipeline generates the corresponding query and component. The developer only supplies rendering logic and styling \u2014 data fetching, types, and component mapping are handled. A content editor adds a new section, and the next page load renders it. No deployment, no PR, no coordination with the development team.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">How Content Entries Are Fetched: The Stub-and-Load Pattern<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A Contentful content model is a <strong>graph<\/strong>. A page references sections, sections reference teasers, teasers reference images and links. Loading the entire graph in one request would hit Contentful&#8217;s API response size limits, transfer unnecessary data, and create an all-or-nothing loading bottleneck.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead, <strong>every Contentful entry is fetched at minimal depth, and every reference is initially just a stub.<\/strong><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What Is a Stub?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A stub is a content entry stripped to two fields: <code>__typename<\/code> (content type) and <code>sys.id<\/code> (unique identifier). No title, no body text, no image URLs \u2014 just enough to identify <em>what<\/em> the entry is and <em>where<\/em> to fetch it.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n    subgraph Full_Entry\n        A[\"__typename: 'Section'\"]\n        B[\"sys.id: 'abc123'\"]\n        C[\"title: 'Our Services'\"]\n        D[\"backgroundColor: 'white'\"]\n        E[\"items: [...]\"]\n        F[\"sectionText: {...}\"]\n    end\n\n    subgraph Stub\n        G[\"__typename: 'Section'\"]\n        H[\"sys.id: 'abc123'\"]\n    end<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>SysFields<\/code> GraphQL fragment \u2014 automatically applied to all reference fields in generated queries \u2014 implements this:<\/p>\n\n\n\n<pre><code class=\"language-graphql\">fragment SysFields on Entry {\n  __typename\n  sys {\n    id\n  }\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Minimal-Depth Queries<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When a generated query fetches a content type, it requests <strong>all scalar fields<\/strong> of that type (strings, numbers, booleans, rich text) but only the <code>SysFields<\/code> stub for every reference. Collections of references return arrays of stubs.<\/p>\n\n\n\n<pre><code class=\"language-graphql\"># Generated Query: cfSection($id)\nquery cfSection($id: String!) {\n  section(id: $id) {\n    __typename\n    sys { id }\n    anchor              # scalar: fetched in full\n    backgroundColor     # scalar: fetched in full\n    sectionHeadingH2    # scalar: fetched in full\n    sectionText { ... } # rich text: fetched in full\n    itemsCollection {\n      items {\n        __typename      # stub: only typename + id\n        sys { id }\n      }\n    }\n    mediaCollection {\n      items {\n        __typename      # stub: only typename + id\n        sys { id }\n      }\n    }\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Each entry is self-contained and flat. It knows its own data and the identities of its children, but not their content.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How Vue Components Load the Tree<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Every generated Vue component follows the same pattern:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>It accepts a <code>model<\/code> prop \u2014 which can be either a <strong>stub<\/strong> (just <code>__typename<\/code> and <code>sys.id<\/code>) or a <strong>fully loaded entry<\/strong>.<\/li>\n<li>It calls a generated composable (<code>useCf{ContentType}Entry<\/code>) that delegates to <code>useOrLoadCfEntry<\/code>.<\/li>\n<li><code>useOrLoadCfEntry<\/code> always triggers a GraphQL fetch by ID \u2014 but Apollo&#8217;s cache handles the rest.<\/li>\n<\/ol>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n    A[\"Component receives prop&lt;br\/&gt;model: stub or full entry\"] --&gt; B[\"useCfSectionEntry(() =&gt; props.model)\"]\n    B --&gt; C[\"useOrLoadCfEntry(fetchById, () =&gt; model)\"]\n    C --&gt; D{Apollo cache hit?}\n    D -- Yes --&gt; E[\"Return cached data&lt;br\/&gt;No network request\"]\n    D -- No --&gt; F[Fetch from CMS by ID]\n    F --&gt; G[Store in Apollo cache]\n    G --&gt; E<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The component renders its scalar fields and passes each child reference to the next generated component, repeating the cycle.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The Rendering Cascade<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">On first page load, this creates a cascade of individual fetches, each triggered when a Vue component mounts:<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n    A[\"Browser requests&lt;br\/&gt;\/products\/premium-plan\"] --&gt; B[\"cf-page loads Page entry&lt;br\/&gt;(title, slug, stubs for sections, header, footer)\"]\n\n    B --&gt; C[\"cf-section loads Section 'abc123'&lt;br\/&gt;(scalars + stubs)\"]\n    C --&gt; D[cf-teaser loads Teaser 'def456']\n    D --&gt; E[cf-image loads Image 'ghi789']\n    D --&gt; F[cf-link loads Link 'jkl012']\n    C --&gt; G[cf-slider loads Slider 'mno345']\n\n    B --&gt; H[cf-header loads Header 'pqr678']\n    B --&gt; I[cf-footer loads Footer 'stu901']<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Each component loads exactly one entry from Contentful. The parent passes its child stubs as props; each child component independently calls <code>useOrLoadCfEntry<\/code> to fetch the full entry by ID. The Vue component tree <em>is<\/em> the assembled page \u2014 it mirrors the content graph through composition, each component owning its own data.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Every <code>useOrLoadCfEntry<\/code> call goes through <code>useGraphQlQuery<\/code>, which checks a per-URL cache before hitting GraphQL. On the first SSR render of a URL, each query result is collected in a request-scoped <code>Map<\/code> and persisted to Redis after render. On subsequent requests, the plugin loads the entire <code>Map<\/code> in a single read at request start. Each component&#8217;s query then short-circuits from the <code>Map<\/code> \u2014 no Apollo Client, no Apollo Server, no Contentful call. This turns 40 individual round-trips into one bulk cache lookup.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n    subgraph First_SSR_Request\n        A1[Component query] --&gt; B1[useGraphQlQuery]\n        B1 --&gt; C1{URL-scoped Map has entry?}\n        C1 -- No --&gt; D1[Call Contentful via GraphQL]\n        D1 --&gt; E1[Store result in Map]\n    end\n\n    subgraph Persist\n        E1 --&gt; F1[\"Persist Map to Redis&lt;br\/&gt;per URL after render\"]\n    end\n\n    subgraph Subsequent_Requests\n        G1[Load Map from Redis at request start] --&gt; H1[Component query]\n        H1 --&gt; I1[useGraphQlQuery]\n        I1 --&gt; J1{Map has entry?}\n        J1 -- Yes --&gt; K1[\"Return cached result&lt;br\/&gt;Skip Apollo\/Contentful\"]\n    end<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">The Polymorphic Dispatch Component<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Reference collections in Contentful are polymorphic \u2014 a section&#8217;s <code>items<\/code> can contain teasers, sliders, images, FAQ accordions, or anything else. The generated code routes each stub to the correct component based on <code>__typename<\/code>:<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n    A[\"cf-section-items-item&lt;br\/&gt;receives stub\"] --&gt; B{__typename}\n    B -- \"Teaser\" --&gt; C[\"\u2039cf-teaser :model='stub' \/\u203a\"]\n    B -- \"Slider\" --&gt; D[\"\u2039cf-slider :model='stub' \/\u203a\"]\n    B -- \"Image\" --&gt; E[\"\u2039cf-image :model='stub' \/\u203a\"]\n    B -- \"FAQ\" --&gt; F[\"\u2039cf-faq :model='stub' \/\u203a\"]\n    B -- other\/unknown --&gt; G[\"\u2039contentful-error \/\u203a\"]<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Each resolved component calls its own <code>useCf{Type}Entry<\/code> composable to fetch the full entry by ID. The developer never writes dispatch logic \u2014 the code generator produces the complete dispatch tree from the schema.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Apollo Cache as the Deduplication Layer<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Every entry is fetched by unique ID, so Apollo&#8217;s <code>InMemoryCache<\/code> deduplicates naturally. If the same entry appears in multiple places \u2014 shared footer, reused teaser, global navigation \u2014 it is fetched once. Subsequent references reuse the cached result.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Conditional Entries and Deferred Loading<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Some Contentful entries have conditions attached \u2014 visibility rules evaluated at runtime (time-based, A\/B test variants, feature flags). When conditions hide an entry, its children are never loaded. The content tree remains intentionally incomplete.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">When a previously cached page is rendered again and a condition changes \u2014 making a previously hidden entry visible \u2014 the stub for that entry is still present in the parent&#8217;s data. The child component mounts, calls <code>useOrLoadCfEntry<\/code>, and the entry is fetched from Contentful for the first time. The component tree grows to include the newly visible branch.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The page is <strong>eventually complete<\/strong>: as conditions change and new branches become visible, the corresponding components mount and load their entries on demand, without requiring a full page reload.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n    A[Parent entry with stub child] --&gt; B{Condition true now?}\n    B -- No --&gt; C[\"Child component not mounted&lt;br\/&gt;No fetch\"]\n    B -- Yes --&gt; D[Child component mounts]\n    D --&gt; E[useOrLoadCfEntry for child ID]\n    E --&gt; F[\"Fetch from Contentful&lt;br\/&gt;(if not cached)\"]\n    F --&gt; G[Render newly visible subtree]<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Why This Pattern Works<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>No deep queries<\/strong>: Each fetch returns a single, flat object. No nested queries that risk hitting Contentful API limits.<\/li>\n<li><strong>Incremental assembly<\/strong>: The page builds itself component by component \u2014 each one independently loading its own data. No all-or-nothing loading.<\/li>\n<li><strong>Automatic deduplication<\/strong>: Apollo&#8217;s cache ensures each entry is fetched exactly once, regardless of how many components reference it.<\/li>\n<li><strong>Cache-friendly<\/strong>: Once all components have rendered during SSR, their individual query results are collected and cached as a set per URL (detailed in the following article).<\/li>\n<li><strong>Condition-safe<\/strong>: Entries behind conditions are loaded on demand. The tree adapts to runtime state without refetching the entire page.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">The Trade-Off: Request Volume<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The downside is obvious: many small requests instead of one large one. A page with 40 content entries issues 40 individual GraphQL requests to Contentful on first load. This hits rate limits faster, especially under traffic spikes when multiple server instances render concurrently. Exponential backoff (described below) and aggressive page-level caching (next article) exist specifically to mitigate this. Without caching, this pattern would not scale.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">SEO and SSR: The Critical Combination<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Content-driven routing only works for SEO if the server renders the full page. Search engines need complete HTML, not a JavaScript shell that fetches content on the client.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Because Nuxt is an SSR framework, the catch-all route executes on the server:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>The server receives the request for <code>\/products\/premium-plan<\/code>.<\/li>\n<li>The catch-all route&#8217;s <code>setup()<\/code> function runs server-side.<\/li>\n<li>The GraphQL query fetches the page data from Contentful.<\/li>\n<li>Vue renders the full component tree to HTML.<\/li>\n<li><code>useHead()<\/code> sets the <code><title><\/code>, <code><meta description><\/code>, and Open Graph tags from Contentful fields.<\/li>\n<li>The browser receives complete, crawlable HTML.<\/li>\n<\/ol>\n\n\n\n<pre class=\"mermaid\">sequenceDiagram\n    participant User\n    participant Browser\n    participant Nuxt_SSR as Nuxt SSR Server\n    participant Contentful\n\n    User-&gt;&gt;Browser: Request \/products\/premium-plan\n    Browser-&gt;&gt;Nuxt_SSR: HTTP GET \/products\/premium-plan\n    Nuxt_SSR-&gt;&gt;Nuxt_SSR: Match pages\/[...id].vue&lt;br\/&gt;run setup()\n    Nuxt_SSR-&gt;&gt;Contentful: GraphQL pageByPath(path)\n    Contentful--&gt;&gt;Nuxt_SSR: Page data (title, sections, SEO fields)\n    Nuxt_SSR-&gt;&gt;Nuxt_SSR: Render Vue tree to HTML&lt;br\/&gt;useHead() for meta tags\n    Nuxt_SSR--&gt;&gt;Browser: HTML response&lt;br\/&gt;(fully rendered content)\n    Browser--&gt;&gt;User: Display crawlable page<\/pre>\n\n\n\n<pre><code class=\"language-text\">Search Engine Crawler receives:\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 &lt;html&gt;                                      \u2502\n\u2502   &lt;head&gt;                                    \u2502\n\u2502     &lt;title&gt;Premium Plan \u2014 Our Products&lt;\/title&gt;\n\u2502     &lt;meta name=\"description\"                \u2502\n\u2502           content=\"Discover our...\" \/&gt;      \u2502\n\u2502     &lt;meta property=\"og:image\" ... \/&gt;        \u2502\n\u2502   &lt;\/head&gt;                                   \u2502\n\u2502   &lt;body&gt;                                    \u2502\n\u2502     &lt;section class=\"hero\"&gt;...&lt;\/section&gt;     \u2502\n\u2502     &lt;section class=\"features\"&gt;...&lt;\/section&gt; \u2502\n\u2502     &lt;section class=\"faq\"&gt;...&lt;\/section&gt;      \u2502\n\u2502     &lt;!-- All content rendered, no JS needed --&gt;\n\u2502   &lt;\/body&gt;                                   \u2502\n\u2502 &lt;\/html&gt;                                     \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Redirects from Contentful<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">URLs change. Subscription plans get renamed, pages reorganized, campaigns expire. In traditional architectures, redirect rules live in web server config and need a deployment to change.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In a large enterprise application, redirect rules can be content entries. Editors create a redirect with a source path and destination. The application loads all redirects into an in-memory map at startup and checks incoming requests against it in server middleware.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">When Contentful publishes a change, a webhook fires and the application refreshes the redirect map \u2014 no restart, no deployment. A content editor adds a redirect and sees it take effect within seconds.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n    A[Content editor creates\/updates Redirect entry] --&gt; B[Contentful publishes change]\n    B --&gt; C[Webhook to Application]\n    C --&gt; D[Refresh in-memory redirect map]\n    subgraph Request_Flow\n        E[Incoming HTTP request] --&gt; F{Path in redirect map?}\n        F -- No --&gt; G[Continue normal routing]\n        F -- Yes --&gt; H[Return 301\/302 to target URL]\n    end<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Content Security Policy from Contentful<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The same pattern applies to Content Security Policy. Instead of hardcoding CSP directives in application config, the CSP is stored as a Contentful entry. A server plugin fetches it at runtime and applies it as an HTTP header.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The security team can update the CSP \u2014 add a script source, remove a deprecated domain \u2014 without involving developers or waiting for a deployment.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TB\n    A[\"Security team edits CSP entry&lt;br\/&gt;in Contentful\"] --&gt; B[Contentful publishes CSP]\n    B --&gt; C[\"Webhook or periodic fetch&lt;br\/&gt;by server plugin\"]\n    C --&gt; D[Update in-memory CSP config]\n    E[Incoming HTTP response] --&gt; F[\"Attach CSP header&lt;br\/&gt;from current config\"]<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Rate Limiting and Resilience<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Contentful&#8217;s APIs have rate limits. Hitting them during traffic spikes means pages render without content or return errors.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The mitigation is <strong>exponential backoff<\/strong> in the GraphQL client chain:<\/p>\n\n\n\n<pre class=\"mermaid\">sequenceDiagram\n    participant Client as GraphQL Client\n    participant Contentful\n\n    Client-&gt;&gt;Contentful: Request\n    Contentful--&gt;&gt;Client: 429 Too Many Requests\n    Client-&gt;&gt;Client: Wait 300ms (+ jitter)\n    Client-&gt;&gt;Contentful: Retry #1\n    Contentful--&gt;&gt;Client: 429\n    Client-&gt;&gt;Client: Wait 600ms (+ jitter)\n    Client-&gt;&gt;Contentful: Retry #2\n    Contentful--&gt;&gt;Client: 429\n    Client-&gt;&gt;Client: Wait 1200ms (+ jitter)\n    Client-&gt;&gt;Contentful: Retry #3\n    Contentful--&gt;&gt;Client: 200 OK<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The retry link handles both 429 (rate limit) and 5xx (server error) responses. Backoff increases exponentially up to 10 seconds. Jitter prevents thundering herd problems when multiple replicas retry at once.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">From the user&#8217;s perspective, the page loads slightly slower during a rate limit event. The system self-heals. Logs show warnings, not errors.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Preview Mode: Real-Time WYSIWYG<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Content editors need to see changes before publishing. Contentful provides a Preview API that returns draft content, but integrating it with an SSR application takes some work.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The system supports two modes:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Mode<\/th><th>API<\/th><th>Cache<\/th><th>Use Case<\/th><\/tr><\/thead><tbody><tr><td><strong>Published<\/strong><\/td><td>Delivery API<\/td><td>Aggressive caching<\/td><td>Production, public traffic<\/td><\/tr><tr><td><strong>Preview<\/strong><\/td><td>Preview API<\/td><td>No caching<\/td><td>CMS editor preview, draft content<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">A <code>preview<\/code> runtime flag switches the entire GraphQL client chain from the Delivery API to the Preview API. When an editor views the application inside Contentful&#8217;s preview iframe, they see draft content rendered by the same SSR pipeline that serves production traffic.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Contentful&#8217;s Live Preview SDK goes further: when the editor changes a field in the Contentful sidebar, a <code>postMessage<\/code> event fires to the application iframe. The application re-renders the affected component \u2014 near-real-time WYSIWYG without a page reload.<\/p>\n\n\n\n<pre class=\"mermaid\">sequenceDiagram\n    participant Editor as Content Editor\n    participant CMS as Contentful UI\n    participant App as Nuxt App (iframe)\n    participant GraphQL as GraphQL Client\n\n    Editor-&gt;&gt;CMS: Edit field value\n    CMS-&gt;&gt;CMS: Use Preview API data\n    CMS--&gt;&gt;App: postMessage(change event)\n    App-&gt;&gt;App: Determine affected component\n    App-&gt;&gt;GraphQL: Fetch draft content (Preview API&lt;br\/&gt;via preview flag)\n    GraphQL--&gt;&gt;App: Updated draft data\n    App--&gt;&gt;Editor: Re-render affected component&lt;br\/&gt;inside preview iframe<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Lessons Learned<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">The catch-all route is an enabler, not a hack<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A single <code>pages\/[...id].vue<\/code> serving all CMS-driven pages can feel wrong to developers used to explicit route files. It is the correct pattern for content-driven applications. The CMS is the router. The catch-all route is the adapter between the CMS URL model and Nuxt&#8217;s file-based routing.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Content editors should not need developers for URL changes<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Any system where URL changes require a deployment creates a bottleneck. The dev team becomes a gatekeeper for content operations. Storing redirects and URL structures in the CMS removes that bottleneck.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Runtime configuration from the CMS is a pattern, not a one-off<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">CSP from the CMS, redirects from the CMS, feature flags from the CMS \u2014 the pattern generalizes. Anything that needs to change without a deployment is a candidate for CMS-managed configuration. The requirement is reliable invalidation (webhooks + in-memory refresh) so changes propagate quickly.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Preview integration is harder than it looks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Making SSR work with Contentful&#8217;s preview mode means switching the API source, disabling all caches, handling the three-level iframe hierarchy (CMS \u2192 application \u2192 DevTools), and coordinating live update events across component boundaries. Budget real time for this.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">What&#8217;s Next<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Article 6<\/strong>: <em>Performance Optimization \u2014 Chasing That 100% Lighthouse Score<\/em> \u2014 Multi-tier caching, deferred hydration, and the techniques that add up to near-perfect scores.<\/li>\n<li><strong>Article 7<\/strong>: <em>Type-Safe Form Generation from GraphQL Introspection<\/em> \u2014 How the backend schema drives form rendering and validation without any frontend duplication.<\/li>\n<li><strong>Article 8<\/strong>: <em>The Modular Architecture \u2014 Independent Building Blocks<\/em> \u2014 How 35+ custom Nuxt modules create a maintainable, extensible system.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Munir Husseini is a software architect specializing in full-stack TypeScript, .NET, and cloud-native architectures.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Ninth in a series about migrating from legacy architectures to a modern Nuxt 4 stack. The Fundamental Tension Nuxt is a file-based router. You create pages\/products\/index.vue, and the framework serves it at \/products. Routes are known at build time. The directory structure is the URL structure. Contentful \u2014 a headless CMS \u2014 is the opposite. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":226,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[5],"tags":[],"class_list":["post-16","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-advanced-web-app-with-nuxt-and-net"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Nuxt and Contentful CMS \u2014 Why It&#039;s Hard and How It Can Work - Software Production<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/softwareproduction.eu\/?p=16\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Nuxt and Contentful CMS \u2014 Why It&#039;s Hard and How It Can Work - Software Production\" \/>\n<meta property=\"og:description\" content=\"Ninth in a series about migrating from legacy architectures to a modern Nuxt 4 stack. The Fundamental Tension Nuxt is a file-based router. You create pages\/products\/index.vue, and the framework serves it at \/products. Routes are known at build time. The directory structure is the URL structure. Contentful \u2014 a headless CMS \u2014 is the opposite. [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/softwareproduction.eu\/?p=16\" \/>\n<meta property=\"og:site_name\" content=\"Software Production\" \/>\n<meta property=\"article:published_time\" content=\"2026-06-06T21:48:32+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-06-07T01:21:54+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/09-nuxt-and-headless-cms.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1880\" \/>\n\t<meta property=\"og:image:height\" content=\"1253\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Munir Husseini\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Munir Husseini\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"16 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=16#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=16\"},\"author\":{\"name\":\"Munir Husseini\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/person\\\/fec48f54713e1bd117640fb9b748802f\"},\"headline\":\"Nuxt and Contentful CMS \u2014 Why It&#8217;s Hard and How It Can Work\",\"datePublished\":\"2026-06-06T21:48:32+00:00\",\"dateModified\":\"2026-06-07T01:21:54+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=16\"},\"wordCount\":1986,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=16#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/09-nuxt-and-headless-cms.jpg\",\"articleSection\":[\"Advanced Web App with Nuxt and .NET\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/softwareproduction.eu\\\/?p=16#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=16\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=16\",\"name\":\"Nuxt and Contentful CMS \u2014 Why It's Hard and How It Can Work - Software Production\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=16#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=16#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/09-nuxt-and-headless-cms.jpg\",\"datePublished\":\"2026-06-06T21:48:32+00:00\",\"dateModified\":\"2026-06-07T01:21:54+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=16#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/softwareproduction.eu\\\/?p=16\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=16#primaryimage\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/09-nuxt-and-headless-cms.jpg\",\"contentUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/09-nuxt-and-headless-cms.jpg\",\"width\":1880,\"height\":1253},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=16#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/softwareproduction.eu\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Nuxt and Contentful CMS \u2014 Why It&#8217;s Hard and How It Can Work\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#website\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/\",\"name\":\"Softwareproduction\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/softwareproduction.eu\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\",\"name\":\"Munir Husseini\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/softwareproduction-logo-32.png\",\"contentUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/softwareproduction-logo-32.png\",\"width\":32,\"height\":32,\"caption\":\"Munir Husseini\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/person\\\/fec48f54713e1bd117640fb9b748802f\",\"name\":\"Munir Husseini\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"caption\":\"Munir Husseini\"},\"sameAs\":[\"https:\\\/\\\/softwareproduction.eu\\\/\"],\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/?author=1\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Nuxt and Contentful CMS \u2014 Why It's Hard and How It Can Work - Software Production","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/softwareproduction.eu\/?p=16","og_locale":"en_US","og_type":"article","og_title":"Nuxt and Contentful CMS \u2014 Why It's Hard and How It Can Work - Software Production","og_description":"Ninth in a series about migrating from legacy architectures to a modern Nuxt 4 stack. The Fundamental Tension Nuxt is a file-based router. You create pages\/products\/index.vue, and the framework serves it at \/products. Routes are known at build time. The directory structure is the URL structure. Contentful \u2014 a headless CMS \u2014 is the opposite. [&hellip;]","og_url":"https:\/\/softwareproduction.eu\/?p=16","og_site_name":"Software Production","article_published_time":"2026-06-06T21:48:32+00:00","article_modified_time":"2026-06-07T01:21:54+00:00","og_image":[{"width":1880,"height":1253,"url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/09-nuxt-and-headless-cms.jpg","type":"image\/jpeg"}],"author":"Munir Husseini","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Munir Husseini","Est. reading time":"16 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/softwareproduction.eu\/?p=16#article","isPartOf":{"@id":"https:\/\/softwareproduction.eu\/?p=16"},"author":{"name":"Munir Husseini","@id":"https:\/\/softwareproduction.eu\/#\/schema\/person\/fec48f54713e1bd117640fb9b748802f"},"headline":"Nuxt and Contentful CMS \u2014 Why It&#8217;s Hard and How It Can Work","datePublished":"2026-06-06T21:48:32+00:00","dateModified":"2026-06-07T01:21:54+00:00","mainEntityOfPage":{"@id":"https:\/\/softwareproduction.eu\/?p=16"},"wordCount":1986,"commentCount":0,"publisher":{"@id":"https:\/\/softwareproduction.eu\/#organization"},"image":{"@id":"https:\/\/softwareproduction.eu\/?p=16#primaryimage"},"thumbnailUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/09-nuxt-and-headless-cms.jpg","articleSection":["Advanced Web App with Nuxt and .NET"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/softwareproduction.eu\/?p=16#respond"]}]},{"@type":"WebPage","@id":"https:\/\/softwareproduction.eu\/?p=16","url":"https:\/\/softwareproduction.eu\/?p=16","name":"Nuxt and Contentful CMS \u2014 Why It's Hard and How It Can Work - Software Production","isPartOf":{"@id":"https:\/\/softwareproduction.eu\/#website"},"primaryImageOfPage":{"@id":"https:\/\/softwareproduction.eu\/?p=16#primaryimage"},"image":{"@id":"https:\/\/softwareproduction.eu\/?p=16#primaryimage"},"thumbnailUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/09-nuxt-and-headless-cms.jpg","datePublished":"2026-06-06T21:48:32+00:00","dateModified":"2026-06-07T01:21:54+00:00","breadcrumb":{"@id":"https:\/\/softwareproduction.eu\/?p=16#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/softwareproduction.eu\/?p=16"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/softwareproduction.eu\/?p=16#primaryimage","url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/09-nuxt-and-headless-cms.jpg","contentUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/09-nuxt-and-headless-cms.jpg","width":1880,"height":1253},{"@type":"BreadcrumbList","@id":"https:\/\/softwareproduction.eu\/?p=16#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/softwareproduction.eu\/"},{"@type":"ListItem","position":2,"name":"Nuxt and Contentful CMS \u2014 Why It&#8217;s Hard and How It Can Work"}]},{"@type":"WebSite","@id":"https:\/\/softwareproduction.eu\/#website","url":"https:\/\/softwareproduction.eu\/","name":"Softwareproduction","description":"","publisher":{"@id":"https:\/\/softwareproduction.eu\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/softwareproduction.eu\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/softwareproduction.eu\/#organization","name":"Munir Husseini","url":"https:\/\/softwareproduction.eu\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/softwareproduction.eu\/#\/schema\/logo\/image\/","url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/05\/softwareproduction-logo-32.png","contentUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/05\/softwareproduction-logo-32.png","width":32,"height":32,"caption":"Munir Husseini"},"image":{"@id":"https:\/\/softwareproduction.eu\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/softwareproduction.eu\/#\/schema\/person\/fec48f54713e1bd117640fb9b748802f","name":"Munir Husseini","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","caption":"Munir Husseini"},"sameAs":["https:\/\/softwareproduction.eu\/"],"url":"https:\/\/softwareproduction.eu\/?author=1"}]}},"jetpack_featured_media_url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/09-nuxt-and-headless-cms.jpg","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/16","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=16"}],"version-history":[{"count":12,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/16\/revisions"}],"predecessor-version":[{"id":227,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/16\/revisions\/227"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/media\/226"}],"wp:attachment":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=16"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=16"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=16"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}