Giter VIP home page Giter VIP logo

kontent-ai / smart-link Goto Github PK

View Code? Open in Web Editor NEW
9.0 17.0 4.0 30.79 MB

Kontent.ai Smart Link SDK can be used to automatically inject smart links to Kontent.ai according to manually specified HTML data attributes on your website. It also lets you connect your website with Web Spotlight (for faster editing and preview of your content).

License: MIT License

JavaScript 1.96% TypeScript 97.77% HTML 0.28%
web-spotlight kontent-ai kontent-sdk kontent-ai-tool

smart-link's Introduction

Kontent.ai Smart Link SDK

licence npm downloads jsdelivr

Kontent.ai Smart Link SDK can be used to automatically inject smart links to Kontent.ai according to manually specified HTML data attributes on your website. It also lets you connect your website with Web Spotlight for faster editing and preview of your content.

⚠️ Kontent.ai Smart Link SDK is a browser-only SDK, which means that the Node.js environment is not currently supported. Make sure to always initialize the Smart Link SDK in a browser context.

Installation

You can install this library using npm or using global CDNs such as jsdelivr.

npm

npm i @kontent-ai/smart-link

UMD Bundles

When using the UMD bundle and including this library inside the script tag of your HTML page, you can then find an SDK instance under the KontentSmartLink global variable. JS bundle and its minified version are distributed in dist folder.

  • kontent-smart-link.umd.min.js
  • kontent-smart-link.umd.js

CDN

kontent-smart-link.umd.js

Gzip browser bundle

https://cdn.jsdelivr.net/npm/@kontent-ai/smart-link@latest/dist/kontent-smart-link.umd.js
kontent-smart-link.umd.min.js

Gzip browser bundle

https://cdn.jsdelivr.net/npm/@kontent-ai/smart-link@latest/dist/kontent-smart-link.umd.min.js

Usage

Kontent.ai Smart Link SDK parses manually specified HTML data attributes on your webpage and automatically injects smart links to Kontent.ai. Injecting smart links to Kontent.ai means that all elements marked with special data attributes will become interactive (handle clicks/redirect to Kontent.ai/navigate from the preview in Web Spotlight/etc.). The type of injected smart link depends on used data attributes, their hierarchy, and context (Web Spotlight).

Data attributes

Kontent.ai Smart Link SDK highly depends on a set of manually specified data attributes in your HTML markup. That is why it won't work properly without those attributes. The SDK won't add the data attributes to your HTML, you must add them yourself so that SDK will then be able to use them as a source of data (e.g. Kontent.ai project ID, element code name, etc.) when injecting the smart links.

Available data attributes

Attribute Value Description
data-kontent-project-id guid Kontent.ai project/environment ID.
data-kontent-language-codename string Kontent.ai language codename.
data-kontent-item-id guid Content item ID.
data-kontent-component-id guid Content component ID.
data-kontent-element-codename string Content type element codename.
data-kontent-add-button - Specifies that node should have add-button rendered near it.
data-kontent-add-button-insert-position start | before | after | end Specifies the insert position of an item/content component added using add button.
data-kontent-add-button-render-position bottom-start | bottom | bottom-end | left-start | left | left-end | top-start | top | top-end | right-start | right | right-end Specifies visual location of add button.
data-kontent-disable-features highlight Specifies that the selected node should not have highlight (which includes edit buttons). Useful when there are too many smart links on your page.

Data attributes hierarchy

Although it is possible to put all previously specified data attributes on the same DOM node, you don't have to do it. We recommend you set data attributes hierarchically so that you don't have to duplicate the same attributes.

For example, your webpage probably represents one specific project in Kontent.ai, which means that you can place data-kontent-project-id attribute on your <body> element or another wrapping DOM node so that all descendant nodes inherit this project ID. The same could be true for a language code name. If your page uses only 1 language variant at a time, you could place your data-kontent-language-codename attribute next to your data-kontent-project-id from a previous step. But remember, that since language variant is relevant to some specific project, data-kontent-language-codename attribute should always be on the same element as data-kontent-project-id attribute or on some of its descendants. After that, you can find all DOM nodes that represent Kontent.ai items and place data-kontent-item-id attribute on them. Then inside those nodes, you can find all descendants that represent some element of the Kontent.ai item and put data-kontent-element-codename attribute on them. In the case of Rich Text elements and Linked Items elements, there could be other content items or content components inside them, which have their own elements and so on.

Content components

Content component is single-use content, that is also sometimes referred to as one-off, channel-specific, or non-reusable. Content components exist only within a specific rich text element in your content items and become their integral part. This means you won't find components in your list of items in Content Inventory in Kontent.ai.

You should use data-kontent-component-id attribute to specify that something represents a content component in your HTML so that the SDK knows that this item has no separate page in the Kontent.ai and must be opened in the context of its parent content item.

Smart link types

Currently, there are 4 types of smart links supported by Kontent.ai Smart Link SDK. All of them require certain data attributes to be specified in HTML markup of your webpage. Please note, that most of those smart link types are only available and visible inside Web Spotlight preview iframe.

Edit element button

Edit element button allows you to edit a specific element of a content item by clicking on it in preview. Inside Web Spotlight, this will lead to In-Context editor being opened, and the selected element will be scrolled into view. Outside Web Spotlight, you will be redirected to Kontent.ai item editor.

Data attributes: data-kontent-project-iddata-kontent-language-codenamedata-kontent-item-iddata-kontent-component-id?data-kontent-element-codename.

Environment: This feature is available both inside and outside Web Spotlight.

Edit content component button

Edit content component button allows you to edit a specific content component by clicking on it in preview. This will lead to In-Context editor being opened, and the selected content component scrolled into view.

