Embeddable Policy Changelog Widget

If you monitor your own privacy policy, terms of service or DPA with PageCrawl, you can publish the revision history back onto that page as a small embedded widget. Visitors see a dated list of what changed and when, in the same shape as a public tracked page's timeline.

What does the widget show?

A "last updated" line under the heading, then a dated row per detected edit, newest first, each with a one-line summary of what changed. Where AI summaries are enabled the summary describes the edit in plain language. Without them the row names the part of the page that changed, so the list stays accurate rather than guessing.

Optionally it also shows an importance label on each row.

With Detail set to "Summary and diff", each row keeps its short summary as the headline, and a "Show diff" link under it opens a redline of exactly what that update removed and added. The link reads "Hide diff" while it is open.

Any row with more to say expands in place to show the full summary, which parts of the page changed, and the exact time the change was detected. On a row with a diff, that detail opens from the "Show diff" link together with the redline. Expanding needs no JavaScript, so it works even where scripts are blocked.

What does the diff show?

A redline of exactly what was removed and added in that update. Removed words are struck through and added words are underlined, and inside a reworded line only the words that changed are marked. One unchanged line either side of each edit is kept for context, so a reader sees where the edit sits without rereading the page.

Longer unchanged stretches collapse to a single line such as "12 unchanged lines", so a one-word edit in a long policy stays short.

Only text you track gets a diff: the full page, reader view and text selections. Prices, numbers and other values keep their one-line summary. Raw HTML source, hidden text and documents such as PDFs are never diffed, and lines that the monitor's ignore rules touch are left out of the redline entirely.

When an update cannot be shown (it is too large, most of the page was rewritten, or one of your ignore rules spans several lines), the entry keeps its summary and says "The diff for this update cannot be shown here."

Can visitors follow the page themselves?

Yes, three ways, though the third is offered only on a monitor you have also made public, because it takes the visitor to a setup screen showing the page address. They can enter their email in the widget, which adds the page to their own PageCrawl account once they confirm it. They can subscribe to the Atom feed if they would rather follow it in a reader with no account: the feed address is shown in full so it can be selected and copied without leaving the page, and an RSS link sits in the widget footer. Or they can open the normal setup flow with the page already filled in.

The subscribe box is collapsed behind a "Get notified" button in the widget's top right corner, so the list stays the focus.

In every case the page address is resolved on our side, so it never appears in the widget's markup. Note: a visitor who chooses "set it up yourself" is taken to a prefilled setup screen, which does show them the address of the page being monitored.

What does it publish, and what stays private?

The widget publishes the dates, the summaries, and the names you gave the tracked parts of the page. With Detail set to "Summary and diff" it also publishes the wording each text edit removed and added. It never publishes screenshots, the monitored URL, or anything about your account.

Inside a diff, link addresses and image sources from your page are never published, only the visible link text. If the page prints its own address, the diff shows it as "this page".

A new embed starts on "Summary and diff" the first time you switch it on. An embed that was already on keeps the setting it had. You can switch Detail to "Summary only" at any time to publish the summaries without the wording. Detail is a deliberate choice about your own page, so it is stored on the monitor rather than set in the embed code: nobody can turn the diff on by editing the snippet or the address.

The heading is yours to set. If you leave it blank it reads "Changelog", except on a monitor you have also made public, where it falls back to the monitor's name.

This is a separate switch from making a page's full monitoring history public. Turning the changelog on does not open the full history page, the screenshot archive or the history export. The two settings are independent in both directions.

Can I leave small edits out of the changelog?

Yes. Set Minimum importance to "Medium and above", "Important and above" or "Critical only", and updates scored below that level are left out of the list, so small edits such as a footer tweak do not appear. The default, "All changes", lists everything. Updates without an importance score are always listed.

Like Detail, Minimum importance is stored on the monitor rather than in the embed code, so it applies to the widget, the Atom feed and the JSON alike, and nobody can change it by editing the snippet.

How do you turn it on?

  1. Open the monitored page in PageCrawl and choose Share.
  2. Under Changelog embed, switch on Enable embed.
  3. Pick the number of rows, the theme, the date format, the accent colour and whether to show importance labels. Leave Detail on "Summary and diff" to publish the wording each edit removed and added behind a "Show diff" link, or set it to "Summary only". Set Minimum importance if small edits should stay off the list.
  4. Copy the embed code and paste it into your policy page.

There are two embed codes to choose from. The script snippet is the one to prefer: it sizes the widget to its content automatically, so the box never ends up with an inner scrollbar or a gap beneath it, and it grows to fit when a visitor opens a diff. The plain iframe is there for sites that do not allow third-party scripts, and takes a fixed height you choose. An opened diff does not change that height, so the diff scrolls inside the frame.

