Full setup
Set up a JavaScript embed on your page—placement, server path, user identity, and optional iframe messaging.
Use this guide when you need the full reference setup (or to adapt it). For the quickest path, copy the snippet from the Publish page—see Quick start.
Setup flow
- Mobile viewport
- Embed container (
div) - Server base path
- User identity (optional, recommended)
- Embed script
- Iframe messaging (optional)
Step 1: Mobile viewport
Add this meta tag in the page <head> so the embed is responsive on phones:
<meta name="viewport" content="width=device-width, minimum-scale=1.0">
This sets the layout width to the device width at scale 1.0. Users can still pinch-zoom the iframe on touch devices.
Step 2: Place the embed container
Add a div in <body> where the puzzle or picker should appear.
- Single puzzle
- Picker
<div class="pm-embed-div" data-id="5fb0810d" data-set="interesting-puzzles" data-puzzletype="crossword"/>
| Attribute | Purpose |
|---|---|
data-id | Puzzle id |
data-set | Series code |
data-puzzletype | Puzzle type (e.g. crossword, sudoku) |
<div class="pm-embed-div" data-set="interesting-puzzles"/>
| Attribute | Purpose |
|---|---|
data-set | Series code for puzzles shown in the picker |
This div marks where the iframe is injected. Use the values from your Publish-page embed snippet.
Step 3: Set the server base path
Point PM_Config.PM_BasePath at the Amuse Labs server that serves your puzzles (from your Dashboard / Publish snippet), for example:
var PM_Config = {
PM_BasePath: "https://puzzleme.amuselabs.com/pmm"
}
Without the correct base path, the embed cannot load puzzles.
Step 4: User identity (optional)
Passing a stable user id lets PuzzleMe restore progress, support session mobility across devices, and produce reliable analytics. See User identity for why this matters, UID rules, and pitfalls (including double loads).
Browsers often block third-party cookies from the PuzzleMe iframe. A first-party id from your page avoids that.
Simplest option: if you already know the anonymized user id when the page renders, set it on the container:
<div class="pm-embed-div" data-set="interesting-puzzles" data-uid="abc123" ...></div>
Programmatic option: implement PM_Config.getUID so the embed script can resolve the id at load time. Hash the raw id (e.g. SHA-256) so PuzzleMe never receives personally identifiable information (PII).
Skip this step only if third-party iframe cookies are enough for your audience. For signed-in users or browsers that block those cookies, configure identity as below.
Never pass empty, 0, undefined, or null as a UID. If you cannot identify the user, omit the UID entirely. Details: User identity.
- Site has user login
- No login system
Pass your existing login identifier (hashed) into PuzzleMe.
- Include CryptoJS in
<head>(skip if you hash with another library):
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"
integrity="sha512-E8QSvWZ0eCLGk4km3hxSsNmGWbLtSCSUcewDQPQWZF6pEU8GlT8a5fF32wOl1i8ftdMhssTrF/OhyGWwonTcXA=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
- Add
getUIDunder your page script. SetLOGGED_UID_COOKIE_NAMEto the cookie your auth system uses for the user id.
JavaScript: restore state for logged-in users
PM_Config = { ...PM_Config,
getUID: function () {
var LOGGED_UID_COOKIE_NAME = "uid"; // Change it according to your page's cookie name
/**
* This function reads the cookie with the given name and returns the value.
*/
function readCookie(name) {
var nameEQ = name + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = trim(ca[i]);
if (c.indexOf(nameEQ) == 0)
return c.substring(nameEQ.length, c.length);
}
return null;
}
/**
* This function hashes the given string. You can override it with your own hashing function.
* The default method provided here uses the hash function from the CryptoJS library.
* For this to work the CryptoJS library should be included in the page with the following script tag before including this script.
* <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js" integrity="sha512-E8QSvWZ0eCLGk4km3hxSsNmGWbLtSCSUcewDQPQWZF6pEU8GlT8a5fF32wOl1i8ftdMhssTrF/OhyGWwonTcXA==" crossorigin="anonymous" referrerpolicy="no-referrer">
*/
function hash(str) {
if (typeof CryptoJS === 'undefined')
return str; // return original string if CryptoJS is not available.
else if (CryptoJS.SHA256)
return CryptoJS.SHA256(str).toString();
else
return str; // return original string if CryptoJS.SHA256 is not available.
}
return hash(readCookie(LOGGED_UID_COOKIE_NAME));
}
}
/**
* Helper functions to trim a string and encode string for URL
*/
var trim = function (s) {
if (typeof (s) !== 'string')
return s;
var c;
// trim leading
while (true) {
if (s.length === 0)
break;
c = s.charAt(0);
if (c !== '\n' && c !== '\t' && c !== ' ')
break;
s = s.substring(1);
}
// trim trailing
while (true) {
if (s.length === 0)
break;
c = s.charAt(s.length - 1);
if (c !== '\n' && c !== '\t' && c !== ' ')
break;
s = s.substring(0, s.length - 1);
}
return s;
};
Generate a random id, store it in a first-party cookie (default 365 days), and pass a hash to PuzzleMe.
- Include CryptoJS in
<head>(skip if you hash another way):
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"
integrity="sha512-E8QSvWZ0eCLGk4km3hxSsNmGWbLtSCSUcewDQPQWZF6pEU8GlT8a5fF32wOl1i8ftdMhssTrF/OhyGWwonTcXA=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
- Add
getUIDunder your page script. Rename the cookie viaLOGGED_UID_COOKIE_NAMEif you prefer.
JavaScript: restore state without login
PM_Config = { ...PM_Config,
getUID: function () {
var LOGGED_UID_COOKIE_NAME = "uid"; // Change it according to your page's cookie name
/**
* Default implementation to generate a unique id for the user.
*/
function genUUID() {
var dt = new Date().getTime();
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (dt + Math.random() * 16) % 16 | 0;
dt = Math.floor(dt / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
}
/**
* This function reads the cookie with the given name and returns the value.
*/
function readCookie(name) {
var nameEQ = name + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = trim(ca[i]);
if (c.indexOf(nameEQ) == 0)
return c.substring(nameEQ.length, c.length);
}
return null;
}
/**
* This function sets the cookie with the given name and value.
*/
function setCookie(name, value, days) {
var d = new Date();
d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toUTCString();
document.cookie = name + "=" + value + ";" + expires + ";path=/";
}
/**
* This function hashes the given string. override it with your own hashing function.
* Default method provided here uses the hash function from the CryptoJS library.
* For this to work the CryptoJS library should be included in the page with the following script tag before including this script.
* <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js" integrity="sha512-E8QSvWZ0eCLGk4km3hxSsNmGWbLtSCSUcewDQPQWZF6pEU8GlT8a5fF32wOl1i8ftdMhssTrF/OhyGWwonTcXA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
*/
function hash(str) {
if (typeof CryptoJS === 'undefined')
return str; // return original string if CryptoJS is not available.
else if (CryptoJS.SHA256)
return CryptoJS.SHA256(str).toString();
else
return str; // return original string if CryptoJS.SHA256 is not available.
}
/****************** Actual functionality of getUID starts from here ******************/
// read cookie for logged in user
var uid = readCookie(LOGGED_UID_COOKIE_NAME);
// if no cookie found then generate a random uid and store it in the cookie
if (!uid) {
// generate a new uid
var uuid = genUUID();
// set cookie
if (uuid != null && uuid != "")
setCookie(LOGGED_UID_COOKIE_NAME, uuid, 365);
return hash(uuid);
}
return hash(uid);
}
}
/**
* Helper functions to trim a string and encode string for URL
*/
var trim = function (s) {
if (typeof (s) !== 'string')
return s;
var c;
// trim leading
while (true) {
if (s.length === 0)
break;
c = s.charAt(0);
if (c !== '\n' && c !== '\t' && c !== ' ')
break;
s = s.substring(1);
}
// trim trailing
while (true) {
if (s.length === 0)
break;
c = s.charAt(s.length - 1);
if (c !== '\n' && c !== '\t' && c !== ' ')
break;
s = s.substring(0, s.length - 1);
}
return s;
};
Step 5: Run the embed script
This step reads the div from Step 2 and injects an iframe with the correct PuzzleMe URL.
Add the embedGame function in a <script> tag:
JavaScript: embed iframe
/**
* This method will iterate over the dom of the page to look for specific div tags and create an iframe for each of them based on the attributes
* of those tags.
*/
const PM_EMBED_DIV_CLASS= 'pm-embed-div'; // class name used to identify the container div under which the iframe will be embedded.
const PM_EMBED_DIV_SELECTOR= '.pm-embed-div'; // corresponding selector for the above class.
const PM_IFRAME_CLASS= 'pm-iframe'; // Class name assigned to the instantiated iframe.
const PM_IFRAME_CLASS_SELECTOR= '.pm-iframe'; // Class name assigned to the instantiated iframe.
function embedGame() {
// ****************************** Helper functions ****************************
/**
* Helper functions to trim a string and encode string for URL
*/
var trim = function (s) {
if (typeof (s) !== 'string')
return s;
var c;
// trim leading
while (true) {
if (s.length === 0)
break;
c = s.charAt(0);
if (c !== '\n' && c !== '\t' && c !== ' ')
break;
s = s.substring(1);
}
// trim trailing
while (true) {
if (s.length === 0)
break;
c = s.charAt(s.length - 1);
if (c !== '\n' && c !== '\t' && c !== ' ')
break;
s = s.substring(0, s.length - 1);
}
return s;
};
/**
* This function parses the parent page URL and returns the query params as a map.
* @param parentPageURL
*/
function extractParams(parentPageURL) {
// Extract the query params from the URL
var url = new URL(parentPageURL);
var params = new URLSearchParams(url.search);
var paramsMap = {};
params.forEach(function (value, key) {
paramsMap[key] = value;
});
// Return the query params as a map
return paramsMap;
}
/**
* For encoding URI component
*/
function fixedEncodeURIComponent(str) {
return encodeURIComponent(str).replace(/[!'()*]/g, function (c) {
return '%' + c.charCodeAt(0).toString(16);
});
}
/**
* get Configuration parameters (set, id, pageName, width, height) from the div tag or from the parent page URL. In case it is present in both the places
* then the one in the div tag takes precedence.
* @param paramName
* @param divTag
* @param parentPageParams
* returns the approrpiate field value else null if not found in either of the places. It also removes the paramName from parentPageParams if it is present there.
*/
function getConfigParam(paramName, divTag, parentPageParams, defaultVal) {
if (divTag.hasAttribute('data-' + paramName)) {
var val = divTag.getAttribute('data-' + paramName);
// remove if it is present in parentPageParams.
delete parentPageParams[paramName];
return val;
}
else if (parentPageParams[paramName]) {
var val = parentPageParams[paramName];
delete parentPageParams[paramName];
return val;
}
else
return defaultVal ? defaultVal : null;
}
/**
* Get the base name of the server from where the iframe will fetch the puzzle. This is computed based on the location of src tag of this script.
*/
function getServerBaseName(withApp) {
// Find the script tag which loaded this script. We have ensured that the script tag has id as 'pm-script'.
var src = '';
if (PM_Config.PM_BasePath == "") {
if (console)
console.log("Unable to find the hostname and app name of amuselabs.com which will serve the puzzle. Contact support@amuselabs.com");
return '';
}
src = PM_Config.PM_BasePath;
// here src is of the form 'https://staging.amuselabs.com/pmm'
// We need to extract base URL and the app name from this URL. For this create URL object from this string and extract the protocol, host and path name fields.
var url = new URL(src);
if (withApp) {
// url.pathname is everything followed by staging.amuselabs.com. To get the app name we split it by '/' and take the second element.
return url.protocol + "//" + url.host + "/" + url.pathname.split('/')[1];
}
else
return url.protocol + "//" + url.host;
}
// Construct iframe url based on parameters.
function getIframeURL(iframeInfo) {
var serverBaseName = getServerBaseName(true);
if (serverBaseName == null || serverBaseName === '') {
return null;
}
// pageName, puzzleEmbedParams and puzzleSet all three should be non-null when control reaches here. We ensure this in the calling function itself.
// if set is null then we don't proceed. pageName is set to picker by default. puzzleEmbedParams is set to 'embed=1' by default.
// puzzleEmbedParams can have multiple params separated by & so each of them should be encoded separately.
var iframeURL = serverBaseName + '/' + iframeInfo.pageName + '?' + '&set=' + fixedEncodeURIComponent(iframeInfo.puzzleSet);
// Now add the puzzleEmbedParams to the iframe url.
new URLSearchParams(iframeInfo.puzzleEmbedParams).forEach(function (value, key) {
iframeURL += '&' + key + '=' + fixedEncodeURIComponent(value);
});
// Now add other parameters which were passed explicity either by data-attribute in the div tag or by the parent page url.
if (iframeInfo.pickerStyle != null && iframeInfo.pickerStyle !== '' && iframeInfo.pickerStyle !== '0')
iframeURL += '&style=' + fixedEncodeURIComponent(iframeInfo.pickerStyle);
if (iframeInfo.puzzleId != null && iframeInfo.puzzleId !== '')
iframeURL += '&id=' + fixedEncodeURIComponent(iframeInfo.puzzleId);
if (iframeInfo.src != null && iframeInfo.src !== '')
iframeURL += '&src=' + fixedEncodeURIComponent(iframeInfo.src);
if (iframeInfo.uid !== null && iframeInfo.uid !== '')
iframeURL += '&uid=' + fixedEncodeURIComponent(iframeInfo.uid);
if (iframeInfo.puzzlePlayId !== null && iframeInfo.puzzlePlayId !== '')
iframeURL += '&playId=' + fixedEncodeURIComponent(iframeInfo.puzzlePlayId);
// if parentPagePath is not null then add it to the iframe url as src parameter. This is required for the iframe to
// construct social play URL correctly.
if (iframeInfo.parentPagePath !== null && iframeInfo.parentPagePath !== '')
iframeURL += '&src=' + fixedEncodeURIComponent(iframeInfo.parentPagePath);
// append all the remaining params from parentPage url and their values to the iframe URL
// Note that if any of these params in topURL were extracted to define puzzleId, src, uid and puzzlePlayId fields then they were already removed from the parentPageParams
// to avoid them adding again in the url. This was done in getConfigurationParams by the calling function.
for (var param in iframeInfo.parentPageParams) {
if (tmpParentPageParams.hasOwnProperty(param)) {
iframeURL += '&' + param + '=' + fixedEncodeURIComponent(iframeInfo.parentPageParams[param]);
}
}
return iframeURL;
}
// *************************** Function embedGame starts from here ******************************/
// Get top url of the page.
var parentPageBasePath = window.location.href;
// Get the path of the parent page.
var parentPagePath = '';
// if parentPageBasePath has query params then remove them.
if (parentPageBasePath.indexOf('?') !== -1) {
parentPagePath = trim(parentPageBasePath.substring(0, parentPageBasePath.indexOf('?')));
} else
parentPagePath = trim(parentPageBasePath);
// Get the query params of the parent page.
var parentPageParams = extractParams(parentPageBasePath);
// Now iterate over all div tags in the page which have class as 'pm-embed-iframe'.
// create iframe src url by reading the properties from theses div tags and query params.
// Create iframe element and append it to the div tag.
var divs = document.getElementsByClassName(PM_EMBED_DIV_CLASS);
for (var i = 0; i < divs.length; i++) {
var parentDiv = divs[i];
var tmpParentPageParams = JSON.parse(JSON.stringify(parentPageParams)); // create deep copy
var puzzleId = getConfigParam('id', parentDiv, tmpParentPageParams);
var puzzleSet = getConfigParam('set', parentDiv, tmpParentPageParams);
if (puzzleSet == null) {
// we can't construct the iframe url so continue with log message in console.
if (console)
console.log('PuzzleMe: Unable to construct iframe url because set was not specified. Please check the configuration parameters.');
continue;
}
var iframeTitle = getConfigParam('iframetitle', parentDiv, tmpParentPageParams);
var iframeName = getConfigParam('iframename', parentDiv, tmpParentPageParams);
var pickerStyle = getConfigParam('style', parentDiv, tmpParentPageParams);
var puzzlePlayId = getConfigParam('playId', parentDiv, tmpParentPageParams);
var src = getConfigParam('src', parentDiv, tmpParentPageParams);
var puzzleEmbedParams = getConfigParam('embedParams', parentDiv, tmpParentPageParams, 'embed=1');
var Userid = getConfigParam('uid', parentDiv, tmpParentPageParams);
// If userid is not specified either as data attribute in the div tag or as a query param in the parent page url then
// get the userid from the custom implementation of getUid() method if provided
if (Userid == null && PM_Config.getUID != null && typeof PM_Config.getUID === 'function') {
Userid = PM_Config.getUID();
}
// Create a new iframe element.
var iframe = document.createElement('iframe');
// Add the iframe to this div.
parentDiv.appendChild(iframe);
// set the name of the iframe as set.
iframe.setAttribute('name', puzzleSet);
// set the class name of this iframe as PM_IFRAME_CLASS. So that this iframe can be identified later and
// we can change its attributes like height, width etc.
iframe.setAttribute('class', PM_IFRAME_CLASS);
// set the height, width and other style attributes of the iframe.
{
var puzzleHeight = getConfigParam('height', parentDiv, tmpParentPageParams, '700px');
var puzzleWidth = getConfigParam('width', parentDiv, tmpParentPageParams, '100%');
var fixedStyle = "border:none; width: 100% !important; position: static; display: block !important! margin: 0! important;"; // This was taken from the existing embed code url from preview and publish page.
iframe.setAttribute('allow', 'web-share; fullscreen');
iframe.setAttribute('style', fixedStyle);
iframe.height = puzzleHeight;
iframe.width = puzzleWidth;
}
var pageName = 'date-picker';
// set Pagename for URL
{
var puzzleType = getConfigParam('puzzleType', parentDiv, tmpParentPageParams);
var puzzlePage = getConfigParam('page', parentDiv, tmpParentPageParams);
if (puzzlePage != null) {
// Give preference to puzzlePage field irrespeective of puzzleType field being null or not.
pageName = puzzlePage;
} else if (puzzleType != null) {
pageName = puzzleType;
}
// if both are null then take the default value as 'date-picker'
}
// construct the iframe url.
var iframeURL = getIframeURL({
pageName: pageName, puzzleId: puzzleId, puzzleSet: puzzleSet, pickerStyle: pickerStyle,
puzzleEmbedParams: puzzleEmbedParams, parentPageParams: tmpParentPageParams,
src: src, uid: Userid, puzzlePlayId: puzzlePlayId, parentPagePath: parentPagePath
});
if (iframeURL == null) {
if (console)
console.log("Unable to construct the iframe URL for loading puzzle/picker. Please contact support@amuselabs.com for help.");
}
// Set title and name of iframe and load the iframe with iframeURL
if (iframe && iframeURL) {
if (iframeTitle != null && iframeTitle !== '') {
iframe.setAttribute('title', iframeTitle);
}
if (iframeName != null && iframeName !== '') {
iframe.setAttribute('name', iframeName);
}
if (-1 == navigator.userAgent.indexOf("MSIE")) {
iframe.setAttribute('src', iframeURL);
} else {
iframe.setAttribute('location', iframeURL);
}
}
}
}
Call it when the page has loaded:
/**
* Trigger the loading of the puzzle. This is called when the page is loaded.
*/
window.addEventListener('load', function () {
embedGame();
});
Step 6: Iframe messaging (optional)
The puzzle/picker iframe talks to your page with postMessage. Common uses:
- Resize the iframe to fit content
- Scroll the puzzle into view on first tap
- Read metadata, progress, or completion for analytics
Full event list and payloads: Iframe communication.
Hosted JS embed already handles resize and scroll-into-view. Add the listeners below when you need custom behavior or are using this reference setup.
Listen for iframe events
Add an event listener that validates event.origin and forwards messages to your handlers.
JavaScript: receive iframe events
/**
* add event listener for receiving messages from iframe
*/
window.addEventListener("message", receiveMessage, !1);
function receiveMessage(event) {
var PUZZLE_HOST = getServerBaseName(false); //the site name where puzzles are hosted, e.g. cdnx.amuselabs.com
if (PUZZLE_HOST == null) // don't listen to messages unless the source is same as the host that is serving the puzzles.
return;
try {
if (PUZZLE_HOST === event.origin) {
var origin = event.origin;
var data;
if (event.data) {
data = JSON.parse(event.data);
}
if (data) {
if (data.src === 'picker') { // implies that the message is from picker page inside iframe
if ('frameHeight' in data) { // implies that the puzzles in the picker are fully displayed.
if (onPickerDisplayOrResize)
onPickerDisplayOrResize(data); // forward the data to the passed handler if provided by the user.
} else { // implies that the picker is loaded
if (onPickerLoad) // forward to the passed handler if provided by the user.
onPickerLoad(data);
}
} else if (data.src === 'crossword') { // implies that the message is from puzzle page inside iframe
if ('frameHeight' in data) { // implies that the puzzle is completely rendered on the screen
if (onPuzzleDisplayOrResize) // forward to the passed handler if provided by the user.
onPuzzleDisplayOrResize(data);
}
if ('progress' in data) {
if (data.progress === 'puzzleLoaded') { // implies that the puzzle is loaded
if (onPuzzleLoad) // forward to the passed handler if provided by the user.
onPuzzleLoad(data);
} else if (data.progress === 'puzzleCompleted') { // implies that the puzzles is completed
if (onPuzzleComplete) // forward to the passed handler if provided by the user.
onPuzzleComplete(data);
}
}
if ('type' in data && data.type === 'event') { // implies that the puzzle is clicked
if (onPuzzleClick) // forward to the passed handler if provided by the user.
onPuzzleClick(data);
}
}
}
}
} catch (error) {
if (console)
console.log('PuzzleMe embed error: ' + error);
}
}
Example handlers
Wire only the handlers you need. Names must match what receiveMessage calls above. Message shapes: Iframe communication.
Picker displayed or resized — resize the iframe to the picker height:
function onPickerDisplayOrResize(data) {
updateHeight(data);
}
function updateHeight(data) {
if ('frameHeight' in data) {
document.querySelectorAll(PM_IFRAME_CLASS_SELECTOR).forEach(function (iframe) {
if (iframe instanceof HTMLIFrameElement) {
iframe.height = data.frameHeight + 'px';
}
});
}
}
Puzzle loaded — use metadata on your page (title, author, etc.):
function onPuzzleLoad(data) {
// e.g. update page heading from data.title
}
Puzzle displayed or resized — keep iframe height in sync:
function onPuzzleDisplayOrResize(data) {
updateHeight(data);
}
Puzzle click — scroll the puzzle into view (especially useful on mobile):
function onPuzzleClick(data) {
scrollPageToGetView(data);
}
function scrollPageToGetView(data) {
var offset = (data.gridOffset) ? data.gridOffset : 0;
var puzzleContainer = document.querySelector(PM_EMBED_DIV_SELECTOR);
if (puzzleContainer) {
var rect = puzzleContainer.getBoundingClientRect();
var scrollTop = rect.top + window.scrollY + offset;
window.scrollTo({ top: scrollTop, behavior: 'smooth' });
}
}
Puzzle completed — log score/time or trigger on-page actions:
function onPuzzleComplete(data) {
// e.g. data.timeTaken, data.score
}
Next steps
- User identity — UID rules and best practices
- Iframe communication — full postMessage reference
- Iframe dimensions — sizing guidance
- Custom parameters — tweak player behavior via URL