Home / Site Compilation Example

Site Compilation Example

Learn About Creating a Compiled Site

Oracle Content Management (OCM) enables you to compile site pages to static HTML files. By compiling a site beforehand, you allow static HTML site pages to be rendered whenever a corresponding page URL is called, bypassing the Oracle Content Management controller to create or compile a page at runtime and improving overall performance.

This document illustrates the site compilation process using the Knowledge site template as an example. The Knowledge site template is distributed with the Oracle Content Toolkit.

Introduction to Knowledge Template

The Knowledge site template is created with Oracle Content Management. This template contains the following elements:

Template Compilation

To compile a site, you’ll need to have access to the themes and components within the site. A site template contains all the required resources to migrate and run a site. The site template is used to export the site to the Oracle Content Toolkit environment, where the site within the template can be compiled. The following sections walk through compilation of the site within the Knowledge template, which is included when you download the Oracle Content Tookit.

Install And Start Oracle Content Toolkit Development Server

For complete step-by-step process, refer to the GitHub documentation.

  1. As a prerequisite, to set up the Oracle Content Toolkit on your local machine, you’ll need to install Node.js and Node packet manager (npm). Use NodeJS version 14+.

NOTE: Make sure node and npm are in your PATH.

  1. Download Oracle Content Toolkit from GitHub.

  2. Install the dependencies and put cec on your path.

cd  <download-path>/content-and-experience-toolkit/sites
npm install
  1. Create an initial src directory in any location on your local file system to contain your CEC source example.
mkdir cec-src
cd cec-src
cec install
  1. Start the Oracle Content Toolkit development server for testing.
cec develop

The runtime test harness can be accessed from the browser using address:

http://localhost:8085.

Create a Template from the KnowledgeTemplate in the Oracle Content Toolkit

KnowledgeTemplate is available in Oracle Content Toolkit. Run the following command to create a template from KnowledgeTemplate.

cec create-template KnowledgeTemplate -f KnowledgeTemplate

Register Your Oracle Content Management Server

The following steps assume that you will create a site called Knowledge in your Oracle Content Manager server. You can upload the above KnowledgeTemplate and then create this site from the template, but before you upload the created template, you must register the Oracle Content Management server where it is being uploaded. Run the following:

cec register-server DEV -e <host-url> -u username –p password

To learn more on how to encrypt a password and create an encryption key, refer to GitHub.

To view details of your registered server, run the following command:

cec list --server DEV

Upload Your Template and Create a Site

Upload the KnowledgeTemplate to a registered Oracle Content Manegement server and create a site:

cec upload-template KnowledgeTemplate -s DEV

cec create-site Knowledge -t KnowledgeTemplate -s DEV

Compile Your Template

To compile your template, run the following command:

cec compile-template KnowledgeTemplate --verbose

The compiled page markup will be saved under the src/templates/<template>/static folder

As given compilation output, you can preview compiled pages, for example to view compiled home page access link http://localhost:8085/templates/KnowledgeTemplate

Custom Compilation

In general, if there are no custom compilers available for page layouts and components, there will be warnings in the compile-template output stating No compiler found for available page layouts and components. Therefore, custom compilers are required for the following components:

The compilation process can be enhanced with custom compilers. You can call custom compilers to compile the page layout, section layout, custom component, or content layout into the page and avoid the need for the component to be added dynamically at runtime.

Custom Page Compiler

To create a custom page compiler for the page layout, you create an .mjs file named <pageLayoutName>-compile.mjs in the theme. For example, if you have a page layout called ‘index.html’, then the corresponding page compiler for this page layout would be ‘index-compile.mjs’.

The following sample shows page layout compilers for index.html, content.html, contact.html and content-slots.html.


export default class {
    constructor () {}
    
    compile (args) {
    const SCSCompileAPI = args.SCSCompileAPI;
    let compiledPage = args.layoutMarkup;

    return new Promise((resolve, reject) => {
        // Example: use string replace on the page layout to convert it to
runtime values
        compiledPage = compiledPage.replace('opacity: 0;', 'opacity: 1;');

        // return the compiled page
        resolve(compiledPage);
    });
  }
}

Custom Component Compilers

When a site page is compiled, the page compiler will compile the page layout. To compile the components on the page, you need to implement separate component compilers for each component.