Data attributes: data-kontent-project-iddata-kontent-language-codenamedata-kontent-item-iddata-kontent-component-id.

Environment: This feature is only available inside Web Spotlight.

Edit content item button

Edit content item button allows you to edit a specific content item by clicking on it in preview. This will lead to In-Context editor being opened.

Data attributes: data-kontent-project-iddata-kontent-language-codenamedata-kontent-item-id.

Environment: This feature is only available inside Web Spotlight.

Add button

Add button allows you to add content to your page right from your preview. It supports both Linked items elements and Rich Text elements.

Environment: This feature is only available inside Web Spotlight.

Fixed add button

Data attributes: data-kontent-project-iddata-kontent-language-codenamedata-kontent-item-iddata-kontent-component-id?data-kontent-element-codename(RTE or LIE) → data-kontent-add-button & data-kontent-add-button-render-position? & data-kontent-add-button-insert-position=start|end .

Relative add button

Relative add button allows you to add content relatively to some existing content in your Rich Text element or Linked item element. For example, you can insert a new content component after or before the existing content component in RTE. To turn add button into a relative add button, you need to set insert position to before or after and provide target id on the same node using data-kontent-item-id or data-kontent-component-id attribute.

Data attributes: data-kontent-project-iddata-kontent-language-codenamedata-kontent-item-iddata-kontent-component-id?data-kontent-element-codename(RTE or LIE) → data-kontent-item-id|data-kontent-component-id(target item) & data-kontent-add-button & data-kontent-add-button-render-position? & data-kontent-add-button-insert-position=before|after.

SDK Initialization

After all data attributes have been set, you can initialize Kontent.ai Smart Link SDK on your website. You can use initialize or initializeOnLoad method in order to do it. Both of the previously mentioned methods return an instance of initialized SDK (initializeOnLoad returns a Promise resolving to an instance). The main difference between the two methods is that the initializedOnLoad method will wait for the page to load before initializing the SDK. This can be useful when you want to initialize the SDK in the head section of your webpage when the page has not been fully loaded yet.

Kontent.ai Smart Link SDK uses multiple event listeners, timeouts, observers to track the position of the relevant elements, so please always call .destroy() method to dispose all of those side effects before trying to initialize the SDK again (e.g. inside useEffect cleanup function) to avoid memory leaks.

Configuration

Both initialization methods take an optional configuration argument, that you can use to configure the SDK. You can also use instance setConfiguration method to update configuration of initialized SDK.

Attribute Default Description
debug false When it's set to true, enables all debug logs. Can be useful to get more information about how the SDK works inside, but can affect performance.
defaultDataAttributes { projectId: undefined, languageCodename: undefined } Default values for data attributes, which are only used when those data attributes are not found in DOM during data attributes parsing process. For now, only projectId and languageCodename attributes are supported.
queryParam ksl-enabled Name of the query parameter that must be present in the URL to turn the smart link injection on. It is not necessary for query parameter to have a truthy value (just the presence of this query parameter is checked). If set to falsy value ('', null), the smart link injection will always be enabled. Query parameter is only used outside Web Spotlight.

Customization

The following custom CSS properties can be used to customize the visuals of the SDK output.

Custom property Default Description
--ksl-color-background-default rgba(255, 255, 255, 1) Default background color used in toolbar and popover.
--ksl-color-background-default-disabled rgba(223, 223, 223, 1) Disabled background color for buttons inside toolbar and popover.
--ksl-color-background-default-hover rgba(21, 21, 21, 0.1) Hover background color for buttons inside toolbar and popover.
--ksl-color-background-default-selected rgba(255, 240, 239, 1) Selected background color for buttons inside toolbar and popover.
--ksl-color-background-secondary rgba(20, 22, 25, 1) Secondary background color used in tooltips.
--ksl-color-primary rgba(219, 60, 0, 1) Primary color used as a hover border color in highlights and as a background color in add buttons.
--ksl-color-primary-hover rgba(149, 48, 0, 1) Primary color used as a hover background color in add buttons.
--ksl-color-primary-transparent rgba(219, 60, 0, 0.5) Primary color with transparency used as a default border color in highlights.
--ksl-color-text-default rgba(255, 255, 255, 1) Text color used on a default background (buttons inside toolbar and popover).
--ksl-color-text-default-disabled rgba(140, 140, 140, 1) Disabled text color used on a default background.
--ksl-color-text-secondary rgba(21, 21, 21, 1) Text color used inside tooltips and add buttons.
--ksl-shadow-default 0 8px 32px rgba(16, 33, 60, 0.24), 0 0 8px rgba(0, 0, 0, 0.03) Default shadow for toolbar.
--ksl-shadow-primary 0 8px 10px rgba(219, 60, 0, 0.2), 0 6px 20px rgba(219, 60, 0, 0.12), 0 8px 14px rgba(219, 60, 0, 0.14) Shadow for add buttons.
--ksl-z-index 9000 Base value of z-index used for calculation of individual values for each ksl-element type

For example, if you want to override all SDK colors and shadows for all SDK elements on the page, you can do it by changing the values of all available custom properties of a :root element in your CSS or inside a new <style> tag on your page.

