Convert an existing website to Docly

Best practice Recommended
Converting an existing website to Docly has three obligations that hold simultaneously: the technique is rebuilt to Docly conventions rather than copied, the visual result is pixel-identical to the approved original, and all content becomes schema-bound documents the customer can edit. Pixel-perfect is a measurement - subtract getBoundingClientRect() on every page, not a sample - because each page type usually has its own template, its own type scale and its own background image. Third-party dependencies cannot come along: A+ security headers require a CSP without unsafe-inline, which rules out inline style, inline script, CDN fonts and embedded widgets. The old site's defects are findings to record and fix, not specifications to reproduce. Finish by publishing a conversion report in the new solution at a fixed URL, with its tables generated from the documents so it cannot drift.

What you'll see

A customer has a working website on another platform and wants it rebuilt in Docly. The content is there, the design is approved, and the URLs have rankings. The tempting move is to copy the markup across, wire it to a template and call it converted. That produces a site that looks roughly right, drifts from the original in ways nobody measures, and carries the old platform's dependencies and defects into the new one.

What's actually happening

A conversion has three obligations that pull in different directions, and all three have to hold at once.

The technique is rebuilt, not copied. The old markup, scripts, folder structure and naming conventions do not come across. They encode another platform's constraints. Docly has its own model — schema-bound documents, display templates, master pages, the #/ tree — and the conversion is only worth doing if the result is a genuine Docly solution rather than the old site wearing a new hat.

The visual result is identical. The customer approved that design. A conversion that looks almost the same is a redesign nobody ordered, and the differences surface after launch when they are expensive. Pixel-perfect is the default, and it is a measurement, not an impression.

The content becomes editable. If the customer could edit the old site and cannot edit the new one, the conversion moved backwards. Every piece of text, every price, every phone number belongs in a schema-bound document, not in a .hash file.

Why "it looks the same" is not verification

Eyeballing a screenshot catches gross errors and nothing else. In one conversion the front page matched on five measured elements and was declared pixel-perfect — while the service section rendered as description cards instead of the original's six-button grid, the call-to-action was a filled button instead of an outlined one, the footer had the wrong structure entirely, and every subpage used a different background image that had not been noticed at all.

Each page type generally has its own template. Front page, subpage and detail page routinely differ in heading treatment, font sizes, column widths and background image. Measuring one page tells you about one page.

Third-party dependencies do not survive the trip

The target is A+ on securityheaders.com, which means a Content-Security-Policy without unsafe-inline. That single requirement rules out inline <style> blocks, style= attributes, inline scripts, CDN stylesheets, web-font services and embedded widgets. Fonts get self-hosted. Widgets get replaced or dropped. This is not a preference to be traded away for convenience — one inline attribute forces unsafe-inline into the policy and the rating collapses.

The old site's defects are findings, not specifications

Legacy sites carry bugs that a faithful conversion would faithfully reproduce. Real examples from one conversion: the home page served identical content at two URLs with no canonical; nine pages had double-encoded meta descriptions rendering as H&aring;ndvask in search results; six pages shared one 540-character description; the sitemap omitted a third of the pages; the price list existed in the markup but sat inside a zero-height container and never displayed; jQuery loaded twice.

Fix them, and record each one — what it was, what it cost, what you did. That list is one of the most valuable things the customer gets out of the project.

What to do

Step zero: set up the workspace's instruction files

Before anything is built, create .claude/CLAUDE.md and .claude/docly.md in the workspace as described in Set up AI agent instruction files for a workspace. Record the environment there: source URL, the URL the workspace publishes on now, the domain it takes over at launch, and whether the site is live yet.

Skip this and the next session starts blind — it will re-derive the environment, guess at the conventions, and ask the customer questions that were already answered once.

Ask these before you start

Only the things that cannot be looked up. Ask them together, up front, not one at a time as you hit them:

  • Which URL does this workspace publish on today, and which domain does it take over at launch?
  • Is the existing design approved as-is? Pixel-perfect is the default; a redesign has to be stated explicitly.
  • Who receives contact-form submissions, and is SMTP configured? Without this the form stores enquiries but notifies nobody.
  • Is there source material in higher resolution than what is on the web — logo, photography?
  • Is there content on the old site that should not come across, or content that is missing and should be added?
  • Anything on the old site that is deliberately the way it is, even though it looks wrong?

Never ask about these — decide them yourself

URL structure, folder and file names, schema design, which field type to use, where a file goes, whether something should be a document or a template, and the order of work. All of it follows from this entry and the documentation. Asking costs the customer time and produces no better answer.

What does go back to the customer is business fact: contradictions in the source content, content with no obvious home, and launch decisions. Collect them and present them together at the end, in the conversion report.

Order of work

  1. Map the source. Start with robots.txt and sitemap.xml, then crawl — sitemaps are routinely incomplete. Capture every page, its text, its images, its meta description and its URL.
  2. Capture the design in the browser. Fonts, colours, background images and computed styles on headings, body text and buttons. Read the values; do not estimate them.
  3. Build schemas first, then templates, then content documents.
  4. Measure every page against its original at each milestone.
  5. Publish a conversion report in the new solution.

