The Problem
In a Nuxt 3 project, I was using Universal Viewer (UV) embedded via iframe to display IIIF manifest images. The iframe source pointed to the external https://universalviewer.io/uv.html, but at some point the viewer stopped rendering entirely.
The browser console showed these errors:
SES Removing unpermitted intrinsics
UV.js:2 Unknown content type
Investigation
universalviewer.io Redirect
The first finding was that universalviewer.io now redirects to universalviewer.dev:
<meta http-equiv="refresh" content="0; url=https://universalviewer.dev/uv.html">
Testing the same manifest URL on universalviewer.dev/uv.html produced the identical “Unknown content type” error. The issue was reproducible on the official site itself.
Two Initialization Modes in UV 4.x
UV 4.x ships with two HTML files:
uv.html: Designed for iframe embedding. Initializes withIIIFURLAdapter(true)(embedded mode).index.html: Demo page. Initializes withIIIFURLAdapter()(normal mode), explicitly passingiiifManifestId.
The embedded mode in uv.html fails to determine the content type when loading IIIF Presentation API 2.0 manifests, producing the “Unknown content type” error.
The demo page initialization, however, works correctly. This was confirmed on the Netlify-deployed instance:
https://uv-v4.netlify.app/#?manifest=https://kokusho.nijl.ac.jp/biblio/200017711/manifest&cv=80
URL Parameter Format
Another difference was how URL parameters are passed:
uv.html(embed):?manifest=...#?cv=...(query parameter + hash)index.html(demo):#?manifest=...&cv=...(hash parameters only)
The working format uses hash parameters exclusively.
Solution
1. Host UV 4.2.1 Locally
I extracted the necessary files from the npm package into public/uv/:
npm pack universalviewer@4.2.1
tar xzf universalviewer-4.2.1.tgz
Only four items are needed:
public/uv/
├── umd/ # UV core + chunk JS files (~190 files)
├── uv.css # Stylesheet
├── uv.html # Embed page (customized)
└── uv-iiif-config.json # IIIF configuration
The cjs/, esm/ directories, demo index.html, and collection JSON files can be removed.
2. Custom Embed Page
I rewrote uv.html to use the same initialization pattern as the working demo page:
<script>
document.addEventListener("DOMContentLoaded", function () {
var urlAdapter = new UV.IIIFURLAdapter();
var iiifManifestId =
urlAdapter.get("iiif-content") ||
urlAdapter.get("manifest") ||
urlAdapter.get("iiifManifestId");
var canvasIndex = urlAdapter.get("cv") || 0;
var data = urlAdapter.getInitialData({
iiifManifestId: iiifManifestId,
canvasIndex: Number(canvasIndex),
});
var uv = UV.init("uv", data);
urlAdapter.bindTo(uv);
uv.on("configure", function ({ config, cb }) {
cb(
fetch("uv-iiif-config.json").then(function (response) {
return response.json();
})
);
});
var $UV = document.getElementById("uv");
function resize() {
var w = window.innerWidth;
var h = window.innerHeight;
$UV.style.width = w + "px";
$UV.style.height = h + "px";
if (uv && uv.resize) {
uv.resize();
}
}
addEventListener("resize", resize);
resize();
});
</script>
Key changes:
IIIFURLAdapter(true)→IIIFURLAdapter()(disable embedded mode)getInitialData({embedded: true})→getInitialData({iiifManifestId, canvasIndex})(pass manifest explicitly)- Added a
configurehandler to loaduv-iiif-config.json - Call
uv.resize()on window resize
3. Update iframe URL Format
On the Vue component side, I switched to hash-parameter URLs:
// Before
`https://universalviewer.io/uv.html?manifest=${field.manifest}#?cv=${cv}`
// After
`/uv/uv.html#?manifest=${field.manifest}&cv=${cv}`
Notes
- “SES Removing unpermitted intrinsics” is a warning from UV’s internal security mechanism (SES/Hardened JavaScript) and does not affect functionality.
- The
umd/directory contains approximately 190 chunk JS files.UV.jsalone is not sufficient — these chunks are loaded on demand. - The manifests tested in this case use IIIF Presentation API 2.0 format (
sc:Manifest). Behavior with API 3.0 manifests has not been verified.