:root {
    --ksl-color-background-default: rgba(4, 102, 200, 1);
    --ksl-color-background-default-disabled: rgba(2, 62, 125, 1);
    --ksl-color-background-default-hover: rgba(0, 40, 85, 0.1);
    --ksl-color-background-secondary: rgba(2, 62, 125, 1);
    --ksl-color-background-default-selected: rgba(3, 83, 164, .1);
    --ksl-color-primary: rgba(4, 102, 200, 1);
    --ksl-color-primary-transparent: rgba(4, 102, 200, 0.5);
    --ksl-color-primary-hover: rgba(2, 62, 125, 1);
    --ksl-color-text-default: rgba(255, 255, 255, 1);
    --ksl-color-text-default-disabled: rgba(51, 65, 92, 1);
    --ksl-color-text-secondary: rgba(255, 255, 255, 1);
    --ksl-shadow-default: 0 8px 32px rgba(0, 24, 69, 0.24), 0 0 8px rgba(0, 0, 0, 0.03);
    --ksl-shadow-primary: 0 8px 10px rgba(4, 102, 200, 0.2), 0 6px 20px rgba(4, 102, 200, 0.12), 0 8px 14px rgba(4, 102, 200, 0.14);
    --ksl-z-index: 9000;
}

Preview autorefresh in Web Spotlight

When working with the in-context editor in Web Spotlight, it is good to keep the content of your preview fresh without having to refresh it manually after every change. Starting from version 2.2.0, the Smart Link SDK supports the preview autorefresh feature in Web Spotlight.

For your web apps to support the preview autorefresh feature, make sure that your preview environment:

  1. Uses the latest version of the Smart Link SDK.
  2. Has the X-KC-Wait-For-Loading-New-Content header set to true when fetching data from Delivery Preview API.

If both previously mentioned conditions are met, Web Spotlight will wait for your changes to be ready via the Delivery Preview API, and after that, the preview will be refreshed automatically.

Implementing custom refresh handler

In some cases, simply refreshing the page might not be enough. For example, if you use a static site generator for your preview, you need to trigger the rebuild of the page before refreshing it. Or maybe you don't want to refresh the entire page when a single item has been updated and would rather re-render only the affected place in the UI. In some cases, simply refreshing the preview page after the change in Web Spotlight is not enough.

That's why the Smart Link SDK supports the custom refresh handler, which allows you to specify how your web page reacts to refresh events received from Web Spotlight. If you register a custom refresh handler, it will be called instead of a default handler every time when refresh is triggered in Web Spotlight (both manually and automatically).

You can implement a custom refresh handler using the .on(KontentSmartLinkEvent.Refresh, (data, metadata, originalRefresh) => {}) method on the SDK instance:

import KontentSmartLink, { KontentSmartLinkEvent } from '@kontent-ai/smart-link';

const sdk = KontentSmartLink.initialize({ ... });

sdk.on(KontentSmartLinkEvent.Refresh, (data, metadata, originalRefresh) => {
  // your custom refresh logic
});

A custom refresh handler takes three arguments:

Argument Type Description
Data { projectId: string, languageCodename: string, updatedItemCodename: string } | undefined Information about updated item, that caused autorefresh. It is only available when refresh is triggered automatically.
Metadata { manualRefresh: boolean } Manual refresh is set to true when the refresh is triggered by user.
Original refresh () => void Default refresh handler.

You can then unregister the custom refresh handler using the .off method on the SDK instance.

Examples
Re-fetching item data without fully refreshing in React

It is possible to only update the affected place of the UI by re-fetching page data instead of refreshing the whole page. The following code sample shows how this could be implemented in a React application.

import KontentSmartLink, { KontentSmartLinkEvent } from '@kontent-ai/smart-link';

const PageContent: React.FC = () => {
  const [data, setData] = useState(null);
  const fetchData = useCallback((projectId, languageCodename, itemCodename) => {...}, []);

  useEffect(() => {
    const sdk = KontentSmartLink.initialize();

    // register custom refresh handler to avoid full refresh in some cases
    sdk.on(KontentSmartLinkEvent.Refresh, (data: IRefreshMessageData, metadata: IRefreshMessageMetadata, originalRefresh: () => void) => {
      // if user triggered the refresh manually, just refresh the page
      if (metadata.manualRefresh) {
        originalRefresh();
      } else {
        // refetch data for the updated item, instead of refreshing the whole page
        const { projectId, languageCodename, updatedItemCodename } = data;
        fetchData(projectId, languageCodename, updatedItemCodename);
      }
    });

    return () => {
      sdk.destroy();
    };
  }, [fetchData]);

  return (...);
};
Sending request to rebuild the SSG page before refreshing it (Netlify + Gatsby)

When using static site generators, you have to rebuild your website in order to apply your changes. The following example shows how this could be done using a custom refresh handler. In this example, we used Gatsby deployed to Netlify, but the solution for other SSG frameworks should be similar.

The deploy-status function is a custom Netlify function used to get the status of the last deploy, so that we can check if the deployment is finished and the page can be refreshed.

// ./.netlify/functions/deploy-status.js
import fetch from 'node-fetch';

const siteId = process.env.NETLIFY_SITE_ID;
const token = process.env.NETLIFY_TOKEN;

const handler = async event => {
  try {
    const endpoint = `https://api.netlify.com/api/v1/sites/${siteId}/deploys`;
    const result = await fetch(endpoint, {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });

    const data = await result.json();

    // first entry is last deploy
    const deploy = {
      state: data[0].state,
    };

    return {
      statusCode: 200,
      body: JSON.stringify(deploy),
    };
  } catch (error) {
    return { statusCode: 500, body: error.toString() };
  }
}

module.exports = { handler };

We trigger rebuild process inside our custom refresh handler and wait for the deployment process to finish. After that the page can be refreshed using the originalRefresh callback.

// inside a component used as a wrapper for all pages