Verification that actually verifies

Read getBoundingClientRect() for each significant element on the original, read the same on the new page, and subtract. All four numbers — y, x, width, height — must be zero.

const g = s => { const e = document.querySelector(s); const b = e.getBoundingClientRect();
  return [Math.round(b.y + scrollY), Math.round(b.x), Math.round(b.width), Math.round(b.height)]; };

// original: h1 = [204, 463, 980, 32]
g('h1').map((v, i) => v - [204, 463, 980, 32][i]);   // -> [0, 0, 0, 0]

Do this for every page, not a sample. Anything that will not reach zero is a deviation: write it down with the reason. A recorded deviation is a decision; an unrecorded one is a defect.

Structure

Tjenester/            <- editor-visible, documents only
Sider/                <- editor-visible
Innstillinger/        <- company info, price list
#/
  site.json           <- security headers
  master.hash
  <Schema>.hash       <- display templates, in the ROOT of #/
  Schemas/
  Folder/index.hash
  Root/               <- everything published: index.hash, style.css, 404.hash
  API/

The #/ tree is invisible to non-developers, so everything editable must live outside it. See Separate code from content for non-developer users.

Platform behaviour worth knowing before you start

Per-page background images without inline style. Set a class on <body> from the child page and style it in the stylesheet:

<body class="subpage-bg" xdt:Transform="SetAttributes(class)">

url() in an external stylesheet is not rewritten with the mount path the way href and src in HTML are — it resolves against the stylesheet's own address. Use relative paths (url("images/bg.jpg")) so they work both under a subfolder mount and later on the customer's own domain.

Cache-bust the stylesheet with docly.assetUrl("/style.css"). Without it the browser serves the previous CSS and you will debug a change that is already correct on the server. Fetch the served stylesheet and check before concluding that an edit did not work. See Cache-busting bundles with assetUrl.

A content folder beats a Root page of the same name. With a Tjenester/ folder present, #/Root/Tjenester.hash returns 404; the landing page must be #/Root/Tjenester/index.hash.

Document URLs need redirecting. Documents that exist only as a source for a Root page are still served at their own URL, which is duplicate content. Make the display template redirect to the canonical page.

API endpoints: four traps that all produce a bare 500

  • Form fields are array-like, not strings. On application/x-www-form-urlencoded, form.Name is a .NET array-like object. Array.isArray() returns false on it and String(v) yields an empty string — index it directly.
  • saveJson needs ensurePath. The signature is saveJson(path, data, ensurePath); without true it throws when the folder does not exist.
  • docly.redirect does not add the mount path. Use request.sitepath + "Page".
  • Write functions exist only in the API context. docly.saveJson is undefined in .hash files.

Query parameters are unavailable in .hashquery and request.query are undefined and request.rawurl is stripped. Use dedicated pages for confirmation and error states rather than ?sent=1.

Finish with a conversion report

Publish it in the new solution at a fixed URL — /_conversion works well — with noindex,nofollow and no link from the menu. Generate the tables from the documents with getFiles() rather than typing them out, so the report cannot drift from what the solution actually contains.

Cover: environment and URLs; the content model (schema → folder → document count); a full content listing; the URL map with redirects; defects found in the old solution; the security headers actually served; the pixel measurements element by element with the deviation in numbers; and the open items with who has to resolve each one.

Note in the report that it is readable by anyone who knows the address, and that it can be closed with denyAccess if the customer prefers.

Forms

Use Docly''s built-in form submission. Do not write your own submit endpoint. data-smtp mails the submission and data-validate runs a server-side validation function in #/API/ before the form is accepted. A hand-written endpoint reimplements the platform and gets the ordering, the redirects or the error paths wrong.

Every form is protected with Cloudflare Turnstile, verified server-side, failing closed while a test key is in place. See Protect every form with Turnstile. Submissions are never written into the file tree.

No counter loops with index lookups

for (var i = 0; i < a.length; i++) reaching into a[i] is out. Iterate over the elements instead.

for (let x of list) is the first choice; .forEach() is fine too. Both work, and the engine takes let and const.

for (let d of docly.getFiles("/Settings")) {
    if (d.fileschema == "CompanyInfo") { info = d; }
}

In hash templates the loop spans the block boundary — open it in one block, close it in the next:

#{ for (let s of services) { }#
  <a href="#s.Url#">#docly.htmlEncode(s.Title)#</a>
#{ } }#

Last step: re-read the guidelines against what you built

Before calling it finished, open the KB entries again and check the work against them — from the page, not from memory. The violations that survive to the end are the ones nobody re-read for.

  • No <style>, no style=, no inline <script>
  • No third-party resources — fonts and icons self-hosted
  • Security headers in #/site.json, A+ met
  • No counter loops with index lookups — for...of or .forEach()
  • Every form has Turnstile, verified server-side, failing closed on test keys
  • No submissions stored in the file tree; secrets only under #/
  • No document URL duplicating a page
  • Every page measured against the original, deviation written out in numbers
  • Animations reproduced
  • Scratch files deleted and their URLs returning 404
  • Conversion report published; CLAUDE.md and AGENTS.md return 404