This applies to:

The component compiler does the same thing as the component render.mjs but needs to run in a node application and so won’t have access to browser objects such as “document” or “window.” They also can’t use browser specific JavaScript libraries such as JQuery.

Content Layout Compiler

Each of the content layouts (Knowledge-Banner-overview, Knowledge-Banner-detail, Knowledge-Employee-overview and Knowledge-Employee-detail) will require their own custom component compiler.

When you create a component, the default implementation of the component automatically generates the following files:

NOTE: Previously, rendering and compilation required two different sets of APIs, SCSRenderAPI and SCSCompileAPI. Now you can use SCSComponentAPI for both rendering and compilation components. For more information you can refer to public documentation.

The following sample shows the content item layout compiler for Knowledge-Banner-overview. Custom component compilers all follow the same model as page compilers. During compilation, the cec compile-template command will look for a compile.mjs which has implemented a compile() interface.

import { BaseComponent } from './baseComponent.mjs';

export default class extends BaseComponent {
    // Content Layout constructor 
    constructor(params) {
        super(params);
    }
    // override any functions that require NodeJS specific code
    addToPage(componentHTML) {
        // return the generated HTML and use hydrate in the render.js file to add any event handlers at runtime
        return Promise.resolve({
            hydrate: false, // Note: this component doesn't have any JavaScript event handlers, so there is no need to hydrate the component at runtime
            content: componentHTML
        });
    }
    // compile the component into the page
    compile() {
        // generate the HTML for the component using a Mustache template
        return this.renderComponent();
    }
}

If your component requires additional JavaScript to render, you can leverage the hydrate: true property in the object returned with the compiled content. Learn more about a hydrate function in component hydration documentation.

Compile.mjs is similar to render.mjs, with common functions defined in baseComponent.mjs. Below is the render.mjs for Knowledge-Banner-overview.

import { BaseComponent } from './baseComponent.mjs';

export default class extends BaseComponent {
    // Content Layout constructor 
    constructor(params) {
        super(params);
    }

    // override any functions that require browser specific code
    addToPage(componentHTML) {
        // insert the expanded HTML into the passed in container.
        if (componentHTML) {
            this.container.insertAdjacentHTML('beforeend', componentHTML);
        }
    }

    // render the component into thparamse page
    render(container) {
        this.container = container;

        // generate the HTML for the component using a Mustache template
        return this.renderComponent();
    }
}

Custom Component Compiler

Just like other components, the custom component compiler implements compile() interface. Below is the sample compile.mjs file for the Knowledge-NavBar component.

import { BaseComponent } from './baseComponent.mjs';

export default class extends BaseComponent {
    // Content Layout constructor 
    constructor(params) {
        super(params);
    }

    // override any functions that require NodeJS specific code
    addToPage(componentHTML) {
        // return the generated HTML and use hydrate in the render.js file to add any event handlers at runtime
        return Promise.resolve({
            hydrate: true,  // Note: this component has JavaScript event handlers, so the component needs to be hydrated.  This runtime load could be removed with inline <script> tags
            content: componentHTML
        });
    }

    // compile the component into the page
    compile({ customSettingsData }) {
        // generate the HTML for the component using the GraphQL query & Mustache template
        return this.renderComponent({
            customSettingsData: customSettingsData
        });
    }
}

Other than the default implementation functions of the component, baseComponent.mjs file also contains methods to generate the NavMenu list, which are referred by both compile.mjs and render.mjs. Below is the baseComponent.mjs sample for the Knowledge-NavBar component.