// This function triggers the build hook defined on Netlify, which
// starts deployment process on Netlify. This is required to get new data
// to preview.
//
// This is just the PoC. In a real app, we need to kill the previous active deploy, before
// starting a new one.
const triggerRebuildOnNetlifyAndWaitForDeploy = useCallback(() => {
  fetch('https://api.netlify.com/build_hooks/{HOOK_ID}?trigger_title=autorefresh', {
    method: 'POST',
  }).then(() => {
    async function checkDeployStatus() {
      // 'deploy-status' is a custom Netlify function that returns status of the last deploy
      const buildReq = await fetch('/.netlify/functions/deploy-status');
      const buildData = await buildReq.json();

      // display a loader to users to let them know rebuild is in progress
      setRebuildInProgress(buildData.state !== 'ready');

      if (buildData.state === 'ready') {
        resolve();
      } else {
        setTimeout(checkDeployStatus, 3000);
      }
    }

    checkDeployStatus();
  })
}, []);

useEffect(() => {
  const plugin = KontentSmartLink.initialize({
    queryParam: 'preview-mode',
  });

  // custom refresh handler
  plugin.on('refresh', (data, metadata, originalReload) => {
    // You can trigger the rebuild of the page, wait for it to finish and refresh the page after that.
    // Please consider displaying some sort of loader to your users, in case rebuild process takes time, to let them
    // know that the refresh is in progress.
    triggerRebuildOnNetlifyAndWaitForDeploy().then(originalReload)
  });

  return () => {
    plugin.destroy();
  }
}, [triggerRebuildOnNetlifyAndWaitForDeploy]);

For even better experience, you can combine the previous two methods and re-fetch content from Delivery Preview API on the client-side to update affected placed while waiting for the rebuild process to finish.

Using SDK inside and outside Web Spotlight

When Kontent.ai Smart Link SDK is used outside Web Spotlight, it listens to the query parameters in the URL to toggle smart link injection. The name of the query parameter defaults to ksl-enabled, but can be changed using the queryParam configuration argument of the initialize or initializeOnLoad methods. Only the presence of the query parameter is checked and its value is ignored, so all the following options are valid: ?ksl-enabled=true, ?ksl-enabled=false , ?ksl-enabled, etc.

If you set the query parameter to a false value (null, ""), then the SDK will always be enabled.

If the SDK detects it is run inside an iframe at the beginning of the initialization process, it will try to connect to the Web Spotlight by sending an iframe message to the parent window. If Web Spotlight response is received, query parameter detection will be turned off and additional features (more in the smart link types section) will be enabled. Else the SDK will continue to work as if it was outside Web Spotlight (query parameters detection, redirects to Kontent.ai, etc.)

IFrame Communication

When running inside Web Spotlight preview iframe, Kontent.ai Smart Link SDK enables several additional features and sends iframe messages instead of redirecting user to Kontent.ai page. All message types are listed below.

Message Data Origin Description
kontent-smart-link:initialized { projectId: string | null, languageCodename: string | null, enabled: boolean } SDK This message is sent by the SDK when it is initialized.
kontent-smart-link:initialized:response - Host This message is sent by the host as a response to initialized message.
kontent-smart-link:status { enabled: boolean } Host This message is used to toggle the SDK features.
kontent-smart-link:element:clicked { projectId: string, languageCodename: string, itemId: string, contentComponentId?: string, elementCodename: string } SDK This message is sent by the SDK when element with data-kontent-element-codename attribute is clicked.
kontent-smart-link:content-component:clicked { projectId: string, languageCodename: string, itemId: string, contentComponentId: string } SDK This message is sent by the SDK when element with data-kontent-component-id attribute is clicked.
kontent-smart-link:content-item:clicked { projectId: string, languageCodename: string, itemId: string } SDK This message is sent by the SDK when element with data-kontent-item-id attribute is clicked.
kontent-smart-link:add:initial { projectId: string, languageCodename: string, itemId: string, contentComponentId?: string, elementCodename: string, insertPosition: { targetId?: string, placement: 'start' | 'end' | 'before' | 'after', } } SDK This message is sent by the SDK when add button is clicked.
kontent-smart-link:add:initial:response { elementType: 'LinkedItems' | 'RichText' | 'Unknown', isParentPublished: boolean, permissions: Map<string,string> } Host This message is sent by the host as a response to initial add button click.
kontent-smart-link:add:action { projectId: string, languageCodename: string, itemId: string, contentComponentId?: string, elementCodename: string, action: string, insertPosition: { targetId?: string, placement: 'start' | 'end' | 'before' | 'after', } } SDK This message is sent by the SDK when add button action is clicked.
kontent-smart-link:preview:refresh { projectId: string, languageCodename: string, updatedItemCodename: string } | undefined Host This message is sent when preview has to be refreshed.
kontent-smart-link:preview:current-url - Host This message is sent by host as a request to get URL of current iframe.
kontent-smart-link:preview:current-url:response { previewUrl: string } SDK This message is sent by SDK as a response on the kontent-smart-link:preview:current-url message.

Nested iframes

There may be some cases when you would want to put your page into another iframe (e.g. to simulate a mobile device resolution). But if you then load your nested iframe page inside Web Spotlight preview tab, it would act as if it wasn't inside Web Spotlight. This happens because the initialization message sent from SDK to Kontent.ai gets lost in the parent iframe. You can use the following workaround to fix the issue: #16.

Examples

HTML & UMD & CDN

