PageSourceSearch

https://digitalmaker.ai/

html digitalmaker.ai collected 2026-10-05 13:06:26 UTC 19,576 bytes, 404 lines download raw bytes

1<!doctype html>
2<html lang="en">
3
4<head>
5  <meta charset="UTF-8" />
6  <meta name="google-site-verification" content="Zl_QQ11HjpZNZRX5Zg1fZoAVTGwjAXX-oUYNdTLKE54" />
7  <meta data-react-helmet="true" name="description"
8    content="Create faceless AI videos and digital products. Produce faceless YouTube videos with AI voiceovers and scenes, and build ebooks, planners, spreadsheets and images. Digital Maker AI is the all-in-one platform for creators." />
9  <meta data-react-helmet="true" name="keywords"
10    content="faceless video maker, faceless YouTube videos, AI video generator, faceless AI videos, digital product maker, AI digital products, create digital products, ebook maker, AI spreadsheet, digital product creator, online business tools" />
11  <meta data-react-helmet="true" property="og:title" content="Digital Maker AI - Create Faceless AI Videos & Digital Products" />
12  <meta data-react-helmet="true" property="og:description"
13    content="Create faceless AI videos and digital products in minutes, not months. Produce faceless YouTube videos with AI, and generate ebooks, planners, images and websites." />
14  <meta data-react-helmet="true" property="og:type" content="website" />
15  <meta data-react-helmet="true" property="og:url" content="https://digitalmaker.ai" />
16  <meta data-react-helmet="true" property="og:image"
17    content="https://auth.digitalmaker.ai/storage/v1/object/public/digital-maker-outputs/favicon-256.png" />
18  <meta data-react-helmet="true" name="twitter:card" content="summary_large_image" />
19  <meta data-react-helmet="true" name="twitter:title" content="Digital Maker AI - Create Faceless AI Videos & Digital Products" />
20  <meta data-react-helmet="true" name="twitter:description"
21    content="Create faceless AI videos and digital products in minutes, not months. AI tools for faceless YouTube video, ebooks, planners, images and websites." />
22  <!-- Primary tab favicon embedded inline so it can never be corrupted by asset pipelines -->
23  <link rel="icon" type="image/png" sizes="32x32"
24    href="data:image/png;base64,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">
25  <link rel="icon" type="image/png" sizes="16x16"
26    href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADFklEQVR4nF3Sy2tcdQDF8e9v5r7nzkNNpq01TVKbmLZRMZISUCm4kIKiuKiKSha20lUFN9a6sBSUFhHEF4oipUhcpAuLCmqEQEWjFiOWqrVSGiNxtJlMMs977+93Z+7PxRBoPX
26/AB87hCN0NUirmfjhHu9PBdRws08A001iWhWkaWKaJimOGt/UjhGA9RpIkpFIplkrLXLr8N1+dmSfjOWRcB8+z8TMejmPh2Ba37tzKX0tXuHf3OCkhEEJgrEtKKVZWa8ye+ZGM59BJEpIkIYwUuayHkU7z+vFncWyTue9+5p67xtBak1oHZKSoNwI8zyab9chlPfo2b+Dxvffh2BZ9NxVZWCzxyhtTtMKIs/O/AILUeh+pYqq1BjrRJJ2EK8urSCkZu22IeiOgVmuSy3rcPXE7ruuyVq2zXK5gCCFIkoRIKhrNEBXHJFrz5KN72LjhBkr/VNgxMsDvFxd56dWTZH2X3y7+yf7JB7Atq7uBUjFKKcorVXp7CkyfeJmB/k204w6GmebFQ0/x0akZDh99h9K/Kwzd3IdppPE8pwu02x0qaw0G+zdx9PA+PpuZ49z5P7h0eQnDMNg+3M+R5/cx+dge3j1xmo3F67EtE9M0uyNKFdNsBvi+i59xmZr+kofu300UKXp7CoyPbefhJw5x8MBeJnaNYpomrucgBF0gCEIMw2D641k+/eIbTk8d5/OZOXzfo9kMOPvTBQ4eeIRfLyxQLq9R7C1gW2b3SABSKhrNgHQ6xfsnP2Fgy4188NYL1BsBUipc1+bb78+z/5ljCCEo9hSwrgYiqag3WgjA8xyeO/I2QRBy5x0jxHGHN987xezX8zi2RSHv4/serutcC1RrTVbX6iRaA5pjr33ILUNbaAURC4sl8rkMSrXJ53xc28J17GsrbBvczNOTD9IKI2r1FmvVOuWVKlGkyPoekVRUKjVGd2zFdW0c+ypgfGwnhXyWiV2jKBUTRQopFZFUhJGk1YpoBRGV1SrF3uvQWmM7FlprhNZa878kSYKK20jZhcJQEkmJUjFBGDEyPEg+56O15j/VqWvo8lbqoAAAAABJRU5ErkJggg==">
27  <!-- Real-URL fallback (Google Search reads this); apple-touch for iOS home screen -->
28  <link rel="icon" href="https://auth.digitalmaker.ai/storage/v1/object/public/digital-maker-outputs/favicon-256.png">
29  <link rel="apple-touch-icon"
30    href="https://auth.digitalmaker.ai/storage/v1/object/public/digital-maker-outputs/favicon-256.png">
31  <!-- Tags marked data-react-helmet are the home page defaults. Each page's
32       <Helmet> replaces them in the browser, so a page never carries the home
33       page's description alongside its own. Crawlers that don't run JS (social
34       previews) still read these. -->
35  <meta name="generator" content="Digital Maker AI" />
36  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
37  <title>Digital Maker AI - Create Faceless AI Videos &amp; Digital Products</title>
38  <!-- Organization structured data: tells Google which social profiles are ours (sameAs) -->
39  
39<script type="application/ld+json">
40  {
41    "@context": "https://schema.org",
42    "@graph": [
43      {
44        "@type": "Organization",
45        "@id": "https://digitalmaker.ai/#organization",
46        "name": "Digital Maker AI",
47        "url": "https://digitalmaker.ai",
48        "logo": "https://auth.digitalmaker.ai/storage/v1/object/public/digital-maker-outputs/favicon-256.png",
49        "description": "Create faceless AI videos and digital products, all in one platform.",
50        "sameAs": [
51          "https://www.instagram.com/digitalmakerai/",
52          "https://www.youtube.com/@DigitalMaker-AI",
53          "https://www.facebook.com/p/Digital-Maker-AI-61589220783723/"
54        ]
55      },
56      {
57        "@type": "SoftwareApplication",
58        "@id": "https://digitalmaker.ai/#app",
59        "name": "Digital Maker AI",
60        "url": "https://digitalmaker.ai",
61        "applicationCategory": "MultimediaApplication",
62        "operatingSystem": "Web",
63        "description": "AI faceless video maker: turns a topic into a finished faceless YouTube video or Short with AI script, scene images, voiceover, captions and motion. Also makes eBooks, spreadsheets, planners, images and websites.",
64        "publisher": { "@id": "https://digitalmaker.ai/#organization" },
65        "offers": [
66          { "@type": "Offer", "name": "Free", "price": "0", "priceCurrency": "USD", "url": "https://digitalmaker.ai/pricing" },
67          { "@type": "Offer", "name": "Pro", "price": "29", "priceCurrency": "USD", "url": "https://digitalmaker.ai/pricing" },
68          { "@type": "Offer", "name": "Premium", "price": "59", "priceCurrency": "USD", "url": "https://digitalmaker.ai/pricing" },
69          { "@type": "Offer", "name": "Ultimate", "price": "99", "priceCurrency": "USD", "url": "https://digitalmaker.ai/pricing" }
70        ]
71      },
72      {
73        "@type": "WebSite",
74        "@id": "https://digitalmaker.ai/#website",
75        "name": "Digital Maker AI",
76        "url": "https://digitalmaker.ai",
77        "publisher": { "@id": "https://digitalmaker.ai/#organization" }
78      }
79    ]
80  }
81  </script>
81
82  <!-- Tolt affiliate tracking -->
83  
83<script async src="https://files.tlt-cdn.com/tlt.js" data-tolt="pk_rs75ASjsYcrRHbSFjYjsJk5S"></script>
83
84  <!-- Google Ads tag -->
vendor: 66 bytes, lines 84-85
84
85  <script async src="https://www.googletagmanager.com/gtag/js?id=
85AW-18347101597
vendor: 14 bytes, lines 85-86
85"></script>
86  
86<script>
87    
vendor: 139 bytes, lines 87-90
87window.dataLayer = window.dataLayer || [];
88    function gtag(){dataLayer.push(arguments);}
89    gtag('js', new Date());
90    gtag('config', '
90AW-18347101597
vendor: 6 bytes, lines 90-91
90');
91  
91</script>
91
92  <!-- First-touch attribution capture. Runs synchronously before React mounts
93       and before any OAuth redirect, so ad clicks are never lost. Only writes
94       on a first touch that actually carries tracking params; never overwrites. -->
95  
95<script>
96    (function () {
97      try {
98        var p = new URLSearchParams(window.location.search);
99        var keys = ['utm_source','utm_medium','utm_campaign','utm_content','utm_term','utm_placement','gclid','gbraid','wbraid','gad_source'];
100        var data = {};
101        var hasAny = false;
102        keys.forEach(function (k) {
103          var v = p.get(k);
104          if (v) { data[k] = v; hasAny = true; }
105        });
106        if (hasAny && !localStorage.getItem('dm_attribution')) {
107          data.landing_url = window.location.href;
108          data.captured_at = new Date().toISOString();
109          localStorage.setItem('dm_attribution', JSON.stringify(data));
110        }
111      } catch (e) { /* no-op */ }
112    })();
113  </script>
113
114  
114<script type="module" crossorigin src="/assets/index-OlFrYGZF.js"></script>
114
115  <link rel="stylesheet" crossorigin href="/assets/index-DE6EvvX5.css">
116  
116<script type="module">
117window.onerror = (message, source, lineno, colno, errorObj) => {
118	const errorDetails = errorObj ? JSON.stringify({
119		name: errorObj.name,
120		message: errorObj.message,
121		stack: errorObj.stack,
122		source,
123		lineno,
124		colno,
125	}) : null;
126
127	window.parent.postMessage({
128		type: 'horizons-runtime-error',
129		message,
130		error: errorDetails
131	}, '*');
132};
133</script>
133
134  
134<script type="module">
135const observer = new MutationObserver((mutations) => {
136	for (const mutation of mutations) {
137		for (const addedNode of mutation.addedNodes) {
138			if (
139				addedNode.nodeType === Node.ELEMENT_NODE &&
140				(
141					addedNode.tagName?.toLowerCase() === 'vite-error-overlay' ||
142					addedNode.classList?.contains('backdrop')
143				)
144			) {
145				handleViteOverlay(addedNode);
146			}
147		}
148	}
149});
150
151observer.observe(document.documentElement, {
152	childList: true,
153	subtree: true
154});
155
156function handleViteOverlay(node) {
157	if (!node.shadowRoot) {
158		return;
159	}
160
161	const backdrop = node.shadowRoot.querySelector('.backdrop');
162
163	if (backdrop) {
164		const overlayHtml = backdrop.outerHTML;
165		const parser = new DOMParser();
166		const doc = parser.parseFromString(overlayHtml, 'text/html');
167		const messageBodyElement = doc.querySelector('.message-body');
168		const fileElement = doc.querySelector('.file');
169		const messageText = messageBodyElement ? messageBodyElement.textContent.trim() : '';
170		const fileText = fileElement ? fileElement.textContent.trim() : '';
171		const error = messageText + (fileText ? ' File:' + fileText : '');
172
173		window.parent.postMessage({
174			type: 'horizons-vite-error',
175			error,
176		}, '*');
177	}
178}
179</script>
179
180  
180<script type="module">
181const originalConsoleError = console.error;
182const MATCH_LINE_COL_REGEX = /:(\d+):(\d+)\)?\s*$/; // regex to match the :lineNum:colNum
183const MATCH_AT_REGEX = /^\s*at\s+(?:async\s+)?(?:.*?\s+)?\(?/; // regex to remove the 'at' keyword and any 'async' or function name
184const MATCH_PATH_REGEX = /^\//; // regex to remove the leading slash
185
186function parseStackFrameLine(line) {
187	const lineColMatch = line.match(MATCH_LINE_COL_REGEX);
188	if (!lineColMatch) return null;
189	const [, lineNum, colNum] = lineColMatch;
190	const suffix = `:${lineNum}:${colNum}`;
191	const idx = line.lastIndexOf(suffix);
192	if (idx === -1) return null;
193	const before = line.substring(0, idx);
194	const path = before.replace(MATCH_AT_REGEX, '').trim();
195	
196	if (!path) return null;
197
198	try {
199		const pathname = new URL(path).pathname;
200		const filePath = pathname.replace(MATCH_PATH_REGEX, '') || pathname;
201		return `${filePath}:${lineNum}:${colNum}`;
202	} catch (e) {
203		const filePath = path.replace(MATCH_PATH_REGEX, '') || path;
204		return `${filePath}:${lineNum}:${colNum}`;
205	}
206}
207
208function getFilePathFromStack(stack, skipFrames = 0) {
209	if (!stack || typeof stack !== 'string') return null;
210	const lines = stack.split('\n').slice(1);
211
212	const frames = lines.map(line => parseStackFrameLine(line.replace(/\r$/, ''))).filter(Boolean);
213
214	return frames[skipFrames] ?? null;
215}
216
217function formatConsoleMessage(args, skipStackFrames = 1) {
218	let messageString = '';
219	let filePath = null;
220
221	for (let i = 0; i < args.length; i++) {
222		const arg = args[i];
223		if (arg instanceof Error) {
224			filePath = getFilePathFromStack(arg.stack, 0);
225			messageString = `${arg.name}: ${arg.message}`;
226			if (filePath) {
227				messageString = `${messageString} at ${filePath}`;
228			}
229			break;
230		}
231	}
232
233	if (!messageString) {
234		// Never let logging throw: React elements, DOM nodes and many error objects
235		// are circular, and JSON.stringify on them threw inside console.error,
236		// crashing the page to a black screen for customers (2026-10-05).
237		messageString = args.map(arg => {
238			if (arg === null || typeof arg !== 'object') return String(arg);
239			try { return JSON.stringify(arg); } catch (e) { return Object.prototype.toString.call(arg); }
240		}).join(' ');
241		const stack = new Error().stack;
242		filePath = getFilePathFromStack(stack, skipStackFrames);
243		if (filePath) {
244			messageString = `${messageString} at ${filePath}`;
245		}
246	}
247
248	return messageString;
249}
250
251// Only report to an embedding editor (the old Horizons builder iframe). On the
252// live site there is no parent window, and these wrappers must never throw.
253const IS_EMBEDDED = (() => { try { return window.self !== window.top; } catch (e) { return true; } })();
254
255console.error = function(...args) {
256	originalConsoleError.apply(console, args);
257	if (!IS_EMBEDDED) return;
258	try {
259		window.parent.postMessage({
260			type: 'horizons-console-error',
261			error: formatConsoleMessage(args, 1)
262		}, '*');
263	} catch (e) { /* logging must never break the page */ }
264};
265
266const originalConsoleWarn = console.warn;
267
268console.warn = function(...args) {
269	originalConsoleWarn.apply(console, args);
270	if (!IS_EMBEDDED) return;
271	try {
272		window.parent.postMessage({
273			type: 'horizons-console-warn',
274			warning: formatConsoleMessage(args, 1)
275		}, '*');
276	} catch (e) { /* logging must never break the page */ }
277};
278</script>
278
279  
279<script type="module">
280// Prevents authentication failures (PB returns 400) from crashing the app — use console.info, not console.error/PM2.
281// [urlPattern, bodyPattern] tuples; align with sandboxErrorUtils ERROR_BLACKLIST.
282const BENIGN_FETCH_ERRORS = [
283	[/hcgi\/platform\/api\/collections\/.*auth-with-password.*/i, /Failed to authenticate/i],
284	[/hcgi\/api\//i, /Insufficient credits/i],
285	// A user-supplied integration secret (Stripe/PayPal/Twilio/...) is not set yet — an expected setup state, not a bug to fix.
286	[/hcgi\/api\//i, /INTEGRATION_NOT_CONFIGURED/i],
287];
288
289function isBenignFetchError(url, body) {
290	return BENIGN_FETCH_ERRORS.some(([urlPattern, bodyPattern]) =>
291		urlPattern.test(url) && (!bodyPattern || bodyPattern.test(body)));
292}
293
294const PLATFORM_URL_PATTERN = /hcgi\/platform\//i;
295const VALIDATION_CODE_TEXT_PATTERN = /validation_/;
296
297function hasValidationCode(value) {
298	if (value == null) {
299		return false;
300	}
301	if (typeof value === 'string') {
302		return value.startsWith('validation_');
303	}
304	if (Array.isArray(value)) {
305		return value.some(hasValidationCode);
306	}
307	if (typeof value === 'object') {
308		return Object.values(value).some(hasValidationCode);
309	}
310	return false;
311}
312
313function isValidationFetchWarning(url, body) {
314	if (!PLATFORM_URL_PATTERN.test(url)) {
315		return false;
316	}
317	try {
318		return hasValidationCode(JSON.parse(body));
319	} catch {
320		return VALIDATION_CODE_TEXT_PATTERN.test(body);
321	}
322}
323
324const originalFetch = window.fetch;
325
326window.fetch = function(...args) {
327	const url = args[0] instanceof Request ? args[0].url : args[0];
328
329	// Skip WebSocket URLs
330	if (url.startsWith('ws:') || url.startsWith('wss:')) {
331		return originalFetch.apply(this, args);
332	}
333
334	return originalFetch.apply(this, args)
335		.then(async response => {
336			const contentType = response.headers.get('Content-Type') || '';
337
338			// Exclude HTML document responses
339			const isDocumentResponse =
340				contentType.includes('text/html') ||
341				contentType.includes('application/xhtml+xml');
342
343			if (!response.ok && !isDocumentResponse) {
344					const responseClone = response.clone();
345					const errorFromRes = await responseClone.text();
346					const requestUrl = response.url;
347					const errorMessage = `Fetch error from ${requestUrl}: ${errorFromRes}`;
348
349					if (isBenignFetchError(requestUrl, errorFromRes)) {
350						console.info(errorMessage);
351					} else if (isValidationFetchWarning(requestUrl, errorFromRes)) {
352						console.warn(errorMessage);
353					} else {
354						console.error(errorMessage);
355					}
356			}
357
358			return response;
359		})
360		.catch(error => {
361			if (!url.match(/.html?$/i)) {
362				// Cancelled in-flight fetch (e.g. preview reload) — not a server failure.
363				if (error?.name === 'AbortError') {
364					console.info(error);
365				} else {
366					console.error(error);
367				}
368			}
369
370			throw error;
371		});
372};
373</script>
373
374  
374<script type="module">
375if (window.navigation && window.self !== window.top) {
376	window.navigation.addEventListener('navigate', (event) => {
377		const url = event.destination.url;
378
379		try {
380			const destinationUrl = new URL(url);
381			const destinationOrigin = destinationUrl.origin;
382			const currentOrigin = window.location.origin;
383
384			if (destinationOrigin === currentOrigin) {
385				return;
386			}
387		} catch (error) {
388			return;
389		}
390
391		window.parent.postMessage({
392			type: 'horizons-navigation-error',
393			url,
394		}, '*');
395	});
396}
397</script>
397
398</head>
399
400<body>
401  <div id="root"></div>
402</body>
403
404</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.