const BaseComponent = class {
    constructor({ SCSComponentAPI, componentId }) {
        this.componentAPI = SCSComponentAPI;
        this.id = componentId;

        this.componentPath = import.meta.url.replace('/baseComponent.mjs', '')
    }

    async loadResources() {
        // load in the resources in parallel
        const resources = await Promise.all([
            this.componentAPI.loadResource({
                path: this.componentPath,
                resource: 'template.mustache'
            }),
            this.componentAPI.loadResource({
                path: this.componentPath + '/styles',
                resource: 'design.css'
            }),
        ]);

        // return the resources
        return {
            template: resources[0],
            css: resources[1]
        };
    }

    createTemplateModel({ css, overrides }) {
        // get the componentAPI mustache macros
        const componentMacros = this.componentAPI.getMustacheTags();

        // get the customSettingsData either passed in as an override or stored against the component
        const customData = overrides.customSettingsData || {};

        // create the navigation menu model
        const siteInfo = this.componentAPI.getSiteInfo();
        const navMenu = new NavigationMenu({
            SCSComponentAPI: this.componentAPI,
            structureMap: siteInfo.structureMap,
            rootId: siteInfo.navigationRoot,
            currentNodeId: siteInfo.currentPageId
        });
        const menuModel = {
            navMenuList: navMenu.createNavMenu(),
            navWidth: customData.navWidth || '50%',
            navAlign: customData.nls && customData.nls.navAlign || 'inherit',
        };

        // merge componentAPI macros with the navgiation menu results
        const model = { ...componentMacros, ...{ css: css }, ...menuModel };

        return model;
    }

    addToPage() {
        throw new Error('addTopage method must be overridden');
    }

    // Note: parameters may be passed in as overrides due to events at runtime
    async renderComponent({ customSettingsData, componentLayout, imageWidth } = {}) {
        let renderedHTML = '';

        // load in the resources
        const { template, css } = await this.loadResources();

        // create the model from the query results and resources to be used by the mustache template
        const model = this.createTemplateModel({
            css: css,
            overrides: {
                customSettingsData: customSettingsData,
                componentLayout: componentLayout,
                imageWidth: imageWidth
            }
        });

        // render the mustache template applying the model
        try {
            renderedHTML = this.componentAPI.getMustache().render(template, model);
        } catch (e) {
            console.log('renderComponent: failed to expand Mustache template', e);
        }

        // add the renderHTML to the page
        return this.addToPage(renderedHTML);
    }
};

const NavigationMenu = class {
    constructor({ SCSComponentAPI, structureMap, rootId, currentNodeId }) {
        this.componentAPI = SCSComponentAPI;
        this.structureMap = structureMap;
        this.rootId = rootId;
        this.currentNodeId = currentNodeId;

        // initialize the navigation menu
        this.navMenu = [];
    }

    createNavMenu() {
        const structureMap = this.structureMap,
            rootId = this.rootId;

        // start with Home link...
        this.createNavItem(rootId);

        // top level pages...
        (structureMap[rootId].children || []).forEach(nodeId => {
            // skip hidden node...
            if (!this.isNodeHidden(nodeId)) {
                if (!this.allNodeChildrenAreHidden(nodeId)) {
                    // add top level node with childeren...
                    this.createSubNavItem(nodeId);
                } else {
                    // add top level node without childeren...
                    this.createNavItem(nodeId);
                }
            }
        });

        return this.navMenu;
    }

    createNavItem(id) {
        var linkData = this.componentAPI.createPageLink(id) || {};

        this.navMenu.push({
            name: this.structureMap[id].name,
            target: linkData.target,
            href: linkData.href
        });
    }

    createSubNavItem(id) {
        const linkData = this.componentAPI.createPageLink(id) || {},
            menuItem = this.structureMap[id],
            submenus = [],
            subNodes = menuItem.children || [];

        subNodes.forEach((childId) => {
            if (!this.isNodeHidden(childId)) {
                // add item in the sub-level navigation menu...
                const childlinkData = this.componentAPI.createPageLink(childId) || {};
                submenus.push({
                    name: this.structureMap[childId].name,
                    target: childlinkData.target,
                    href: childlinkData.href
                });
            }
        });

        // add in this node
        this.navMenu.push({
            name: menuItem.name,
            target: linkData.target,
            href: linkData.href,
            hasSubmenu: submenus.length > 0,
            submenus
        });
    }

    allNodeChildrenAreHidden(id) {
        const subnodes = (this.structureMap[id].children || []);

        const visibleNode = subnodes.find(subNodeId => {
            return !this.isNodeHidden(subNodeId);
        });

        return typeof visibleNode !== 'undefined';
    }

    isNodeHidden(id) {
        const navNode = (this.structureMap[id] || {});

        return (navNode.hideInNavigation === true);
    }
};

export { BaseComponent };

Detail Page Compilation