<html>
  <head>
    <title>Kontent.ai Smart Link - HTML example</title>
    <script type="text/javascript"
            src="https://cdn.jsdelivr.net/npm/@kontent-ai/[email protected]/dist/kontent-smart-link.umd.min.js"></script>
    <script type="text/javascript">
      KontentSmartLink.initializeOnLoad({ queryParam: "preview" });
    </script>
  </head>
  <body data-kontent-project-id="1d50a0f7-9033-48f3-a96e-7771c73f9683" data-kontent-language-codename="en-US">
    <nav class="navigation" data-kontent-item-id="6ea11626-336d-47e5-9f35-2d44fa1ad6d6">
      <img class="navigation__logo" data-kontent-element-codename="logo" />
      <ul
        class="navigation__list"
        data-kontent-element-codename="navigation"
        data-kontent-add-button
        data-kontent-render-position="left"
        data-kontent-insert-position="start"
      >
        <li class="navigation__list-item" data-kontent-component-id="036acd8f-5e6d-4023-b0f8-a4b8e0b573b1">
          <span data-kontent-element-codename="title">Home</span>
        </li>
        <li class="navigation__list-item" data-kontent-component-id="f539f1bc-9dc4-4df5-8876-dbb1de5ae6eb">
          <span data-kontent-element-codename="title">About us</span>
        </li>
      </ul>
    </nav>
    <div
      class="page"
      data-kontent-item-id="af858748-f48a-4169-9b35-b10c9d3984ef"
      data-kontent-element-codename="page_content"
    >
      <div
        class="section"
        data-kontent-component-id="51a90561-9084-4d32-9e34-80da7c88c202"
        data-kontent-add-button
        data-kontent-add-button-render-position="bottom"
        data-kontent-add-button-insert-position="after"
      >
        <img class="home__banner" data-kontent-element-codename="image" />
        <h1 data-kontent-element-codename="title">Home page</h1>
      </div>
      <div
        class="section"
        data-kontent-component-id="23e657d2-e4ce-4878-a77d-365db46c956d"
        data-kontent-add-button
        data-kontent-add-button-render-position="bottom"
        data-kontent-add-button-insert-position="after"
      >
        <p data-kontent-element-codename="text">...</p>
      </div>
    </div>
  </body>
</html>

ES6

import KontentSmartLink from "@kontent-ai/smart-link";

// This is just an example of SDK initialization inside ES6 module.
// HTML markup should still contain all necessary data-attributes.
const kontentSmartLink = KontentSmartLink.initializeOnLoad({
  debug: true,
  defaultDataAttributes: {
    projectId: "1d50a0f7-9033-48f3-a96e-7771c73f9683",
    languageCodename: "default",
  },
  queryParam: "ksl-preview"
});

Next.js

In order to use the SDK with the Next.js framework you can either initialize it separately on each page or initialize it once for the whole application using the _app.jsx file. Do not forget to destroy() SDK for it to work properly.

// _app.jsx
import KontentSmartLink from "@kontent-ai/smart-link";

const MyApp = ({
  Component,
  pageProps
}) => {
  useEffect(() => {
    // This is just an example of SDK initialization inside ES6 module.
    // HTML markup should still contain all necessary data-attributes (e.g. PageSection component).
    const kontentSmartLink = KontentSmartLink.initialize({
      defaultDataAttributes: {
        projectId: "1d50a0f7-9033-48f3-a96e-7771c73f9683",
        languageCodename: "default",
      },
      queryParam: "preview-mode"
    });

    return () => {
      kontentSmartLink.destroy();
    };
  });

  return (
    <PageSection>
      <Component {...pageProps} />
    </PageSection>
  );
};

const PageSection = (props) => {
  return (
    <div data-kontent-item-id="3fdbc5a0-13e6-4516-82c3-50bf4db43644">
      <div data-kontent-element-codename="page_section__content">
        {props.children}
      </div>
    </div>
  );
};

Additionally, you may encounter an issue with SameSite cookies not being set correctly. This can be solved by utilizing the code snippet below in your API route handling preview file to replace SameSite=Lax to SameSite=None; Secure; right after res.setPreviewData call.

const setCookieSameSite = (res, value) => {
    const cookies = res.getHeader("Set-Cookie");
    const updatedCookies = cookies?.map((cookie) =>
        cookie.replace(
            "SameSite=Lax",
            `SameSite=${value}; Secure;`
        )
    )
    res.setHeader(
        "Set-Cookie",
        updatedCookies
    );
};
export default function handler(req, res) {
    // ...
    res.setPreviewData({})

    // THIS NEEDED TO BE ADDED
    setCookieSameSite(res, "None");
    // ...
}

Gatsby

You can either initialize the SDK on every page or use a layout to initialize the SDK while using Gatsby. Do not forget to destroy() SDK for it to work properly.

// src/components/layout.jsx
import KontentSmartLink from "@kontent-ai/smart-link";

export default function Layout({ children }) {
  useEffect(() => {
    // This is just an example of SDK initialization inside ES6 module.
    // HTML markup should still contain all necessary data-attributes (e.g. .layout element).
    const kontentSmartLink = KontentSmartLink.initialize({
      queryParam: "enable-ksl-sdk"
    });
    return () => {
      kontentSmartLink.destroy();
    };
  });

  return (
    <div
      class="layout"
      data-kontent-project-id="1d50a0f7-9033-48f3-a96e-7771c73f9683"
      data-kontent-language-codename="en-US"
    >
      {children}
    </div>
  );
}

Tests

Unit tests

Since this SDK highly depends on browser APIs, the unit tests are run by Karma test runner (+ Jasmine) inside Chrome browser. To run all tests in a watch mode you can use the npm run test:unit command. To run all tests only once you can use the npm run test:unit:ci command. All unit tests are located in the test-browser folder.

Visual regression tests

Visual regression testing is implemented using Storybook and Loki. Each story in Storybook represents a test case, which is then used by Loki to generate screenshots. In order to run visual regression tests you need to start Storybook using the npm run storybook command and then start loki testing using the npm run test:visual command. Or you can use the npm run test:visual:ci command to automatically start the Storybook server in a CI mode and run visual tests.

