Builder.io content integration on a PHP page using Vue.js CDN mode

Builder public api key
9c477fefb92c46b68a5bd05ea08879e8

What are you trying to accomplish
I’m trying to integrate a builder.io section on a PHP page using Vue.js 3

Code stack you are integrating Builder with
LAMP + Vue.js

Problem description
I’m trying to integrate builder.io sections into an existing PHP website. The current (quite large) website codebase can’t be migrated to Vue.js/Nuxt in the near future due to limited available implementation time. To modernize the PHP pages in smaller steps, I introduced Vue.js, which works together fine inside PHP pages. Now I want to use the builder.io headless CMS in my PHP pages. In the PHP page, I’m already using Vue.js 3 (in CDN mode, using a script tag, no build step possible in my case). And now I’d like to use a builder.io section model inside the <div id="app"> part of the page (the part of the html template controlled by Vue.js).

I first tried using webcomponents integration mode, but Vue.js doesn’t like a <builder-component> in it’s template, I get warnings in the browser console I can’t insert custom tags:

<div id="app">
  ...
  <builder-component model="homepage-section" api-key="9c477fefb92c46b68a5bd05ea08879e8">
    Builder.io section is loaded here ...
  </builder-component>
  <script async src="https://cdn.builder.io/js/webcomponents"></script>
  ...
</div>

In console, I get:

[Vue warn]: Template compilation error: Tags with side effect (<script> and <style>) are ignored in client component templates.
91 |          Builder.io section is loaded here ...
92 |        </builder-component>

Next, I tried using the builder.io sdk-vue integration using ESM module mode (the same way as with Vue.js 3 using it’s vue.esm-browser.js build). To make this work, I’d need an ESM browser build of the builder.io sdk-vue package. I tried using the current browser build from the lib/browser dir from the npm package:

# npm install @builder.io/sdk-vue

Next, I copy the lib/browser dir contents to my assets dir /assets/@builder.io/sdk-vue-2.0 to make the integration code available in my PHP page homepage-builder.php:

  <div id="app">
      ...
      <Content
        v-if="canShowContent"
        :model="model"
        :content="content"
        :api-key="apiKey"
      ></Content>
      <div v-else>Builder content loading</div>
  </div>
  <script type="importmap">
    {
      "imports": {
        "vue": "/assets/vuejs-3.4.31/vue.esm-browser.js",
        "@builder.io/sdk-vue": "/assets/@builder.io/sdk-vue-2.0/index.mjs"
      }
    }
  </script>
  <script type="module">
    import { createApp, ref, onMounted } from 'vue'
    import { Content, fetchOneEntry, isPreviewing } from '@builder.io/sdk-vue'

    const app = createApp({
      setup() {
        const mainBanners = ref(<?= json_encode($data['mainBanners']) ?>)
        const pageReady = ref(false)
        const content = ref(null)
        // Add your Public API Key below
        const apiKey = '9c477fefb92c46b68a5bd05ea08879e8'
        const canShowContent = ref(false)
        const model = 'homepage-section'

        onMounted(async () => {
          content.value = await fetchOneEntry({
            model,
            apiKey,
            userAttributes: {
              urlPath: window.location.pathname,
            },
          })
          canShowContent.value = content.value ? true : isPreviewing();
          pageReady.value = true
        })

        return {
          mainBanners,
          pageReady,
          content,
          apiKey,
          canShowContent,
          model
        }
      },
    })
    app.component('Content', Content)
    app.mount('#app')
  </script>

But, as expected, this doesn’t work because the lib/browser dir files are not bundled for use inside a browser. I tried to use the index.mjs file as ESM module, but this obviously doesn’t work.

Btw, without the builder.io sdk-vue integration in the PHP page, Vue.js 3 works perfectly fine using it’s esm-browser bundle: PHP fills the initial Vue data server-side and Vue next uses it client-side.

Has someone an idea if there is a browser ESM bundle available for the builder-io sdk-vue integration module or how I can create an ESM bundle file? Or am I overlooking something obvious?

Btw, I also looked at the HTML api integration, but this option has it’s limitations too: Custom components don’t work with HTML api.

Thanks in advance for ideas/suggestions!

Hello @wdbacker,

Integrating Builder.io seamlessly into an existing PHP project with Vue.js using a CDN setup does require some thoughtful workarounds, especially since standard build steps are not possible. Considering your constraints, you have a couple of paths to explore:

  1. Using the Web Components API and addressing Vue.js warnings:
    Since Vue.js doesn’t natively support custom tags via template and throws warnings, we can dynamically create the builder-component using Vue’s render function or a method that doesn’t touch the template compilation. This helps avoid compile-time errors.

  2. Leveraging the SDK for Vue with a custom ESM build:
    While Builder.io doesn’t seem to provide a pre-built ESM module for browsers out of the box, you can bundle your own using a tool like Rollup or Webpack. However, this approach is more complex and typically needs a build step, which you’ve mentioned isn’t feasible.

