> ## Documentation Index
> Fetch the complete documentation index at: https://devs.surferseo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Embed Surfer guidelines in your CMS

> Put Surfer's guidelines panel next to your own editor and feed it the article's HTML.

Surfer's JavaScript library hands you the panel as an iframe to place and size yourself, and the panel scores every change you send it.

The panel shows one Content Editor, the document Surfer builds around your target keywords, holding the guidelines and the Content Score ([The guidelines panel](/integrations/guidelines-panel)).

## Availability

To embed Surfer in your CMS, contact [sales@surferseo.com](mailto:sales@surferseo.com).

The embed itself needs:

* Your editor runs in a browser, on an HTTPS page. The panel's request for storage access is refused on a page that is not served over HTTPS.
* The writer is signed in to Surfer in the same browser, to link or create a Content Editor from the panel. A page whose permalink already resolves shows its guidelines either way ([Access and sharing](/integrations/embed/access-and-sharing)).
* The browser lets Surfer use its cookies inside your page: third-party cookies allowed for `app.surferseo.com`, or storage access granted when the panel asks. No browser extension is needed; [Sign in and cookies](/integrations/guidelines-panel#sign-in-and-cookies) has the details.
* The writer's Surfer account includes Content Editors.

## Add the library

Paste this loader into the `<body>` of your page, before your own script. It defines `window.surferGuidelines` at once and fetches the library in the background:

```html theme={"system"}
<script>
  ;(() => {
    if (window.surferGuidelines !== undefined) return

    window.surferGuidelines = {
      init(...args) {
        window.__sg_in = args
      },
      setHtml(...args) {
        window.__sg_sh = args
      },
      initWithOptions(...args) {
        window.__sh_gi = args
      },
    }

    const libTag = document.createElement('script')
    libTag.async = true
    libTag.src = 'https://app.surferseo.com/static/surfer_guidelines_1_x_x.js'
    document.body.appendChild(libTag)
  })()
</script>
```

A call to `init` or `initWithOptions` made before the library arrives is replayed once it has loaded. A stored `setHtml` is sent by `init`, not by the load itself. A host that uses `initWithOptions` sends its HTML through the handle instead, as the example below does.

> [!NOTE]
>
> `initWithOptions` returns its handle only once the library has loaded; before
> that, the loader's stand-in returns nothing. The example below therefore runs
> on the page's `load` event, which the browser holds until the script has run.

`surfer_guidelines_1_x_x.js` is the current release. [Versioning](/integrations/embed/javascript-api#versioning) covers pinned files and what a pin freezes.

The WordPress plugin, the Contentful app, and the Surfer for Google Docs extension use the same library.

## Add a container and start the panel

Give the panel an element to live in, then create it with `initWithOptions`, set the permalink, and send the editor's HTML. The permalink in the example is a placeholder. A page URL anyone can guess is a permalink anyone can guess, so read [Access and sharing](/integrations/embed/access-and-sharing) before you settle on its format.

Add this after the loader. It has a text area for the article's HTML and a container for the panel:

```html theme={"system"}
<div id="editor-page">
  <textarea id="editor" placeholder="Write the article as HTML"></textarea>
  <div id="surfer-guidelines"></div>
</div>

<script>
  window.addEventListener('load', () => {
    const { $iframe, setPermalink, setHtml } =
      window.surferGuidelines.initWithOptions({
        partner: 'your-cms',
        onContentScoreChanged(score) {
          console.log('Content Score', score)
        },
      })

    document.getElementById('surfer-guidelines').appendChild($iframe)

    setPermalink('https://your.cms.com/draft/some-draft-id')

    const editor = document.getElementById('editor')
    setHtml(editor.value)
    editor.addEventListener('input', () => setHtml(editor.value))
  })
</script>
```

What each call does:

* `initWithOptions` creates the iframe and returns it together with the functions that drive it. The iframe is not in the page until you append it.
* `partner` names your integration to Surfer. Use one short identifier for your product.
* `setPermalink` tells the panel which document this is; until it receives one, the panel stays on its loading state.
* `setHtml` sends the article. Call it once at start and again on every change.
* `onContentScoreChanged` runs each time the SEO score changes.

The library sets no size and injects no CSS, so style the iframe yourself. Surfer's Google Docs panel uses a sidebar 320 to 400 pixels wide. This CSS, in your page's stylesheet, puts the panel at the right edge, full height:

```css theme={"system"}
#editor-page {
  display: flex;
  height: 100vh;
}

#editor {
  flex: 1;
}

#surfer-guidelines iframe {
  width: 400px;
  height: 100%;
  border: 0;
}
```

## What you see

The panel shows one of four things, depending on whether the permalink resolves and whether the writer is signed in.

| Situation | The panel shows |
| - | - |
| The panel is checking cookie access, waiting for a permalink, or resolving one | A loading indicator. |
| The permalink resolves to a Content Editor, whether or not the writer is signed in | The guidelines and the Content Score. |
| Nothing is linked to the permalink and the writer is signed in | The dashboard, listing the writer's Content Editors. |
| Nothing is linked and the writer is signed out | The connect screen, headed `Connect Content Editor to this document`. |

Picking or creating a Content Editor in the dashboard links it to the permalink ([Credits](/integrations/guidelines-panel#credits) covers what that costs). From then on the panel shows its guidelines, and every later visit with the same permalink finds it. [What the panel shows](/integrations/guidelines-panel#what-the-panel-shows) covers the tabs and controls.

## Send the article's HTML

Send the article body as HTML, on every change, and read the score back from the callback.

* When: once at start and after each edit. Each call posts the whole HTML to the panel, so debounce the call if your editor fires on every keystroke.
* After `setPermalink`: call `setHtml` again, because a new permalink drops the HTML the library holds ([The handle](/integrations/embed/javascript-api#the-handle)).

Use `onContentScoreChanged` to receive scores. The [JavaScript API](/integrations/embed/javascript-api#options) documents its payload, the zero-score behavior, and the other callbacks.

## Next steps

* [Access and sharing](/integrations/embed/access-and-sharing): the signed-in and shared modes, getting a permalink for an existing Content Editor from the API, and sharing tokens.
* [JavaScript API](/integrations/embed/javascript-api): every option, callback, and handle function with its payload.

## Troubleshooting

The symptoms below trace to cookies, the permalink, the HTML feed, or the subscription.

> **The panel shows the connect screen although the writer is signed in**
>
> The browser blocks third-party cookies on your page, so the panel cannot read the writer's Surfer session. The screen offers a way to grant access. [The connect screen](/integrations/guidelines-panel#the-connect-screen) has its controls and the steps.
>
> A private or Incognito window that blocks third-party cookies shows the same screen.

> **The panel does not find the document's Content Editor**
>
> The permalink did not resolve.
>
> * A value that differs by one character from the one the Content Editor was linked with is a different permalink.
> * A changed sharing token also makes a different permalink.
> * `onDraftLoaded` receives `null` when the permalink does not resolve.
>
> A panel that never leaves the loading indicator has received no permalink at all. [Access and sharing](/integrations/embed/access-and-sharing) covers linking an existing Content Editor and keeping tokens stable.

> **The score never changes**
>
> The panel sends a score only when it has HTML and the score is above zero. Check that:
>
> * `setHtml` receives a string of HTML, not your editor's document object.
> * It runs again after every change and after each `setPermalink`.
> * The text is not empty.
>
> Timing is not the cause. The library holds HTML sent early and delivers it once the panel has loaded the Content Editor ([The handle](/integrations/embed/javascript-api#the-handle)).

> **The panel shows Content Editor is not available**
>
> An organization's subscription is not active. [Plans](/integrations/guidelines-panel#plans) has the full message and says which organization it refers to.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.