Visual regression tests use the built version of SDK, so before running them make sure you rebuild the SDK after the last change you made. You can this using the npm run build command or using the npm run dev command to start build in a watch mode.

Please note that the reference screenshots for visual regression tests are created on the ubuntu-latest environment, which is utilized in our GitHub Action workflow for visual tests. It means tests could (and probably will) fail on Windows. In case the visual regression tests fail during a pull request, and you need to update reference screenshots, you can locate the new screenshots in the failed GitHub Action run. Navigate to the Artifacts section (.loki/current) of the failed run to find the updated screenshots.

Breaking changes

All breaking changes can be found in a separate markdown file.

Feedback & Contribution

Feedback & Contributions are welcomed. Feel free to take/start an issue & submit PR.

smart-link's People

Contributors

dependabot[bot] avatar ivankiral avatar kontent-ai-bot avatar pavelvyskocil avatar peterskoda avatar petrsvihlik avatar simply007 avatar vladbulyukhin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

smart-link's Issues

Website with SDK cannot be used inside of an iframe (other than WS)

Motivation

Right now if the website connected to the SDK is run inside of an iframe (other than WS), the redirect feature and Web Spotlight features (enable/disable editable elements, in-context editor, etc.) won't work. This happens because the SDK assumes that the iframe, it is being run inside of, is a Web Spotlight preview iframe and tries sending messages to this iframe which are left unprocessed. Currently, there is no way to override this behavior.

Proposed solution

First of all, we need to decide whether we want to somehow fix this issue or not. There are several possible solutions that can be used to solve it:

  • We can add a boolean flag to the SDK configuration object that will allow users to disable the iframe communication. This way when someone wants to wrap a website connected to the SDK with an iframe, they can disable the iframe communication feature and smart links will always open Kontent in a new tab instead of sending iframe messages. The main disadvantage of this method is that without the iframe communication smart links will open Kontent in a new tab even in Web Spotlight because Web Spotlight requires iframe communication to work properly.
  • We can add some public helpers to SDK that will help users with this problem (more information about this can be found in the Workaround section of this issue).

Workaround

If you plan on using the website (connected to the SDK) inside of an iframe, you can use this workaround to make everything work properly with Web Spotlight.

The first thing you need to do is listen to all message events on your host page window object.

window.addEventListener('message', handleMessage);

Then whenever you receive a new message, you need to check whether it is a message related to SDK. If it is related to the SDK, you need to check if you are currently inside an iframe and if you aren't, you just generate a smart link (using buildKontentLink helper from @kentico/kontent-smart-link) and open it in a new tab. If you are inside an iframe, you need to check the direction of the received message (upwards from inner iframe/downwards from parent window) by comparing the source of this event to the iframe content window. Finally, you need to resend the whole event to the parent window or inner iframe according to the event direction.

import { buildKontentLink } from '@kentico/kontent-smart-link';

function handleMessage(event) {
  const eventTypeRegex = /^kontent-smart-link:/;
  
  // check if this message is not related to the SDK
  if (!event.data || !event.data.type || !eventTypeRegex.test(event.data.type)) {
    return;
  }

  const isInsideIFrame = window.self !== window.parent;
  const message = event.data;

  if (isInsideIFrame) {
    // The iframe variable has a reference to the iframe element.
    // You don't have to use document.querySelector here, for example,
    // in React you can use `useRef` hook to save the reference and then 
    // use it here, in Vue you can use `$refs`, etc.
    const iframe = document.querySelector('#your-iframe-id');
    const isMessageGoingUpwards = event.source === iframe.contentWindow;

    if (isMessageGoingUpwards) {
      // resend this message to the parent window
      window.parent.postMessage(message, '*');
    } else {
      // resend this message to the iframe
      iframe.contentWindow.postMessage(message, '*');
    }
  } else if (message.type === 'kontent-smart-link:element:clicked') {
    const link = buildKontentLink(event.data.data);
    window.open(link, '_blank');
  }
}

Additional context

Add any other context, screenshots, or reference links about the feature request here.

Add helper methods for creating attributes

Motivation

Manually adding element attributes can be rather repetitive, especially for add buttons, where multiple attributes need to be specified. This could be mitigated to some extent by including helper methods in the SDK.

Proposed solution

Add a set of helper methods which will create the required attributes for each available smartlink button based on user-provided values. See example implementation in our sample app source code.

Make controls' z-index configurable

Motivation

It would be great to have z-index CSS property of overlay (border with pencil).

I have my menu with z-index:10002 (inherited from template) , so I would appreciate being able to customize that.

image

Proposed solution

Make ti a part of the configuration

Additional context

Add any other context, screenshots, or reference links about the feature request here.

Allow edit item button outside Web Spotlight

Motivation

Edit item is effectively a superset of Edit element, the latter being available both in WSL and standard preview. As such, it would make sense to allow Edit item to be rendered in preview as well, as a similar functionality can be achieved by using the first element of an item to render Edit element button, which seems like an unnecessary workaround.

Proposed solution

Make Edit item button available outside web spotlight context.

Missing data-kontent-language-codename attribute logs project ID error to console.

Brief bug description

While using SmartLink SDK in Web Spotlight, when data-kontent-language-codename is missing, the error:
Logger.js:15 [KSL][Error]: Project ID is required to handle element click.

is logged to the browser console, even if the kontent-data-project-id attribute is set correctly in the markup.

Repro steps

  1. Open up a WSL project with SL SDK
  2. Remove the data-kontent-language-codename attribute using Chrome DevTools
  3. Click a SL SDK edit button
  4. See browser console error in DevTools

Expected behavior

The console should only report that there is an issue with the language codename element, not the project ID.