How do I embed the changelog on a website with a Content Security Policy?

Allow https://pagecrawl.io in your website's frame-src directive. For the plain iframe embed, that is the only additional resource permission your page needs: the widget's scripts and styles run inside the PageCrawl frame. The JavaScript embed also needs permission to load https://pagecrawl.io/widget.js.

Add this source to your existing policy, preserving the other sources your site needs:

frame-src 'self' https://pagecrawl.io;

This is a directive example, not a complete CSP header. Replace 'none' if it currently blocks frames. Do not change your site's frame-ancestors: that controls who can embed your site, rather than which widgets your site can embed.

For a policy based on allowed script hosts, also add https://pagecrawl.io to script-src (or script-src-elem if you use it). For a nonce-based policy, put your response's script nonce on the embed script:

<script src="https://pagecrawl.io/widget.js" nonce="YOUR_SCRIPT_NONCE" async></script>

Replace the placeholder with the nonce authorized by your CSP, generated fresh for each response. With 'strict-dynamic', a host allowance alone does not authorize this script. A page with only changelog widgets does not need a subscription style block or a PageCrawl connect-src allowance. If you fetch the changelog JSON from your own frontend instead, allow https://pagecrawl.io in connect-src.

For a strict style policy, remove the plain iframe snippet's style attribute and style the frame through your own stylesheet. React and Vue changelog components need the same frame permission. Scripts and bot checks inside the PageCrawl frame do not require additional sources in your parent page's CSP.

Note: every applicable CSP header and meta policy must permit the embed. Check the browser console for the blocked directive if the widget does not load. The widget builder also explains the subscribe widget's script, connection, and style requirements. For background on the directives, see MDN's CSP reference.

Does the widget set cookies on my page?

No. Loading the widget sets no cookie, which matters because it is designed to sit on privacy policy and cookie policy pages. It is also marked so that search engines do not index it separately from your own page.

The widget itself sets no cookie, and its expanding rows and diffs work without any JavaScript.

Two things do load a script, and only where they are needed. The script snippet (or an iframe with resize=1 in its address) adds a small height reporter so the widget can size itself to its content; it reads nothing from your page. And where the subscribe box is shown, Cloudflare Turnstile runs invisibly to keep bots off the email field, which means a script from Cloudflare loads on that page. Switch the "Get notified" button off in the Share dialog (or add subscribe=0 to the address) and the bot check is gone. With the box off and a fixed-height iframe, which does not ask for the height reporter, the widget loads nothing beyond its own markup and styles.

A visitor who submits their email is taking an explicit action, and gets a confirmation email before anything is monitored.

Is the embed id safe to publish?

Yes, and it is deliberately not the same id as your public page link. The embed id is minted separately the first time you switch the embed on, and it only ever opens the changelog. Your public page link, which also opens the full history and its export, uses a different id that never appears in the embed.

Both ids are 32 random characters from a cryptographic random source, so neither can be guessed.

How is the email field protected from bots?

Two layers, neither of which asks a visitor to do anything. Cloudflare Turnstile runs in invisible mode, so a human sees no challenge and no checkbox. Separately, the submitting address is checked against a reputation database of reported abusive networks.

The reputation check fails open on purpose: if that service is slow, rate limited, or down, the signup goes through rather than turning away a real person. There is also a hidden honeypot field that quietly absorbs the simplest bots.

How quickly does turning it off take effect?

At our end, straight away: once you switch it off, the widget address stops serving your changelog. A visitor whose browser already loaded the widget may keep showing the cached copy for up to 15 minutes.

The same goes for publishing less. Switching Detail back to "Summary only", or raising the minimum importance, reaches the widget within about 15 minutes as those browser copies expire. Feed readers, and anyone who already fetched the JSON, keep what they received.

Can I style it myself?

Yes. You can set an accent colour, a corner radius, a light, dark or visitor-matched theme, comfortable or compact row spacing, a custom heading, and the date format. You can also turn off the border, the importance labels, the RSS link, or the subscribe box, so the widget can be as plain as a list of dates.

The accent applies to the button and the RSS link, and the button's own text colour is worked out from it, so a pale accent never ends up with unreadable white text on it.

If you would rather render the list with your own markup, the dates and summaries are available as JSON at the same address with /json on the end, so you can fetch them and lay them out to match your site. The JSON carries the same rows as the widget, with the same minimum importance applied. A summary-only embed returns exactly the fields it always has: date, date_iso, summary, and priority_label when requested. The rest of the expanded detail (the sections that changed and the detection time) is in the Atom feed and the widget itself. With the diff on, each Atom entry also carries the redline as HTML in its content, starting with the summary.

What does the JSON return when the diff is on?

