# Vev Developer > Building no-code with code import Table from '../../components/Table'; ## Webhook Integration The Vev webhook is the recommended approach for enterprise-level self-hosting. Instead of using embed scripts or ZIP downloads, the webhook delivers your project's HTML, CSS, and JavaScript directly to your server on every publish. This gives you full control over rendering, hosting, and caching — resulting in the best possible performance for SEO, LLMs, and page load times. ### Why use the webhook? | Approach | SEO | Load time | Self-hosted | Automation | | ------------ | -------------------- | ----------------- | ------------ | -------------------- | | **Webhook** | Server-rendered HTML | Fastest | Full control | Automatic on publish | | Embed script | Client-rendered | Slower (extra JS) | Partial | Manual | | ZIP download | Depends | Depends | Full control | Manual | The webhook sends a `POST` request to your endpoint every time a project is published. Your server receives the complete page content and can serve it directly — no client-side rendering required. Search engines, LLMs, and performance tools see fully rendered HTML on first load. ### How it works 1. **Configure** a webhook destination in the Vev editor (Publishing > Add destination > Webhook) 2. **Publish** your project from the editor 3. **Vev sends** a `POST` request to your endpoint with the full page content 4. **Your server** stores the HTML and serves it to visitors ``` Vev Editor → Publish → POST to your endpoint → Your server stores & serves content ``` ### Setting up the webhook In the Vev editor, go to **Publishing** and add a new **Webhook** destination. You will need to configure: * **Webhook URL** — Your endpoint that accepts `POST` requests (must be publicly accessible) * **Authentication** — Optional security for your endpoint (Basic Auth, Bearer Token, or none) #### Webhook options | Option | Description | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Send full page** | When enabled, the payload includes complete HTML documents with ``, ``, and `` tags, styles, and scripts. When disabled, only the body content is sent — useful when you want to inject Vev content into your own page template. | | **Send plugin settings** | Include configuration data for any Vev plugins/integrations used in the project. | | **Content as links** | Instead of including HTML inline in the payload, Vev provides temporary download URLs. Useful for very large pages. | | **Include asset URLs** | All asset URLs (images, fonts, etc.) are listed in the payload with paths rewritten for local hosting. This is the key option for full self-hosting. | | **Custom assets folder** | Set the folder name for locally hosted assets (defaults to `assets`). | | **Fix og:image path** | Converts relative `og:image` meta tag paths to absolute URLs for proper social media sharing. | ### Payload structure Every webhook request is a JSON `POST` with this structure: ```json { "id": "unique-event-id", "hosting": "hosting-key", "event": "PUBLISH", "payload": { "projectId": "abc123", "projectTitle": "My Project", "version": 42, "dir": "/my-project", "pages": [...], "css": [...], "js": [...], "other": [...] } } ``` #### Events The webhook sends three types of events: #### Pages Each page in the `pages` array contains: ```json { "key": "page-key", "title": "Page Title", "path": "/about", "html": "
...full page content...
", "index": true } ``` When **Send full page** is enabled, the `html` field contains a complete HTML document: ```html Page Title
...page content...
``` When disabled, only the inner body content is provided — giving you full control over the surrounding HTML structure. When **Content as links** is enabled, the `html` field is replaced with a `downloadUrl` pointing to a temporary storage location where the HTML can be fetched. #### CSS and JavaScript External stylesheets and scripts are listed separately: ```json { "css": [ { "url": "https://cdn.vev.design/...", "contentType": "text/css" } ], "js": [ { "url": "https://cdn.vev.design/...", "contentType": "application/javascript" } ] } ``` #### Assets (self-hosting mode) When **Include asset URLs** is enabled, the payload includes: * `assets` — An array of URLs for all images, fonts, and other assets used by the project * `assetsFolder` — The folder name assets are mapped to (default: `assets`) * `embedScripts` — Local embed script paths with download URLs * All asset references in the HTML and CSS are rewritten to use relative paths pointing to the assets folder ```json { "assets": [ "https://storage.googleapis.com/.../assets/image1.webp", "https://storage.googleapis.com/.../assets/style.css" ], "assetsFolder": "assets", "embedScripts": [ { "downloadUrl": "https://storage.googleapis.com/.../embed.js", "localPath": "embed.js" } ] } ``` ### Implementation guide #### Option 1: Full page with Vev CDN (simplest) Use the HTML as a complete page and let Vev's CDN (powered by Cloudflare) serve the CSS, JS, and assets. Best for getting started quickly. **Webhook settings:** * Send full page: **enabled** * Include asset URLs: **disabled** ```js // Express.js example app.post('/vev-webhook', express.json({ limit: '50mb' }), (req, res) => { const { event, payload } = req.body; if (event === 'PUBLISH') { for (const page of payload.pages) { // Store the full HTML — assets load from Vev CDN const filePath = page.index ? 'index.html' : `${page.path}.html`; fs.writeFileSync(`./public/${filePath}`, page.html); } } if (event === 'UNPUBLISH') { // Remove published files } res.json({ received: true }); }); ``` #### Option 2: Body content injected into your template Receive only the body content and inject it into your existing page layout. This is ideal when Vev content is part of a larger page with your own header, footer, and navigation. **Webhook settings:** * Send full page: **disabled** * Include asset URLs: **disabled** ```js app.post('/vev-webhook', express.json({ limit: '50mb' }), (req, res) => { const { event, payload } = req.body; if (event === 'PUBLISH') { for (const page of payload.pages) { // Store body content to be injected into your template at render time db.upsert('vev_pages', { path: page.path, html: page.html, title: page.title, projectId: payload.projectId, version: payload.version, }); } // Store CSS and JS references db.upsert('vev_assets', { projectId: payload.projectId, css: payload.css.map(f => f.url), js: payload.js.map(f => f.url), }); } res.json({ received: true }); }); // Serve the page with your own template app.get('/landing/:slug', (req, res) => { const page = db.get('vev_pages', { path: `/${req.params.slug}` }); const assets = db.get('vev_assets', { projectId: page.projectId }); res.send(` ${page.title} — My Site ${assets.css.map(url => ``).join('\n')}
${page.html}
${assets.js.map(url => ``).join('\n')} `); }); ``` #### Option 3: Fully self-hosted (recommended for enterprise) Download all assets and serve everything from your own infrastructure. This gives you complete control, independence from any external CDN, and the best performance through your own caching strategy. **Webhook settings:** * Send full page: **enabled** (or disabled if injecting into a template) * Include asset URLs: **enabled** * Custom assets folder: set to your preferred path (e.g., `static/vev`) ```js const fetch = require('node-fetch'); const path = require('path'); app.post('/vev-webhook', express.json({ limit: '50mb' }), async (req, res) => { // Respond quickly — process in background res.json({ received: true }); const { event, payload } = req.body; if (event !== 'PUBLISH') return; const assetsDir = `./public/${payload.assetsFolder || 'assets'}`; fs.mkdirSync(assetsDir, { recursive: true }); // 1. Download all assets to your server for (const assetUrl of payload.assets || []) { const fileName = path.basename(new URL(assetUrl).pathname); const response = await fetch(assetUrl); const buffer = await response.buffer(); fs.writeFileSync(path.join(assetsDir, fileName), buffer); } // 2. Download embed scripts for (const script of payload.embedScripts || []) { const response = await fetch(script.downloadUrl); const content = await response.text(); fs.writeFileSync(`./public/${script.localPath}`, content); } // 3. Store HTML pages (asset paths are already rewritten to local paths) for (const page of payload.pages) { const html = page.html || await fetch(page.downloadUrl).then(r => r.text()); const filePath = page.index ? 'index.html' : `${page.path}.html`; fs.writeFileSync(`./public/${filePath}`, html); } }); ``` In this mode, Vev rewrites all asset URLs in the HTML and CSS to point to your local assets folder. No requests go to Vev's CDN at runtime. ### Verifying webhook requests Every webhook request includes an `X-Vev-Signature` header containing an HMAC-SHA512 signature of the request body, signed with your webhook secret. ```js const crypto = require('crypto'); function verifySignature(req, secret) { const signature = req.headers['x-vev-signature']; if (!signature) return false; const hmac = crypto.createHmac('sha512', secret); const expected = 'sha512=' + hmac.update(JSON.stringify(req.body)).digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } app.post('/vev-webhook', express.json({ limit: '50mb' }), (req, res) => { if (!verifySignature(req, process.env.VEV_WEBHOOK_SECRET)) { return res.status(401).json({ error: 'Invalid signature' }); } // Process webhook... }); ``` The request also includes an `X-Vev-Hosting` header with the hosting key, which can be used to identify the source destination. ### Headers reference
### Best practices * **Respond quickly** — Return a `2xx` response as fast as possible. Process heavy work (downloading assets, writing files) asynchronously after responding. * **Handle idempotency** — Use the `version` field to avoid processing duplicate publishes. Store the latest version and skip older ones. * **Set cache headers** — When self-hosting assets, configure appropriate `Cache-Control` headers. Vev assets are immutable per version, so long cache lifetimes are safe. * **Use full page for SEO** — The full page mode includes all meta tags, Open Graph tags, and structured data configured in the editor. This gives search engines and LLMs the best possible content to index. * **Monitor your endpoint** — The Vev editor shows the HTTP response status from your webhook. Use this to debug integration issues. * **Size limits** — For projects with many pages or large assets, enable **Content as links** to keep the payload size manageable. Your server then downloads the content from temporary URLs. import Table from '../../components/Table'; ## Tracking and Analytics [Vev](https://vev.design) takes an agnostic approach to analytics and *does not* directly track visitors on Vev-created content or pages. However, we offer tools and methods to help you implement your preferred tracking solutions. This can be done through our [hosting integrations](https://www.vev.design/features/integrations/) or by using *event tracking* as described in this article. :::info [Vev's privacy policy](https://www.vev.design/legal/privacy-policy/): Vev does not have a direct relationship with Customers’ End Users. A “Customer End User” is an individual that provides their Personal Information to our Customers. We do not control the purposes nor the means by which this Personal Information is collected, and we are not in a direct relationship with Customer End Users. As described in. ::: ### Integration To implement event-based tracking, you need to listen for all Vev tracking events. This is done by listening for the `vev.track` event on the `window` object. Once captured, you can forward the event data to your preferred tracking tool, internal data repository, or any similar system. ```js window.addEventListener('vev.track', (e) => { const event = e.detail; console.log('event', event); }); ``` Example: Adobe Analytics Implementation ```js window.addEventListener('vev.track', (e) => { const event = e.detail.data; // Example: Send to Adobe Analytics alloy('sendEvent', { type: event.type, data: { ...event.data, vevProjectKey: event.metaData.projectKey, vevPageKey: event.metaData.pageKey, }, }); }); ``` #### Event data structure Vev offers two methods for creating tracking events: standard events and interaction-based event tracking. The `data` object is unique to each event type, while `metaData` remains consistent across all events.