Here’s how you can dynamically create the builder-component avoiding Vue’s template compilation issues:

Solution using Web Components:

Modify your Vue component to dynamically insert the builder-component:

<div id="app">
  <!-- Other Vue components -->
  <div id="builder-section"></div>
  <script async src="https://cdn.builder.io/js/webcomponents"></script>
</div>

<script type="module">
  import { createApp, onMounted } from '/assets/vuejs-3.4.31/vue.esm-browser.js';

  const app = createApp({
    setup() {
      onMounted(() => {
        const builderComponent = document.createElement('builder-component');
        builderComponent.setAttribute('model', 'homepage-section');
        builderComponent.setAttribute('api-key', '9c477fefb92c46b68a5bd05ea08879e8');
        document.getElementById('builder-section').appendChild(builderComponent);
      });
    },
  });

  app.mount('#app');
</script>

By appending the builder-component dynamically, Vue.js won’t complain as the tag is handled outside of Vue’s template compilation.

Solution using SDK for Vue with Custom ESM Build:

Here’s a simplified setup using Rollup to create an ESM bundle for @builder.io/sdk-vue:

  1. Setup Rollup:
    a. Ensure you have Node.js installed.
    b. Create a rollup.config.mjs file:
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { terser } from 'rollup-plugin-terser';

export default {
  input: 'node_modules/@builder.io/sdk-vue/index.mjs',
  output: {
    file: 'public/assets/@builder.io/sdk-vue-2.0/bundle.js',
    format: 'esm',
  },
  plugins: [
    resolve(),
    commonjs(),
    terser(),
  ],
};

c. In your project root, create a package.json and add development dependencies:

{
  "devDependencies": {
    "@rollup/plugin-commonjs": "^21.0.0",
    "@rollup/plugin-node-resolve": "^13.0.6",
    "rollup": "^2.58.0",
    "rollup-plugin-terser": "^7.0.2"
  }
}

d. Run the following commands to install dependencies and build the bundle:

npm install
npx rollup --config rollup.config.mjs
  1. Use the created bundle in your PHP page:
<div id="app">
  <!-- Vue app and Builder content -->
  <div v-if="canShowContent">
    <Content :model="model" :content="content" :api-key="apiKey" />
  </div>
  <div v-else>Builder content loading</div>
  <script type="importmap">
    {
      "imports": {
        "vue": "/assets/vuejs-3.4.31/vue.esm-browser.js",
        "@builder.io/sdk-vue": "/assets/@builder.io/sdk-vue-2.0/bundle.js"
      }
    }
  </script>
  <script type="module">
    import { createApp, ref, onMounted } from 'vue';
    import { Content, fetchOneEntry, isPreviewing } from '@builder.io/sdk-vue';

    const app = createApp({
      setup() {
        const mainBanners = ref(<?= json_encode($data['mainBanners']) ?>);
        const pageReady = ref(false);
        const content = ref(null);
        const apiKey = '9c477fefb92c46b68a5bd05ea08879e8';
        const canShowContent = ref(false);
        const model = 'homepage-section';

        onMounted(async () => {
          content.value = await fetchOneEntry({
            model,
            apiKey,
            userAttributes: {
              urlPath: window.location.pathname,
            },
          });
          canShowContent.value = content.value ? true : isPreviewing();
          pageReady.value = true;
        });

        return {
          mainBanners,
          pageReady,
          content,
          apiKey,
          canShowContent,
          model
        };
      },
    });

    app.component('Content', Content);
    app.mount('#app');
  </script>
</div>

Both methods should fit your constraints, allowing a gradual integration of Builder.io into your PHP and Vue.js application without a build step for Vue.js.

1 Like

Hi @manish-sharma, thank you very much for both excellent examples! You set me on the right track.

The best solution (I think) is to go with the Vue.js integration: I used your suggested build steps to create a browser bundle. With a few small adjustements, I got the Vue.js integration working on a PHP page (and you could use this technique with a lot of “classic” SSR environments like PHP, ASP, … where probably a lot of website still run on).

Btw, another attempt yesterday before you posted your reply with a ready-to-use ESM build of the @builder.io/sdk-vue from https://www.jsdelivr.com/package/npm/@builder.io/sdk-vue by changing the Vue import code line to:

import { Content, fetchOneEntry, isPreviewing } from 'https://esm.run/@builder.io/sdk-vue'

didn’t work: no browser errors but no content displayed unfortunately.

