# Asset Extraction and Downloading in the AI Website Cloner Template: A Complete Guide

> Learn asset extraction and downloading for images videos and fonts. Our AI Website Cloner Template uses a three-stage pipeline for local self-contained resources.

- Repository: [JCodesMore/ai-website-cloner-template](https://github.com/JCodesMore/ai-website-cloner-template)
- Tags: how-to-guide
- Published: 2026-06-23

---

**Asset extraction and downloading in the AI Website Cloner Template follows a three-stage pipeline: browser-based discovery, Node.js downloading to `public/`, and Next.js integration, ensuring every image, video, and font becomes a local, self-contained resource.**

The `JCodesMore/ai-website-cloner-template` treats binary assets as first-class citizens during the cloning process. Instead of leaving remote URLs in the generated code, the template enumerates all visual resources from the target page, persists them locally, and rewrites references to point to the bundled copies. This guide walks through the exact implementation found in the repository's command definitions and scaffold structure.

## Browser-Based Asset Discovery

The foundation of asset extraction begins with a JavaScript reconnaissance script executed via browser automation (Playwright, Chrome MCP, or Puppeteer). According to the clone-website command documentation in [`.opencode/commands/clone-website.md`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/.opencode/commands/clone-website.md) (lines 1894-2261), this script walks the DOM to catalog every extractable resource.

The discovery process captures:

- **Images**: `src`, `alt` text, natural dimensions, and positioning context from `<img>` tags
- **Videos**: Source URLs, poster frames, and playback flags (`autoplay`, `loop`, `muted`)
- **Background Images**: Computed CSS `background-image` values with their associated element selectors
- **Fonts**: All unique `fontFamily` declarations found in the first 200 DOM elements
- **Favicons**: Icon links and their size attributes
- **SVGs**: Count of inline SVGs for later React component conversion

```javascript
// Asset discovery – run via browser MCP
JSON.stringify({
  images: [...document.querySelectorAll('img')].map(img => ({
    src: img.src || img.currentSrc,
    alt: img.alt,
    width: img.naturalWidth,
    height: img.naturalHeight,
    parentClasses: img.parentElement?.className,
    position: getComputedStyle(img).position,
    zIndex: getComputedStyle(img).zIndex
  })),
  videos: [...document.querySelectorAll('video')].map(v => ({
    src: v.src || v.querySelector('source')?.src,
    poster: v.poster,
    autoplay: v.autoplay,
    loop: v.loop,
    muted: v.muted
  })),
  backgroundImages: [...document.querySelectorAll('*')].filter(el => {
    const bg = getComputedStyle(el).backgroundImage;
    return bg && bg !== 'none';
  }).map(el => ({
    url: getComputedStyle(el).backgroundImage,
    element: `${el.tagName}.${el.className?.split(' ')[0]}`
  })),
  svgCount: document.querySelectorAll('svg').length,
  fonts: [...new Set([...document.querySelectorAll('*')]
    .slice(0,200)
    .map(el => getComputedStyle(el).fontFamily))],
  favicons: [...document.querySelectorAll('link[rel*="icon"]')]
    .map(l => ({ href: l.href, sizes: l.sizes?.toString() }))
});

```

This JSON payload is returned to the cloning agent, which passes the asset inventory to the download phase.

## Downloading Assets to the Local Project

The second stage persists discovered resources to the filesystem using a Node.js script. While the repository provides the specification in [`.opencode/commands/clone-website.md`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/.opencode/commands/clone-website.md) (lines 1318-1325), you must implement `scripts/download-assets.mjs` to handle the actual fetching.

The script must write files into a structured hierarchy under `public/`:

- `public/images/` for photographs and background assets
- `public/videos/` for media files
- `public/fonts/` for typography files
- `public/seo/` for favicons and Open Graph images

```javascript
// scripts/download-assets.mjs
import { promises as fs } from 'node:fs';
import path from 'node:path';
import https from 'node:https';
import http from 'node:http';

async function fetchAndSave(url, dest) {
  const client = url.startsWith('https') ? https : http;
  const resp = await new Promise((resolve, reject) => {
    client.get(url, r => resolve(r), reject);
  });
  if (resp.statusCode !== 200) throw new Error(`GET ${url} → ${resp.statusCode}`);
  await fs.mkdir(path.dirname(dest), { recursive: true });
  const file = await fs.open(dest, 'w');
  await new Promise((res, rej) => {
    resp.pipe(file.createWriteStream())
      .on('finish', res)
      .on('error', rej);
  });
  await file.close();
}

async function main() {
  const data = JSON.parse(await fs.readFile(process.argv[2], 'utf8'));
  const tasks = [];

  // Images
  for (const img of data.images) {
    const filename = path.basename(new URL(img.src).pathname);
    const dest = path.join('public', 'images', filename);
    tasks.push(fetchAndSave(img.src, dest));
  }

  // Videos
  for (const vid of data.videos) {
    if (!vid.src) continue;
    const filename = path.basename(new URL(vid.src).pathname);
    const dest = path.join('public', 'videos', filename);
    tasks.push(fetchAndSave(vid.src, dest));
  }

  // Background images (strip CSS url() wrapper)
  for (const bg of data.backgroundImages) {
    const raw = bg.url.replace(/^url\(["']?/, '').replace(/["']?\)$/, '');
    const filename = path.basename(new URL(raw).pathname);
    const dest = path.join('public', 'images', filename);
    tasks.push(fetchAndSave(raw, dest));
  }

  // Favicons
  for (const ico of data.favicons) {
    const filename = path.basename(new URL(ico.href).pathname);
    const dest = path.join('public', 'seo', filename);
    tasks.push(fetchAndSave(ico.href, dest));
  }

  // Process with concurrency limit of 4
  const concurrency = 4;
  while (tasks.length) {
    await Promise.all(tasks.splice(0, concurrency));
  }
}

main().catch(err => {
  console.error('Asset download failed:', err);
  process.exit(1);
});

```

**Critical implementation detail**: Without local asset storage, the cloned site would retain remote URLs, breaking offline builds and causing visual mismatches when external resources change or become unavailable.

## Integrating Assets into the Next.js Scaffold

Once downloaded, assets must be wired into the Next.js application. The scaffold provides placeholder directories (`public/images/.gitkeep`, `public/videos/.gitkeep`) to ensure Git tracks these folders, but the integration requires specific code updates.

**Images** are referenced using `next/image` components or CSS background properties in [`src/app/globals.css`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/app/globals.css). **Videos** use standard HTML5 `<video>` tags pointing to `public/videos/`. **Fonts** require updating [`src/app/layout.tsx`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/app/layout.tsx) to import discovered typefaces via `next/font/google` or `next/font/local`.

```tsx
// src/app/layout.tsx
import { Inter } from 'next/font/google';

const inter = Inter({ 
  subsets: ['latin'], 
  weight: ['400','600','700'] 
});

export default function RootLayout({ 
  children 
}: { 
  children: React.ReactNode 
}) {
  return (
    <html lang="en" className={inter.className}>
      <body>{children}</body>
    </html>
  );
}

```

**SEO assets** (favicons and OG images) live in `public/seo/` and are linked through the metadata API in [`layout.tsx`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/layout.tsx). When `npm run build` executes, Next.js automatically includes these static files in the production bundle, creating a fully self-contained replica.

## Summary

- **Discovery**: Run the browser MCP script from [`.opencode/commands/clone-website.md`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/.opencode/commands/clone-website.md) to generate a complete JSON inventory of images, videos, fonts, and favicons.
- **Download**: Implement `scripts/download-assets.mjs` to fetch resources with a concurrency limit of 4, storing them in `public/images/`, `public/videos/`, `public/fonts/`, and `public/seo/`.
- **Integration**: Update [`src/app/layout.tsx`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/app/layout.tsx) with discovered fonts and ensure CSS/JSX references point to the local `public/` paths rather than remote URLs.

## Frequently Asked Questions

### Where does the asset discovery script execute?

The discovery script runs inside the browser automation context (Playwright, Chrome MCP, or Puppeteer) as specified in [`.opencode/commands/clone-website.md`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/.opencode/commands/clone-website.md) lines 1894-2261. It executes via `document.querySelectorAll` to capture live DOM state including computed styles for background images and font families.

### How does the download script prevent overwhelming the network?

The reference implementation in `scripts/download-assets.mjs` limits concurrent downloads to 4 simultaneous connections using `Promise.all(tasks.splice(0, concurrency))`. This prevents memory exhaustion and avoids triggering rate limits on the source servers.

### What directory structure should downloaded assets follow?

Assets must follow the scaffold's expected structure: images and background assets go to `public/images/`, video files to `public/videos/`, font files to `public/fonts/`, and SEO assets (favicons, OG images) to `public/seo/`. These directories are pre-seeded with `.gitkeep` files in the repository template.

### How are Google Fonts handled during asset extraction?

The discovery script captures `fontFamily` values from computed styles. During integration, you import these into [`src/app/layout.tsx`](https://github.com/JCodesMore/ai-website-cloner-template/blob/main/src/app/layout.tsx) using `next/font/google` for optimal loading, or download the font files directly to `public/fonts/` if self-hosting is required. The template specifically mentions updating fonts as part of Phase 2 in the clone command documentation.