Specific data for the event.

], [ { name: 'metaData', type: 'object' }, () => ( <> projectKey string
pageKey string
timestamp number
), ], ]} /> ### Standard events
( <> projectKey string
projectName string
breakpoint string ), ], [ { name: 'VEV_PAGE_LOAD', type: 'string' }, 'Dispatches when the page load', () => ( <> pageKey string
pageName string
projectKey string
breakpoint string ), ], [ { name: 'VEV_LINK_CLICK', type: 'string' }, 'Dispatches when an external link is clicked', () => ( <> url string
), ], [ { name: 'VEV_VIDEO_PLAY', type: 'string' }, 'Dispatches when an video is started', () => ( <> videoUrl string
totalPlayTime number (seconds)
percentagePlayed number (percentage)
), ], [ { name: 'VEV_VIDEO_STOP', type: 'string' }, 'Dispatches when an video is stopped or paused', () => ( <> videoUrl string
totalPlayTime number (seconds)
percentagePlayed number (percentage)
), ], [ { name: 'VEV_VIDEO_END', type: 'string' }, 'Dispatches when an video is fully played.', () => ( <> videoUrl string
totalPlayTime number (seconds)
percentagePlayed number (percentage)
), ], [ { name: 'VEV_VIDEO_PROGRESS', type: 'string' }, 'Dispatches the progress of a video in seconds.', () => ( <> videoUrl string
videoName string
progress number (seconds)
totalPlayTime number (seconds)
percentagePlayed number (percentage)
), ], ]} /> ### Custom events Custom events can be added using [interactions](https://help.vev.design/en/articles/8449049-how-to-use-interactions). For these events, the `type` and `data` are defined by the designer within the design editor. Custom events can be linked to various interaction triggers, such as `onVisible`, `onScroll`, and more. They are typically used for tracking call-to-action (CTA) interactions. ![interactionTracking](.//assets/interaction-tracking.png) ## Components ### Image The `Image` component can be used with the `type: image` schema field. #### Example ```jsx import React from 'react'; import { registerVevComponent, Image } from '@vev/react'; const MyComponent = (props) => ; registerVevComponent(MyComponent, { name: 'My component', props: [ { name: 'image', type: 'image', }, ], }); export default MyComponent; ``` #### Interface ```ts type Props = { className?: string; sizes?: [number, number][]; src?: string | { key: string }; style?: { [attr: string]: number | string }; }; ``` ### Link The `Link` component can be used together with the `type: link` schema field. ##### Example ```jsx import React from 'react'; import { registerVevComponent, Link } from '@vev/react'; const MyComponent = (props) => ; registerVevComponent(MyComponent, { name: 'My component', props: [ { name: 'link', type: 'link', }, ], }); export default MyComponent; ``` #### Interface ```ts type Props = { /** * Preset for what linkType the field should be. * 0: Page, 1: Element, 2: External link, 3: Email, 4: Phone. */ mode: 0 | 2 | 3 | 4; href?: string; page?: string; target?: boolean; phone?: string; email?: string; }; ``` ### Render The `Render` component is used to render a [Vev](https://www.vev.design/) project in your React application. The component supports [server side rendering](https://web.dev/rendering-on-the-web/) using [Suspense](https://react.dev/reference/react/Suspense), which means it will deliver static HTML on the server (if the app is built for server side rendering using Suspense) and hydrate on the client. This is also compatible with [NextJS](https://nextjs.org/) if you are using the [app routing feature](https://nextjs.org/docs/app). :::caution To use this component with [NextJS](https://nextjs.org/) you have to [opt-in to the app directory routing](https://nextjs.org/docs/app/getting-started/installation). ::: This component will also work in a traditional React application, but the component will only render client side. ##### Example ```jsx import React from 'react'; import { Render as VevRender } from '@vev/react'; const MyComponent = (props) => { return ; }; export default MyComponent; ``` :::info To find the `projectKey` and `pageKey` look at your [Vev Editor](https://editor.vev.design) URL: ``` https://editor.vev.design/edit/[projectKey]/[pageKey] ``` ::: #### Interface ```ts type Props = { projectKey: string; pageKey: string; noCache: boolean; fallback: React.ReactNode; // Render a fallback component when loading }; ``` ## Hooks These hooks offer different way to interact with the Vev Editor. ### useIntersection Tracks the portion of an element that intersects with the visible area of the screen (the viewport). ```ts const el_intersection: false | IntersectionObserverEntry = useIntersection(ref: React.RefObject, options?: IObserverOptions); ``` ##### Interface ```ts interface IntersectionObserverEntry { /** * Properties of the tracked reference element */ boundingClientRect: DOMRectReadOnly; /** * Visible amount of the tracked reference element */ intersectionRatio: number; /** * Properties of only the visible portion of the tracked reference element (a subset of `boundingClientRect`) */ intersectionRect: DOMRectReadOnly; isIntersecting: boolean; rootBounds: DOMRectReadOnly | null; target: Element; time: number; } interface IObserverOptions { /** * Number of times the hook updates as the tracked element enters the visible screen. Defaults to 1. */ steps?: number; /** * Decimal values indicating the intersection percentage at which the hook updates. */ threshold?: number[]; } ``` It is not possible to define both a step and a threshold. Define one at most. ##### Example Display the percentage of a widget that intersects with the viewport. Update this value 10 times over the course of the widget’s entrance into the viewport. ```tsx import { useRef } from 'react'; import { useIntersection } from '@vev/react'; export default function () { const widgetReference = useRef(null); const intersection = useIntersection(widgetReference, { steps: 10 }); return (

