Convert an existing website to Docly
-
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
.hashfile.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 forcesunsafe-inlineinto 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åndvaskin 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.mdand.claude/docly.mdin 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
- Map the source. Start with
robots.txtandsitemap.xml, then crawl — sitemaps are routinely incomplete. Capture every page, its text, its images, its meta description and its URL. - 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.
- Build schemas first, then templates, then content documents.
- Measure every page against its original at each milestone.
- 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 wayhrefandsrcin 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,/Tjenesterredirects to/Tjenester/and#/Root/Tjenester.hashis never used — the folder's index wins (or the generic#/Folder/index.hashlisting). 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.Nameis a .NET array-like object.Array.isArray()returns false on it andString(v)yields an empty string — index it directly. saveJsonneedsensurePath. The signature issaveJson(path, data, ensurePath); withouttrueit throws when the folder does not exist.docly.redirectdoes not add the mount path. Userequest.sitepath + "Page".- Write functions exist only in the API context.
docly.saveJsonisundefinedin.hashfiles.
Query parameters are unavailable in
.hash—queryandrequest.queryare undefined andrequest.rawurlis 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 —
/_conversionworks well — withnoindex,nofollowand no link from the menu. Generate the tables from the documents withgetFiles()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
denyAccessif 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-smtponly chooses which SMTP configuration sends it — it is not the recipient), anddata-validateruns 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 intoa[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 takesletandconst.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>, nostyle=, 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...ofor.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.mdandAGENTS.mdreturn 404
Last reviewed 16 Oct 2026