LayerPort

Guide · Deep dive

Framer export vs saving the page.

Everyone tries Ctrl+S first. It is worth understanding exactly why it fails on a Framer site, because the same reasons decide what a real capture has to do -- and give you a test you can run on any copy, from any tool, in four minutes.

In brief

  1. Save As gets a snapshot of the DOM

    Chrome's “Webpage, Complete” saves the DOM at that instant and the assets it can see. Lazy images, srcset variants, fonts in CSS and dynamically imported modules are not what it sees.

  2. View Source gets a skeleton

    The initial HTML is a hydration target. Framer's interactivity arrives as ES modules that import each other several levels deep.

  3. wget and HTTrack follow links, not JavaScript

    They mirror what an href or src points at. A dynamic import() and a runtime-built URL are invisible to them.

  4. A capture works from what the browser painted

    Rendered DOM, the closed module graph, every font and image variant, three breakpoints -- then references rewritten to local paths.

  5. Test any copy the same way

    Local server, Offline throttle, walk the routes at three widths, filter the network panel for foreign hosts.

What “Save page as” actually saves

Chrome's Webpage, Complete writes the current DOM to an .html file and a _files folder with the resources it has already loaded. That last clause is the whole problem. A Framer page at the moment you press Ctrl+S has not loaded the images below the fold (they are lazy), has not fetched the srcset variants for widths you have not viewed, has not imported the modules an animation would pull in on scroll, and still references fonts through url() inside CSS that the save may or may not rewrite. Open the result and you get the header, a few images, and a lot of empty rectangles.

View Source is worse for a different reason: it is the initial HTML before hydration, which on Framer is a skeleton plus <script type="module"> tags. The page assembles itself in the browser. Copying the source copies the instructions to fetch the page, not the page.

Source: Google Chrome Help — Download a page · MDN — Lazy loading · MDN — <img>: srcset and sizes

Five ways to copy a site, against the things a Framer page needs

Ctrl+S / View Sourcewget --mirror / HTTrackCapture (LayerPort)
Rendered DOM, not the initial skeletonCtrl+S yes, View Source noNo -- fetches HTML as servedYes -- the painted page
ES modules imported at runtimeOnly what had loadedNo -- import() is not a linkYes -- graph walked to closure, ≤1,500 modules
Every srcset / lazy image variantOnly what had loadedsrcset partly; lazy never triggersYes -- every referenced variant
Fonts referenced from CSS url()SometimesWith -p, usuallyYes, and rewritten
Three breakpointsThe one you were viewingAll markup, untestedReviewed at desktop, tablet, phone
Editable, versioned, resyncableNoNoYes
Authorization requiredNoNoYes -- a recorded per-site confirmation

The wget everybody pastes

wget --mirror --page-requisites --convert-links \
     --adjust-extension --no-parent --span-hosts \
     --domains=example.com,framerusercontent.com \
     https://example.com/
This is a good mirror of a 2009 website. On Framer it downloads the skeleton, the CSS, the fonts it can see, and none of the module graph that draws the page -- because import() inside JavaScript is not a link wget can follow.

Source: HTTrack — FAQ

What defeats the mirror tools

A published Framer page is a React application that hydrates. Its behaviour lives in ES modules under framerusercontent.com/modules/… and framer.com/m/…, which import further modules -- the appear effect imports the motion library, the motion library imports helpers -- and some of those imports are import() calls decided at runtime. HTTrack's own FAQ is candid about this: it lists “intensive Java/Javascript sites” among the cases it cannot yet handle, and says they “might be bogus/incomplete”. wget does not run JavaScript at all. So they mirror the entry points and miss the graph.

The second trap is breakpoints. Framer renders every breakpoint's layout into the DOM and hides all but one with media queries listed in <script id="__framer__breakpoints">. A mirror gets that markup but has never *seen* the tablet layout, so a broken tablet image goes unnoticed until a client opens it on an iPad. A capture that reviews at three widths finds it on day one.

LayerPort's capture starts from the rendered page, walks the module graph until every import resolves locally, pulls every image variant and font, and rewrites references across HTML, CSS, JS, MJS, JSON, SVG, XML, TXT and web manifests. Then a portability check reads the result and refuses anything that would only resolve on the machine that made it. Where a reference could not be resolved, the manifest says so.

Three numbers from the capture pipeline

  • 3 layouts a Framer page carries -- desktop, tablet, phone -- reviewed on every capture
  • 1500 module ceiling per site when closing the import graph; a marketing site uses a fraction
  • 9 text formats searched for references and rewritten to local paths

The four-minute test for any copy

  1. Serve it, do not open it

    Double-clicking index.html loads it from file://, where ES modules and fonts are blocked by the browser regardless of whether the copy is good. Run python3 -m http.server 8080 in the folder and open it over HTTP.

  2. Go offline and hard-reload

    DevTools → Network → throttle Offline → Cmd/Ctrl+Shift+R. A real copy renders completely. A saved page shows the shape of the failure: which images, which fonts, which animations.

  3. Look for foreign hosts

    Back online, filter the Network panel with -domain:127.0.0.1. Every remaining row is a dependency on someone else's server. Analytics you added deliberately are fine; framerusercontent.com is not.

  4. Resize to three widths

    1280, 1024, 390. Framer's tablet layout is the one nobody checks. Open the phone menu.

Source: Chrome DevTools — Network reference · MDN — Cross-Origin Resource Sharing

Whose site is it?

The question no mirroring tool asks

Confirming authorization records what you told us — that the site is yours or that you have permission. It is not, and could not be, a determination of legal copyright ownership: no technical signal settles that question, so we do not claim to have settled it.

LayerPort — published ownership policy — Shown in the product before any capture runs, and recorded with the run

Sources

  1. Google Chrome Help — Download a page — What “Webpage, Complete” promises.
  2. MDN — Lazy loading — loading="lazy" and why the first images stay eager.
  3. MDN — <img>: srcset and sizes — Why a page can reference five files for one picture.
  4. HTTrack — FAQ — The mirror tool's own list of what it cannot yet handle. Read 28 Sep 2026.
  5. Chrome DevTools — Network reference — Filtering requests by domain, the Offline throttle.
  6. MDN — Cross-Origin Resource Sharing — Why fonts and modules fail from file:// and across origins.

FAQ

Why does my saved Framer page look broken?

Because Save As keeps what had loaded at that instant: lazy images, other srcset widths, runtime-imported modules and cross-origin fonts were not part of it, and some references still point at Framer's hosts.

Would wget or HTTrack do better?

For a classic HTML site, yes. For Framer, they mirror the skeleton and the CSS but cannot follow import() inside JavaScript, so the module graph that draws the page never arrives.

Is a capture the same as a screenshot?

No. A screenshot is pixels. A capture is the working page: the DOM the browser painted plus every script, style, font and image it needs, rewritten to load from your folder.

Can I test that a copy really works offline?

Serve it over HTTP, set DevTools to Offline, hard-reload, and walk the routes at three widths. The steps above take four minutes and work on any tool's output.

Features · Workflow · Pricing · FAQ