During the detail page compilation, the compiler checks the custom compiler of the content item, which is rendered on the page using the Content Placeholder component. If there’s no custom compiler for the corresponding content layout on the detail page, then the detail page won’t be created for the detail page URL, and the page will render dynamically at runtime. For detail page compilation, a separate static HTML file is created for each detail page URL.

In the Knowledge template, there is a list of banner articles on the home page that displays a summary view for each item. To get more details about a banner article, for example, page visitors click on READ MORE to drill down to a Banner-Detail page, which displays the whole article.

On a detail page, a Content Placeholder component is set for Knowledge-Banner-detail, for which a custom compiler exists in the asset folder as mentioned in the content layout compiler section.

The custom compiler implementation for the Knowledge-Banner-detail is the same as other components’ custom compilers. Knowledge-Banner-detail (Knowledge-Banner Type) uses graph.gql to fetch author details (Knowledge-Employee-Detail Type). To pass the details of author, a referenced field, to the Mustache template, you can access author data using a graphQL query defined under graph.gql. Below is the graph.gql. sample for Knowledge-Banner-detail. The query results are further passed to loadResource() defined in baseComponent.mjs.

query ($channelToken: String, $id: ID) {
    contentItemData:getItem(channelToken: $channelToken, id: $id) {
      ...knowledgeBannerData
    }
  }
  
  fragment knowledgeBannerData on knowledgeBanner {
    ...itemData
    fields {
      bannerImage {
        ...imageData
      }
          title
          bannerDate
      bannerDescription
      author {
        ...authorData
      }
    }
  }
  
  fragment authorData on knowledgeEmployeeDetail {
    ...itemData
    fields {
      roledescription
      employeename
      profilepicture {
        ...imageData
      }
    }
  }
  
  fragment itemData on item {
    id
    name
  }
  
  fragment imageData on image {
    ...itemData
    fields {
      renditions {
        name
        format
      }
    }
  }

Below is a snippet from baseComponent.mjs that loads graphQL query results in a Mustache Template.

async loadResources() {
        // load in the resources in parallel
        const resources = await Promise.all([
            this.componentAPI.loadResource({
                path: this.componentPath,
                resource: 'template.mustache'
            }),
            this.componentAPI.loadResource({
                path: this.componentPath,
                resource: 'design.css'
            }),
            this.componentAPI.loadResource({
                path: this.componentPath,
                resource: 'query.gql'
            })
        ]);

        // return the resources
        return {
            template: resources[0],
            css: resources[1],
            queryGQL: resources[2]
        };
    }

    async queryGraphQL(queryGQL) {
        const contentClient = this.componentAPI.getContentClient();
        let queryResult;

        // run the graphQL query passing in any variables
        try {
            queryResult = await contentClient.graphql({
                query: queryGQL,
                variables: {
                    channelToken: contentClient.getInfo().channelToken,
                    id: this.contentItemData.id
                }
            });
        } catch (e) {
            console.log('queryGraphQL: failed', e);
        }

        return queryResult;
    }

Upload Compiled Site Pages

Upload the compiled static files into the static folder under your site folder hosted on your Oracle Content Management server.

cec upload-static-site-files src/templates/Knowledge-Compile-Demo/static --site Knowledge --server DEV

To revert to non-compiled behavior, you need to remove the static files that you uploaded into the site.

cec delete-static-site-files Knowledge --server DEV

Publishing Files Using the Oracle Content Management Web Interface

When you publish the site, any files in the static folder will be automatically included.

Out-of-the-Box Site Compilation Server

Using Oracle Content Toolkit is best for compiling your site and pushing the changes back to the server for testing and development. After you have validated that the site and components compile successfully and produce the desired the results, you can use Oracle Content Management’s built-in site compilation server to automatically compile when you publish a site.

To enable site compilation on publish:

  1. Select a site on the Sites page and select Properties from the action bar or context menu to open the properties panel. You must have the manager role for the site.
  2. Select the Static Delivery tab.
  3. Enable Compile site after publish.
  4. Optionally, add a custom compile_site.sh file to optimize how this site is compiled by downloading the default file from the Static Delivery tab, modifying it, then uploading the modified file.

Static Delivery tab.

After you publish the site, the compilation job will be submitted to the server. You can track its progress and status from Static Delivery tab. Once compilation has completed or failed, you can download the log of the compilation process from the properties panel.

Compilation status.

References