import { PublishSettings } from '../vfs/types';
import { generateTrackingScript } from '../analytics/tracking-script';
import { generateConsentBanner } from './consent-banner';
export interface HtmlProcessingOptions {
publishSettings: PublishSettings;
projectId: string;
baseUrl: string;
deploymentId: string;
hasEdgeFunctions?: boolean;
}
/**
* Injects scripts, CDN links, SEO meta tags, analytics, and compliance banner into HTML
*
* Note: Under construction mode is handled separately by serving a dedicated page,
* not by overlaying content
*/
export function processHtml(html: string, options: HtmlProcessingOptions): string {
const { publishSettings, projectId, baseUrl, deploymentId, hasEdgeFunctions } = options;
let processed = html;
// 1. Inject SEO meta tags into
processed = injectSeoMetaTags(processed, publishSettings, baseUrl);
// 2. Inject CDN links into
processed = injectCdnLinks(processed, publishSettings);
// 3. Inject head scripts into
processed = injectHeadScripts(processed, publishSettings);
// 4. Inject edge function interceptor into (only if project has edge functions)
if (hasEdgeFunctions) {
processed = injectEdgeFunctionInterceptor(processed, deploymentId);
}
// 5. Inject body scripts before
processed = injectBodyScripts(processed, publishSettings);
// 6. Inject analytics tracking (if enabled and builtin)
processed = injectAnalytics(processed, deploymentId, publishSettings);
// 7. Inject compliance banner (if enabled)
processed = injectComplianceBanner(processed, deploymentId, publishSettings);
return processed;
}
/**
* Injects SEO meta tags into
*/
function injectSeoMetaTags(html: string, settings: PublishSettings, baseUrl: string): string {
const { seo } = settings;
if (!seo || Object.keys(seo).length === 0) {
return html;
}
const metaTags: string[] = [];
// Basic meta tags
if (seo.title) {
metaTags.push(`${escapeHtml(seo.title)}`);
metaTags.push(``);
metaTags.push(``);
}
if (seo.description) {
metaTags.push(``);
metaTags.push(``);
metaTags.push(``);
}
if (seo.keywords && seo.keywords.length > 0) {
metaTags.push(``);
}
// Open Graph
if (seo.ogImage) {
metaTags.push(``);
metaTags.push(``);
}
metaTags.push(``);
metaTags.push(``);
// Twitter Card
metaTags.push(``);
// Canonical URL
if (seo.canonical) {
metaTags.push(``);
}
// Robots directives
const robotsDirectives: string[] = [];
if (seo.noIndex) robotsDirectives.push('noindex');
if (seo.noFollow) robotsDirectives.push('nofollow');
if (robotsDirectives.length > 0) {
metaTags.push(``);
}
// Inject into
return injectIntoHead(html, metaTags.join('\n '));
}
/**
* Injects CDN links (CSS and JS) into
*/
function injectCdnLinks(html: string, settings: PublishSettings): string {
const enabledCdnLinks = settings.cdnLinks.filter(cdn => cdn.enabled);
if (enabledCdnLinks.length === 0) {
return html;
}
const links: string[] = [];
for (const cdn of enabledCdnLinks) {
if (cdn.type === 'css') {
links.push(``);
} else if (cdn.type === 'js') {
links.push(``);
}
}
return injectIntoHead(html, links.join('\n '));
}
/**
* Injects head scripts into
*/
function injectHeadScripts(html: string, settings: PublishSettings): string {
const enabledHeadScripts = settings.headScripts.filter(script => script.enabled);
if (enabledHeadScripts.length === 0) {
return html;
}
const scripts: string[] = [];
for (const script of enabledHeadScripts) {
if (script.type === 'inline') {
scripts.push(``);
} else if (script.type === 'external') {
scripts.push(``);
}
}
return injectIntoHead(html, scripts.join('\n '));
}
/**
* Injects body scripts before
*/
function injectBodyScripts(html: string, settings: PublishSettings): string {
const enabledBodyScripts = settings.bodyScripts.filter(script => script.enabled);
if (enabledBodyScripts.length === 0) {
return html;
}
const scripts: string[] = [];
for (const script of enabledBodyScripts) {
if (script.type === 'inline') {
scripts.push(``);
} else if (script.type === 'external') {
scripts.push(``);
}
}
const bodyCloseTag = '';
const bodyCloseIndex = html.lastIndexOf(bodyCloseTag);
if (bodyCloseIndex === -1) {
// No tag, append at the end
return html + '\n' + scripts.join('\n') + '\n';
}
return (
html.slice(0, bodyCloseIndex) +
' ' + scripts.join('\n ') + '\n' +
html.slice(bodyCloseIndex)
);
}
/**
* Helper to inject content into
*/
function injectIntoHead(html: string, content: string): string {
const headCloseTag = '';
const headCloseIndex = html.indexOf(headCloseTag);
if (headCloseIndex === -1) {
// No tag, try to inject after or at the beginning
const headOpenTag = '';
const headOpenIndex = html.indexOf(headOpenTag);
if (headOpenIndex !== -1) {
return (
html.slice(0, headOpenIndex + headOpenTag.length) +
'\n ' + content + '\n' +
html.slice(headOpenIndex + headOpenTag.length)
);
}
// No at all, prepend
return content + '\n' + html;
}
return (
html.slice(0, headCloseIndex) +
' ' + content + '\n' +
html.slice(headCloseIndex)
);
}
/**
* Inject analytics tracking script (if enabled and provider is builtin)
*/
function injectAnalytics(html: string, deploymentId: string, settings: PublishSettings): string {
if (!settings.analytics.enabled || settings.analytics.provider !== 'builtin') {
return html;
}
// Get analytics config with token and features
const { analytics } = settings;
const trackingOptions = {
deploymentId: deploymentId,
token: analytics.token,
features: {
basicTracking: analytics.features?.basicTracking !== false, // Default to true
heatmaps: analytics.features?.heatmaps === true,
sessionRecording: analytics.features?.sessionRecording === true,
performanceMetrics: analytics.features?.performanceMetrics === true,
engagementTracking: analytics.features?.engagementTracking === true,
customEvents: analytics.features?.customEvents === true,
},
};
// If compliance is enabled and blocks analytics, wrap in consent check
if (settings.compliance.enabled && settings.compliance.blockAnalytics) {
const wrappedScript = `
`.trim();
return injectBeforeBodyClose(html, wrappedScript);
}
// Otherwise, inject directly
const trackingScript = generateTrackingScript(trackingOptions);
return injectBeforeBodyClose(html, trackingScript);
}
/**
* Inject compliance/consent banner (if enabled)
*/
function injectComplianceBanner(html: string, deploymentId: string, settings: PublishSettings): string {
if (!settings.compliance.enabled) {
return html;
}
const banner = generateConsentBanner({
deploymentId,
compliance: settings.compliance,
});
return injectBeforeBodyClose(html, banner);
}
/**
* Helper to inject content before
*/
function injectBeforeBodyClose(html: string, content: string): string {
const bodyCloseTag = '';
const bodyCloseIndex = html.lastIndexOf(bodyCloseTag);
if (bodyCloseIndex === -1) {
// No tag, append at the end
return html + '\n' + content + '\n';
}
return (
html.slice(0, bodyCloseIndex) +
content + '\n' +
html.slice(bodyCloseIndex)
);
}
/**
* Inject edge function interceptor script into
* Routes fetch/form requests to edge function API endpoints
*/
function injectEdgeFunctionInterceptor(html: string, deploymentId: string): string {
if (!deploymentId) {
return html;
}
// Minified interceptor script for production
const interceptorScript = ``;
return injectIntoHead(html, interceptorScript);
}
/**
* Escape HTML special characters
*/
function escapeHtml(text: string): string {
const map: Record = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
return text.replace(/[&<>"']/g, (char) => map[char]);
}