Today following your integration steps, I got a working setup to include a Builder.io section in an existing PHP page:

  • create an empty directory builder.io to create an ESM build of the @builder.io/sdk-vue package

  • create a package.json file containing:

    {
      "dependencies": {
        "@builder.io/sdk-vue": "^2.0.0"
      },
      "devDependencies": {
        "@rollup/plugin-commonjs": "^21.0.0",
        "@rollup/plugin-node-resolve": "^13.0.6",
        "rollup": "^2.58.0",
        "rollup-plugin-terser": "^7.0.2",
        "rollup-plugin-import-css": "^3.5.0"
      }
    }
    

    (you’ll notice I need to add the rollup-plugin-import-css module to build the style.css file correctly)

  • create a rollup.config.mjs file containing:

    import resolve from '@rollup/plugin-node-resolve';
    import commonjs from '@rollup/plugin-commonjs';
    import { terser } from 'rollup-plugin-terser';
    import css from "rollup-plugin-import-css";
    
    export default {
      input: 'node_modules/@builder.io/sdk-vue/lib/browser/index.mjs',
      output: {
        dir: 'dist',
        format: 'esm',
      },
      external: [
    	'vue'
      ],
      plugins: [
    	css({
    		output: 'style.css'
    	}),
        resolve(),
        commonjs(),
        terser(),
      ],
    };
    

    Here I added the css plugin too and changed the output file to a output dir dist for the css file. I also made the vue library external (avoids the peer dependency Vue to be included too in the Builder.io Vue sdk bundle - you’ll normally include vue in your PHP page yourself)

  • Now you can build a browser version of the Builder.io Vue sdk by issuing:

    # npm install
    # npx rollup --config rollup.config.mjs
    
  • Next, in the dist folder, you’ll find a bundle set ready for use in a browser:

    block-styles-cVSrkWog-37b72cf4.js
    block-wrapper-maIbj8Gi-695cbb5e.js
    component-ref-1N_Kijok-0a4c0b70.js
    get-block-properties-AuDiFohY-ec045ce9.js
    index-pDvo-fub-6104b510.js
    index.js
    repeated-block-nP-Etgd7-9f7c99a5.js
    style.css
    

    the index.js file is the main file to use in the import statement in you HTML file

  • In your PHP file, you can now use this bundle like this:

    ...
    <?php
      // prepare some SSR data by PHP for use by Vue
      $banners = getBanners(); 
      $data = [
        'mainBanners' => $banners['mainBanners']
      ];
    ?>
    <html>
      <head>
        <!-- include Builder.io's styles -->
        <link rel="stylesheet" type="text/css" href="/assets/@builder.io/sdk-vue-2.0/style.css" />
      </head>
      ...
      <body>
        ...
        <div class="row mx-3">
          <!-- include Builder.io's Content component -->
          <Content
            v-if="canShowContent"
            :model="model"
            :content="content"
            :api-key="apiKey"
          ></Content>
          <div v-else>Builder.io content loading (or missing)</div>
        </div>
        ...
        <script type="importmap">
          {
            "imports": {
              "vue": "/assets/vuejs-3.4.31/vue.esm-browser.js",
              "@builder.io/sdk-vue": "/assets/@builder.io/sdk-vue-2.0/index.js"
            }
          }
        </script>
        <script type="module">
          import { createApp, ref, onMounted } from 'vue'    
          import { Content, fetchOneEntry, isPreviewing } from '@builder.io/sdk-vue'
    
          const app = createApp({
            setup() {
              const mainBanners = ref(<?= json_encode($data['mainBanners']) ?>)
              const pageReady = ref(false)
              const content = ref(null)
              // Add your Public API Key below
              const apiKey = '9c477fefb92c46b68a5bd05ea08879e8'
              const canShowContent = ref(false)
              const model = 'homepage-section'
    
              onMounted(async () => {
                content.value = await fetchOneEntry({
                  model,
                  apiKey,
                  userAttributes: {
                    urlPath: window.location.pathname,
                  }
                })
                console.log('content.value: ', content.value)
                canShowContent.value = content.value ? true : isPreviewing();
              })
    
              return {
                mainBanners,
                content,
                apiKey,
                canShowContent,
                model
              }
            },
          })
          // define the Content component to make it available in the Vue template
          app.component('Content', Content)
          app.mount('#app')
        </script>
      </body>
    </html>
    

    You’ll notice you can use the code from the Builder.io examples with only very small changes. I just needed to register the Content component globally to allow Vue to recognize it.

    Context assumed here: at the root of your PHP website, you need an /assets folder containing a folder for the Vue framework (in /assets/vue-3.4.31/) and the Builder.io Vue sdk (/assets/@builder.io/sdk-vue-2.0/). Using the script importmap, the Vue ESM browser build vue.esm-browser.js and the Builder.io Vue sdk browser bundle extrypoint file index.js are mapped for use in the module script below it.

Following these steps, you can use Builder.io content in any (even static) html page using Vue.js CDN mode (using script tag). Because in a lot of (larger) existing websites still using e.g. PHP for SSR, you can’t use newer frameworks like Vue immediately which normally use setups with a build step. When you want to gradually upgrade your PHP website, this is a possible solution which allows code reuse when switching your website to Nuxt in the future.

1 Like