Docly Developers
Knowledge base Best practice Recommended

Convert an existing website to Docly

In shortConverting 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.
Applies to Site structureTemplates
  1. 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.

  2. 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.

  3. 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

    .docly                <- the workspace root: keeps the type Docly Workspace
    Front-page.docly      <- the front page: a page with its own type
    Tjenester/            <- editor-visible, pages only
      .docly              <- the folder's own page and menu fields (folder schema)
    Sider/                <- editor-visible, pages only
    #/
      site.json           <- security headers
      Config.docly        <- site-wide settings: footer, default texts
      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 all page content must live outside it; the site-wide settings go in #/Config (see Put configuration and secrets in the Config document). See Separate code from content for non-developer users.

    The content tree holds pages, nothing else. The old CMS's helper items do not come across as documents or folders:

    • A section's own page (its intro, heading, menu text) is fields on the folder — a folder schema — not an Overview document in the section. The site-wide settings (footer, default texts) go in #/Config, not a Settings folder, and the workspace root keeps its type Docly Workspace. See Give folders a folder schema for their own page and settings.
    • Images are image fields on the page that shows them, not <img> tags in rich text pointing at an images folder. See Keep images out of rich text fields.
    • Every text the visitor sees comes from a field: menu texts and order, form labels, button and link texts («Read more», «Back»), the 404 text. A text typed into a template cannot be changed by the customer.

    Check it in the workspace as the customer sees it: every item in a content folder is a page, and every document opens in the editor without broken images.

    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, /Tjenester redirects to /Tjenester/ and #/Root/Tjenester.hash is never used — the folder's index wins (or the generic #/Folder/index.hash listing). 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. Before creating such a document, check whether the data belongs on the folder instead — a section page's text does (see above), and then there is no extra URL to redirect.

    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 .hash — query 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. Docly mails every submission to the workspace owner (data-smtp only chooses which SMTP configuration sends it — it is not the recipient), 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

Last reviewed 16 Oct 2026