{intersection.intersectionRatio * 100}

); } ``` ### useDevice Vev projects can be custom-designed for various presentation modes such as desktop, tablet, or mobile. `useDevice()` returns the currently active mode `('desktop' | 'tablet' | 'mobile')`. ##### Usage ```ts const device: string = useDevice(); ``` ##### Example ```tsx import { useDevice } from '@vev/react'; export default function () { const device: 'desktop' | 'tablet' | 'mobile' = useDevice(); return

Device: {device}

; } ``` ### useEditorState For better widget usability, consider changing the state of your widget depending on which mode the user is in. For example, play a video when in preview mode, but stop it when in editor mode. Or, if the user is changing the background color of a hidden dropdown in your widget, reveal that dropdown when its background colour property is being changed. `useEditorState()` returns the following: * `disabled: boolean` - true when in editor mode, false when in preview mode. * `rule: string` - tells which CSS rule the user is editing. Sets to ‘host’ when no rule is being edited . * `selected: boolean` - true when the widget is selected in editor mode. ##### Example ```tsx import { useEditorState } from '@vev/react'; import { useState, useEffect, useRef } from 'react'; export default function ({ url }: Props) { const { disabled } = useEditorState(); const videoReference = useRef(null); // this function runs on every value change of 'disabled' useEffect(() => { // If disabled, pause video if (disabled) { videoReference.current.pause(); } else { videoReference.current.play(); } }, [disabled]); return