Each entry whose wording changed also carries a diff object, holding sections and more_sections. Each section has a label (the tracked part's name, or null) and its lines, and more_sections counts the changed sections left out. A missing diff means no diff is available for that entry, not that nothing changed.

Each line has a type:

  • unchanged, removed or added, with its text.
  • changed, with parts: each part is an unchanged, removed or added run of text, so only the words that changed are marked.
  • gap, with the count of unchanged lines left out.
  • more, with the count of changed lines left out.

A line or part that was shortened carries clipped, set to start or end. The text itself never contains an inserted ellipsis, so add your own marker where you want one. An entry with "sections": [] and more_sections above zero means the wording changed but cannot be shown.

All text is plain text, not HTML, so escape it when you render it.

How do I embed it in React, Vue or another JavaScript framework?

The script snippet is for plain HTML pages. In a single-page app, use an iframe with resize=1 in its address, plus a message listener that sets the frame's height. The widget then reports its own height, so it grows to fit (including when a visitor opens a diff) without loading any script from us on your page.

The Share dialog in PageCrawl generates the React and Vue code with your own embed id and options filled in. The examples below use YOUR_EMBED_ID as a placeholder.

React and Next.js

import { useEffect, useRef, useState } from 'react';

export default function PolicyChangelog() {
    const frame = useRef(null);
    const [height, setHeight] = useState(240);

    useEffect(() => {
        const onMessage = event => {
            const value = event.data && event.data.pagecrawlChangelogHeight;
            if (event.source === frame.current?.contentWindow && typeof value === 'number' && value > 0) {
                setHeight(Math.min(value, 20000));
            }
        };
        window.addEventListener('message', onMessage);
        // The frame may have loaded before this listener existed (server rendering).
        frame.current?.contentWindow?.postMessage({ pagecrawlChangelogPing: 1 }, '*');
        return () => window.removeEventListener('message', onMessage);
    }, []);

    return (
        <iframe
            ref={frame}
            src="https://pagecrawl.io/widget/changelog/YOUR_EMBED_ID?theme=auto&resize=1"
            title="Policy changelog"
            scrolling="no"
            style={{ width: '100%', height, border: 'none', display: 'block' }}
        />
    );
}

Note: in the Next.js App Router, add 'use client' at the top of the component file, because the listener runs in the browser.

Vue

<template>
    <iframe
        ref="frame"
        src="https://pagecrawl.io/widget/changelog/YOUR_EMBED_ID?theme=auto&resize=1"
        title="Policy changelog"
        scrolling="no"
        :style="{ width: '100%', height: height + 'px', border: 'none', display: 'block' }"
    />
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';

const frame = ref(null);
const height = ref(240);

function onMessage(event) {
    const value = event.data && event.data.pagecrawlChangelogHeight;
    if (event.source === frame.value?.contentWindow && typeof value === 'number' && value > 0) {
        height.value = Math.min(value, 20000);
    }
}

onMounted(() => {
    window.addEventListener('message', onMessage);
    // The frame may have loaded before this listener existed (server rendering).
    frame.value?.contentWindow?.postMessage({ pagecrawlChangelogPing: 1 }, '*');
});
onBeforeUnmount(() => window.removeEventListener('message', onMessage));
</script>

Svelte, Angular and others

Any framework needs the same pieces: an iframe whose address includes resize=1, a message listener that sets the frame's height, and one message to the frame asking for its height once the listener is in place. That last step matters on server-rendered pages, where the frame can finish loading before your code starts listening. Add the listener when the component mounts and remove it when the component is destroyed. The plain JavaScript version, for an iframe with the id policy-changelog:

const frame = document.querySelector('#policy-changelog');
window.addEventListener('message', event => {
    const value = event.data && event.data.pagecrawlChangelogHeight;
    if (event.source === frame.contentWindow && typeof value === 'number' && value > 0) {
        frame.style.height = Math.min(value, 20000) + 'px';
    }
});
frame.contentWindow.postMessage({ pagecrawlChangelogPing: 1 }, '*');

The other options go in the same address, joined with &:

  • limit: the number of rows, 1 to 50 (10 by default).
  • theme: light, dark or auto to match the visitor.
  • dates: long, short or iso.
  • priority=1 to show importance labels.
  • color: the accent as a hex colour, with %23 in place of the # (for example color=%23e11d48).
  • radius: the corner radius in pixels, 0 to 50.
  • density=compact for tighter rows.
  • border=0 to drop the border, and rss=0 to drop the RSS link.
  • subscribe=0 to leave out the "Get notified" box.
  • title: your own heading.

Detail and Minimum importance are not address options. They are read from the monitor, so set them in the Share dialog.

Ready to Track Changes?

Set up monitoring in under 60 seconds and never miss important updates again.

Track a New Page