diff --git a/3p/vendors/neuwo.js b/3p/vendors/neuwo.js
new file mode 100644
index 000000000000..6b845c0fe225
--- /dev/null
+++ b/3p/vendors/neuwo.js
@@ -0,0 +1,12 @@
+// src/polyfills.js must be the first import.
+import '#3p/polyfills';
+
+import {register} from '#3p/3p';
+import {draw3p, init} from '#3p/integration-lib';
+
+import {neuwo} from '#ads/vendors/neuwo';
+
+init(window);
+register('neuwo', neuwo);
+
+window.draw3p = draw3p;
diff --git a/ads/_config.js b/ads/_config.js
index 1070c19e7c8f..d38e102a83dd 100755
--- a/ads/_config.js
+++ b/ads/_config.js
@@ -976,6 +976,10 @@ const adConfig = jsonConfiguration({
renderStartImplemented: true,
},
+ 'neuwo': {
+ renderStartImplemented: true,
+ },
+
'noddus': {
prefetch: 'https://noddus.com/amp_loader.js',
renderStartImplemented: true,
diff --git a/ads/vendors/neuwo.js b/ads/vendors/neuwo.js
new file mode 100644
index 000000000000..f69aad421908
--- /dev/null
+++ b/ads/vendors/neuwo.js
@@ -0,0 +1,23 @@
+import {validateData, writeScript} from '#3p/3p';
+
+/**
+ * Neuwo Monetisation Platform.
+ * @param {!Window} global
+ * @param {!Object} data
+ */
+export function neuwo(global, data) {
+ validateData(data, ['scriptid'], []);
+
+ // The Neuwo SDK dispatches these custom events on the 3p-iframe window when a
+ // placement renders (fill) or has no fill. Bridge them to the AMP ad
+ // lifecycle so AMP can start rendering / fall back to the next slot.
+ global.addEventListener('neuwoAdRender', () => global.context.renderStart());
+ global.addEventListener('neuwoAdNoFill', () =>
+ global.context.noContentAvailable()
+ );
+
+ writeScript(
+ global,
+ 'https://ads.neuwo.ai/loader/neuwo-ad.js?staticTagId=' + data['scriptid']
+ );
+}
diff --git a/ads/vendors/neuwo.md b/ads/vendors/neuwo.md
new file mode 100644
index 000000000000..5e1d10b82c08
--- /dev/null
+++ b/ads/vendors/neuwo.md
@@ -0,0 +1,19 @@
+# Neuwo
+
+Neuwo Monetisation Platform.
+
+## Example
+
+```html
+
+```
+
+## Configuration
+
+You need to get a `scriptid` (static tag ID) from your Neuwo account manager.
+
+For testing purposes you can use `data-scriptid="91"`.
+
+### Required parameters
+
+- `data-scriptid`
diff --git a/examples/amp-ad/ads.amp.esm.html b/examples/amp-ad/ads.amp.esm.html
index 9a2965878b84..b8c3bd322912 100644
--- a/examples/amp-ad/ads.amp.esm.html
+++ b/examples/amp-ad/ads.amp.esm.html
@@ -206,6 +206,7 @@
+
@@ -1642,6 +1643,10 @@
NETLETIX
data-nxheight='250'>
+ Neuwo
+
+
+
Noddus
navegg
+
@@ -1486,6 +1487,10 @@ NETLETIX
data-nxunit='/60343726/netzathleten_.de' data-nxwidth='300' data-nxheight='250'>
+ Neuwo
+
+
+
Noddus
{
+ let sandbox;
+ let win;
+
+ beforeEach(() => {
+ sandbox = env.sandbox;
+
+ return createIframePromise(true).then((iframe) => {
+ // Simulate the iframe that neuwo will be called inside.
+ win = iframe.win;
+ win.context = {
+ renderStart: sandbox.spy(),
+ noContentAvailable: sandbox.spy(),
+ referrer: null,
+ };
+ });
+ });
+
+ it('should require the scriptid parameter', () => {
+ allowConsoleError(() => {
+ expect(() => neuwo(win, {})).to.throw(/scriptid/);
+ });
+ });
+
+ it('should load the Neuwo static loader with the given scriptid', () => {
+ const writeScript = sandbox.stub(_3p, 'writeScript');
+ neuwo(win, {'scriptid': '91'});
+ expect(writeScript).to.be.calledOnce;
+ expect(writeScript.firstCall.args[1]).to.contain(
+ 'ads.neuwo.ai/loader/neuwo-ad.js?staticTagId=91'
+ );
+ });
+
+ it('should bridge neuwoAdRender to renderStart()', () => {
+ sandbox.stub(_3p, 'writeScript');
+ neuwo(win, {'scriptid': '91'});
+ win.dispatchEvent(new win.Event('neuwoAdRender'));
+ expect(win.context.renderStart).to.be.calledOnce;
+ });
+
+ it('should bridge neuwoAdNoFill to noContentAvailable()', () => {
+ sandbox.stub(_3p, 'writeScript');
+ neuwo(win, {'scriptid': '91'});
+ win.dispatchEvent(new win.Event('neuwoAdNoFill'));
+ expect(win.context.noContentAvailable).to.be.calledOnce;
+ });
+});