Test environment

  • Platform/OS: Windows 10, gatsby-starter-kontent-lumen sample site, gatsby-source-kontent 7.2.0, SL SDK v2.1.0
  • Browser Chrome
  • Version 96.0.4664.110

Screenshots

image

image

Links are not highlighted

Brief bug description

In some cases, links are not highlighted (no dashed border and edit icon). However, they are clickable and lead to the correct place in KK admin.
Could be duplicate of #21

Expected behavior

Smart links should be highlighted.

Additional context

I implement this in the KK Docs and I experience the issue on article pages like https://docs.kontent.ai/tutorials/set-up-kontent/set-up-your-project/projects (Smart links are not on the live site. However if you try to implement Smart links on that page, you will experience the issue.) This is a classic Express.js site with full server-side rendering. Smart links JS and CSS files are loaded using CDN.

I noticed that on pages that have the issue, the "kontent-smart-link-overlay" element gets rendered twice. See the screenshot linked below. Maybe this could be related to the issue.

Screenshots

https://prnt.sc/xlg4j8

CSS properties are overriden - causing spotlight overlay to not display

Brief bug description

When using the latest version of Kontent Smart Link (v2.1.0), the :host CSS properties will be overidden by any higher-order CSS selector. For example, Tailwind CSS's Preflight sets all border width's to 0. This causes the :host border-width to be ignored, and the border width to be set to 0. This causes the spotlight overlay to be hidden.

image
image

This can be remediated by flagging the appropriate properties as !important:

image
image

Repro steps

  1. Create a page using a higher-order CSS selector (such as those used by Tailwind CSS's Preflight)
  2. Observe - no spotlight overlay will show
  3. Add the !important flag
  4. Observe - overlay will show

Expected behavior

The overlay should display.

image

Test environment

  • Platform/OS: Windows 10
  • Browser: Firefox
  • Version: 89.0.2 (64-bit)

Additional context

Add any other context about the problem here.

Screenshots

Add links to screenshots, if possible.

Add button toolbar is not fully visible on the edge of the screen

Brief bug description

When the add button is on the edge of the preview page inside iFrame, the tooltip and toolbar are not shown correctly.

Expected behavior

The add button and its tooltip should be fully visible even on the edge of the screen.

Screenshots

image-2021-06-10-11-51-35-717

SDK does not work outside of browser

Brief bug description

Using this SDK is problematic under node.js (e.g. when using Angular universal = SSR) because SDK directly invokes browser specific API - MutationObserver (https://github.com/Kentico/kontent-smart-link/blob/e173a36628ccfb851a0f54b93a965957b3d3e309/src/lib/NodeSmartLinkProvider.ts#L44)

In such cases you get ReferenceError: MutationObserver is not defined exception.

This SDK should check whether the API is available and use Polyfills (if available for our use case) or use different API alltogether specific to Node.js. When using API specific do Node.js, be sure to "exclude" this code publicly exported API that might be used in browser where this API is not available as it might prevent you from building the library with Typescript/Webpack etc.. Careful testing is needed for this to work both in node.js & browsers.

If none of this is possible, we should add a disclaimer that node.js is not supported, but I don't think that should be our goal as this should be quite common use case.

SDK Does not support multiple delivery URLs

Brief bug description

What went wrong?
When developing an application with multiple URLs endpoints for the web, we need the ability to use SmartLink on different domains. In my scenario, I'm using an Azure Function to determine which URL should be used to preview content based primarily on the collection that the content belongs to. This performs a 302 redirect to the correct URL.

The issue then arises that Smark Link forces the origin and the preview URL to match. If they do not, an error is raised and the SmartLink SDK will not render. This is not compatible with the omnichannel narrative and ties Kontent to only being capable of supporting a single web application.

Repro steps

  1. Set a preview URL for a content type to link to an Azure Function (or another API endpoint) that will redirect to another domain containing the application.
  2. Configure preview mode and SmartLink in the target web application
  3. View page in web spotlight
  4. View error in console - SmartLink does not render.

Expected behavior

SmartLink should open regardless of the different origin.

Test environment

  • Platform/OS: [e.g. .NET Core 2.1, iOS]
  • Browser [e.g. chrome, safari]
  • Version [e.g. 22]

Additional context

Other use cases here/scenarios are being able to provide a customer support portal, consumer-facing and B2B web application from the same project in Kontent.

If the concern/reason for this restriction is related to security, I'd rather make a 'secret' available in my applications that can be validated that the current implementation or the ability to add preview URL aliases. Simply preventing it (and also not documenting that limitation as far as I can see) is making working with Web Spotlight less of a delight than it could/should be :)

Screenshots

Add links to screenshots, if possible.
image

My preview URL for the above image looks like this: https://gr-schedule.azurewebsites.net/api/gr-preview-url?secret=1234&cn={Codename}&col={Collection}&l={Lang}&url={URLSlug}

Add option to hide features

Motivation

In some scenarios, it would be handy to have an option to hide some of the features which are not desirable in a given context. Take these examples

  • I want to show a relative add-button on an element. To do that I need to add an itemId attribute on that element. The itemId attribute will also render the edit-button, which I don’t want here. => I can’t hide the edit-button while keeping the add-button, without resorting to some wrapper and css workaround, which is cumbersome.
  • by default, the edit button functionality extends to the whole area of the element it's enabled for. this can be potentially undesirable in situations where the overlaid content includes some interactive/clickable buttons, in which case smartlink needs to be disabled.

Proposed solution

Provide more configuration options, which would allow disabling unwanted features either per element basis (e.g. using an optional boolean attribute) or globally

Additional context

Add any other context, screenshots, or reference links about the feature request here.

Add developer mode to see all buttons outside WSL

Motivation

Some of the smartlink features are only available in WSL environment. While understandable from customer's point of view, it would make development easier if there was an option to display all buttons in a regular preview.

Proposed solution

Add a toggle in the SDK initialization or as a query parameter, which would display all buttons, regardless of the environment the site is rendered in.

Vue.js smart links are not highlighted on the first load

Brief bug description

When the SDK is initialized inside of the main Vue component (App.vue) using lifecycle methods (mounted, destroyed), smart links are not highlighted (there is no blue border around Kontent elements), but clicking on the element still redirects you to Kontent.

Expected behavior

Smart links should be highlighted on the first load of Vue application as they are highlighted in React applications.

Additional context

I investigated it a little and found out that the problem is probably here in the relevantMutations computation condition. We used && in this condition between addedNodes and removedNodes, because we mostly tested this SDK using React application and React always tries to make as many changes to DOM in one mutation as possible, that is why there are always both removedNodes and addedNodes. But as I found out today Vue separates additions and removals into separate mutations and this condition is never met. Replacing it with something like this should fix the problem:

const relevantMutations = mutations.filter(
  (mutation) =>
    mutation.target instanceof Element &&
    mutation.type === 'childList' &&
    ![HighlighterElementTag, HighlighterContainerTag].includes(mutation.target.tagName) &&
    (Array.from(mutation.addedNodes).some((node) => (node as HTMLElement).tagName !== 
    HighlighterElementTag) ||
    Array.from(mutation.removedNodes).some((node) => (node as HTMLElement).tagName !== 
    HighlighterElementTag))
);

Developer toolbar

Motivation

In some situations, while developing data attributes, it is difficult to verify that edit buttons will show properly on the webpage: e.g. if I want to use kontent-smart-link-enabled query param in an application which does not keep query params between links/redirects, I would need to do changes in routing just for this sake.

Proposed solution

It would be nice to allow developers enable/disable edit buttons on some specific page without refresh or altering query params. E.g. some developer toolbar.

Make smart link injection possible without a query parameter

Motivation

For our website, we have a preview stage separated from the production site. On that preview stage, we want to have smart links always injected. However, as the SDK requires a query parameter to enable smart link injection, editors must add that parameter by themselves which is annoying. Another possible solution is to inject that query parameter programmatically which is annoying too.

Proposed solution

Allow the queryParam to accept an empty string which would result in the smart link injection with no query parameter. Any other reasonable type of configuration that would result in the smart link injection with no query parameter is acceptable too.

data-kontent-add-button only functions if content item id and element codename are on the same HTML tag

Brief bug description

What went wrong?
After adding a data-kontent-add-button & data-kontent-element-codename attributes to a div, the + button appeared next to a related items field in the preview, but clicking it greyed it out and gave a JS exception in the console:
image

The console suggests theres no language, project, content item or element codename set, however all are set in parent html elements.

Repro steps

  1. Create a content item with a related items field.
  2. Set the codename to "related_pages"
  3. Create a content item, and add some pages to the related pages field from 2.
  4. Note the content item id
  5. Setup a page with this sample html:
<body data-kontent-language-codename="en-GB" data-kontent-project-id="YOUR PROJECT ID">

    <div class="page-container" data-kontent-item-id="<The content item id from 4. that has the related_pages field>">

          <div class="related-pages" data-kontent-element-codename="related_pages" data-kontent-add-button>
               <h1>Example related page list item 1</h1>
               <h1>Example related page list item 2</h1>
           </div>

      </div>

</body>
  1. View this page on the web spotlight preview area (might require setting up the content item as a subpage of homepage)
  2. Click the + button next to the example h1s
  3. See the error in the console.

Note: This doesnt occur as soon as you add data-kontent-item-id="<The content item id from 4. that has the related_pages field>" to the .related-pages html element.

Expected behavior

Like the other data-kontent tags, Id expect the kontent-item-id to be inherited based on the hierarchy for use on the kontent-element's button, as per the readme:

'Although it is possible to put all previously specified data attributes on the same DOM node, you don't have to do it. We recommend you set data attributes hierarchically so that you don't have to duplicate the same attributes.'

Test environment

  • Platform/OS: .NET Core 5, Windows 11
  • Browser Chromium
  • Version 99

Add a Troubleshooting section to docs and cover the X-Frame-Options

Motivation

Depending on the server's settings, it might be necessary to adjust the X-Frame-Options header in order to make Web Spotlight work flawlessly.

Proposed solution

Add a Troubleshooting section to the README. This new section should explain the problem, outline the possible solutions, and refer to appropriate detailed resources.

Additional context

This was pointed out to me by @Simply007 at kontent-ai/sample-app-net@384efb0#r44481635
This can serve as an example for .NET-based solutions.

Use environment ID instead of project ID

Motivation

In the product, we're using environment ID as the name of the identifier, but the Smart Link SDK still uses the data-kontent-project-id attribute, which is confusing to users.

Proposed solution

Update the SDK to use data-kontent-environment-id instead because that's what you can get in the product UI.

Border not rendered if the element is big

Brief bug description

I have tried to render the web spotlight and if I use it on pretty long article text, the border and the pencil is not rendered. The click-through is working still.

I don't see the <kontent-smart-link-element> tag in <kontent-smart-link-overlay> tag in html.

image

Repro steps

  1. Clone https://github.com/Kentico/gatsby-starter-kontent-lumen/tree/webspotlight
  2. npm i # should copy .env.template and name it .env
  3. npm run develop
  4. Enter http://localhost:8000/articles/humane-typography-in-the-digital-age/?preview-mode=true

Expected behavior

I expect to have a pencil and border no matter what 😄

Additional context

If I un-zoom the page the border is rendered.
image

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.