EditlyCMS
A lightweight, flat-file CMS for editing HTML templates directly in the browser. No database setup required — just drop it next to your HTML files and start editing.
Introduction
EditlyCMS is a flat-file content management system that lets you edit HTML pages directly in the browser. Unlike WordPress or other database-driven CMS platforms, EditlyCMS works directly with your existing HTML files — there is no database to configure, no hosting requirements beyond PHP, and no migration needed.
It works by scanning your website for HTML files that contain specially marked elements (cms-content), then letting you click and edit those elements through a floating toolbar injected into the page.
How it works
When you open a page in edit mode, EditlyCMS:
- Reads the live HTML file from disk
- Injects a floating toolbar at the bottom of the page
- Makes all
cms-contentelements directly editable (they show a dashed blue outline) - When you save, writes your changes to a draft file (
page.draft.html) - When you publish, copies the draft over the live file and creates a backup
What EditlyCMS is good for
- Static HTML sites that need occasional content updates
- Client handoff — give clients a way to edit text and images without touching code
- Landing pages, portfolio sites, restaurant menus, property listings
- Any site where the developer builds the HTML and the client maintains the content
What it is not
- Not a page builder — it cannot change layout or add new sections
- Not a multi-user system — single account only
- Not suitable for highly dynamic sites (e-commerce, user accounts, etc.)
Want to see it first? Open the live demo to try Editly CMS on a real site.
What's new in 1.1.0
Version 1.1.0 is a redesign of everything you see in the CMS. How your pages are found, saved, published and backed up has not changed.
- Light and dark mode. The login screen, dashboard and editor follow your device's setting automatically.
- A visual dashboard. Pages appear as cards with a live preview, a Draft tag, the time of the last edit and a status dot. Sites with more than four pages get a search box.
- A compact editor toolbar. The everyday controls sit on one bar. Strikethrough, indent, alignment, undo and redo, video, map and file upload moved into a More menu, and Discard draft and Move toolbar into a ⋯ menu. On small screens the bar wraps onto two rows instead of running off the page.
- The editor stays out of your site's way. Its styles are scoped to its own toolbar and dialogs, and the small ID labels above editable regions no longer pick up your site's heading styles.
- Steadier text selection. Clicking a toolbar button no longer takes the selection away from the page, so bold, links and lists apply reliably.
- Line icons instead of emoji on the toolbar, and plainer wording in dialogs.
Upgrading from 1.0.0
Replace cms/index.php and the three files in cms/views/. Nothing else changed, so your database, drafts, backups and uploads carry over as they are.
Your templates need no changes. If you build image galleries, see the note on the edit-mode image wrapper.
Installation
EditlyCMS requires a web server running PHP 7.4 or higher with the SQLite3 extension enabled. It works on shared hosting (InfinityFree, cPanel, etc.), VPS servers, and locally via WAMP, XAMPP, Laragon, or similar.
Requirements
| Requirement | Details |
|---|---|
| PHP | 7.4 or higher |
| Extensions | SQLite3, mbstring (usually enabled by default) |
| Writable folders | cms/ directory must be writable by PHP |
| Your HTML files | Must be in the same folder as cms/ or in subfolders |
Setup steps
Upload the cms/ folder
Place the cms/ folder in the root of your website — the same folder that contains your HTML files.
Set folder permissions
Make sure the cms/ folder is writable by the web server. On Linux/cPanel set it to 755 or 777. On shared hosting this is usually done via your FTP client or File Manager.
Visit /cms/ in your browser
Navigate to https://yoursite.com/cms/. The CMS will initialize itself on first run — it creates the SQLite database, uploads folder, and backups folder automatically.
Log in with default credentials
Username: admin Password: admin — change these immediately after first login.
Security: Change the default password immediately after installation. Anyone who knows the default credentials can edit your site. On shared hosting, also consider restricting access to /cms/ via .htaccess.
Folder structure after installation
First Login
Navigate to /cms/ on your site. You'll see the login screen.
| Field | Default value |
|---|---|
| Username | admin |
| Password | admin |
Change both the username and password immediately after first login. Go to the dashboard and click Settings in the top right corner.
After logging in you'll land on the Dashboard which shows all editable pages found on your site.
Dashboard user
The dashboard is your home base. It automatically scans your website and shows every HTML file that contains editable regions, as a grid of page cards.
Page cards
Each card shows:
- A live preview — a small picture of the page itself. Click it to edit. Previews show the published page without running scripts, so sliders and animations stay still.
- Page title — taken from the HTML
<title>tag. If every title ends with the same site name (for exampleAbout | My Site), that shared ending is hidden so the grid stays readable. Hover the title to see the full text. - File path — the relative path on your server, such as
index.html. - Edit status — when the page was last edited ("Edited 2 days ago"), with a status dot: amber for unpublished changes, green for published, grey for a page that has not been edited yet.
- A Draft tag over the preview when unpublished changes exist. The preview itself still shows the live version.
The home page (index.html) always comes first. The other pages follow in alphabetical order.
Search
Once your site has more than four pages, a search box appears above the grid. Type part of a title or file name to filter the cards.
Page actions
| Button | What it does |
|---|---|
| Edit | Opens the page in edit mode with the floating toolbar. Clicking the preview does the same. |
| View | Opens the published version of the page in a new tab |
| Backups (3) | Shows a list of previous versions you can restore. It appears once the page has at least one backup. |
Settings panel
Click Settings in the top-right header to open the settings panel. It has two tabs:
- Password — change your login password
- Username — change your login username (requires current password)
Light and dark mode
The login screen, dashboard and editor follow your device's light or dark setting. There is no switch to flip: change the setting on your computer or phone and the CMS follows.
Editing Content user
Click Edit on any page card, or click its preview, to open the page in edit mode. The page loads normally in the browser with a floating toolbar at the bottom of the screen. You can move it to the top.
Identifying editable regions
Editable areas have a dashed blue outline. Hover over one and the outline turns solid, and a small label shows its ID. Click inside to start typing.
Only elements marked by the developer as editable will show an outline. You cannot accidentally edit navigation, footers, or other structural elements unless the developer has marked them.
The toolbar
The toolbar is a single compact row. Everyday tools sit on the bar, less frequent ones are in the More menu, and rarely used actions are in the ⋯ menu. On a narrow screen the bar wraps onto a second row so nothing runs off the edge.
The More menu
Click More to open a small menu above (or below) the toolbar. It closes when you pick something, click elsewhere or press Esc.
| Item | Action | Shortcut |
|---|---|---|
| Strikethrough | Strike through the selected text | — |
| Indent / Outdent | Indent or outdent a list item or paragraph | — |
| Align left / centre / right | Align the current paragraph | — |
| Undo / Redo | Step back or forward through your edits | Ctrl+Z / Y |
| Embed video | Embed a YouTube or Vimeo video | — |
| Embed map | Embed a Google Map | — |
| Upload file | Upload a file and insert a download link | — |
The ⋯ menu
The ⋯ menu starts with the name of the file you are editing, followed by two actions:
- Move toolbar to top / bottom — switch where the toolbar sits. The CMS remembers your choice in this browser.
- Discard draft — delete the draft and return to the live version. You are asked to confirm.
Inserting a link
- Select the text you want to turn into a link
- Click the Link button
- Enter the URL and optionally choose to open in a new window
- Click Insert link
If you don't select text first, you can type the link text in the "Link text" field inside the dialog. The link will be inserted at your cursor position.
Embedding a video
- Click inside the editable region where you want the video
- Open More and choose Embed video
- Paste a YouTube or Vimeo URL (not an embed URL — just the regular page URL)
- Set width and height as needed
- Click Embed video
Embedding a Google Map
- In Google Maps, open the location you want
- Click Share → Embed a map and copy the
src="..."URL from the iframe code - In EditlyCMS, open More, choose Embed map and paste that URL
- Set width and height, then click Embed map
Pasting content
EditlyCMS strips all formatting when you paste — only plain text is inserted. This prevents messy styles from being copied in from Word, Google Docs, or other websites. If you need formatting, apply it manually using the toolbar after pasting.
Drag and drop images
You can drag an image file from your computer directly onto any editable region. EditlyCMS will upload it automatically and insert it at the cursor position.
Images & Media user
There are two types of image regions in EditlyCMS — inline images (images inside text content) and gallery/hero images (standalone image containers set up by the developer).
Inserting inline images
- Click inside a text region to place your cursor
- Click the Image button in the toolbar
- Either upload a file from your device or paste an image URL
- Add alt text (important for accessibility and SEO)
- Click Insert
Editing gallery / hero images
For image-only regions (like a photo gallery or hero background), hover over the image. You'll see two controls appear:
- Red ✕ button (top-right) — removes the image
- Blue resize handle (bottom-right) — drag to resize the image width
To replace a gallery image, delete it with the ✕ button, then click the Image button to insert a new one.
Uploaded files
All uploaded images and files are stored in cms/uploads/. They are accessible at /cms/uploads/filename.jpg. Files are never automatically deleted — manage them directly via FTP if you need to clean up.
Uploaded images are stored in the cms/uploads/ folder. If you delete the CMS folder, all uploaded media will be lost. Keep regular backups of this folder.
Draft & Publish user
EditlyCMS uses a draft → publish workflow. Changes you make are never pushed live immediately — they're saved as a draft first, letting you preview and review before going live.
The workflow
Make edits
Click and type in any highlighted editable region. An amber dot appears in the toolbar when there are unsaved changes.
Save as draft
Press Ctrl+S or click Save. Changes are written to a hidden draft file. The live site is untouched. You can close the browser and come back — the draft is preserved.
Preview (optional)
Go back to the dashboard and click View to see the live page. Then click Edit again — the editor loads the draft if one exists.
Publish
Click Publish in the toolbar. EditlyCMS saves the draft, backs up the current live file, then copies the draft to the live file. The page is now updated on your site.
Discarding a draft
If you want to throw away all unsaved changes and revert to the live version, open the ⋯ menu in the toolbar and choose Discard draft. You'll be asked to confirm. This action is permanent — the draft is deleted and cannot be recovered.
The Discard draft option only removes the draft — it does not affect the live site. To undo changes already published to the live site, use the Backups feature.
Unsaved changes warning
If you try to close the browser tab or navigate away with unsaved changes, EditlyCMS will warn you. It also shows a prompt if you click the back arrow with unsaved changes.
Backups user
Every time you publish a page, EditlyCMS automatically creates a backup of the previous live version. Backups are stored as timestamped HTML files in cms/backups/.
Viewing backups
On the dashboard, click Backups (n) on any page card. The number is how many backups the page has, and the button appears once there is at least one. You'll see a list of all saved versions with timestamps.
Restoring a backup
- Click Backups (n) on the page card
- Find the version you want to restore
- Click Restore — a confirmation dialog appears
- Confirm — the backup becomes the new live version (and the current live gets backed up first)
Restoring a backup automatically backs up the current live version first, so you can always undo a restoration.
Deleting backups
Old backups can be deleted from the backup list to save disk space. Click the Delete button next to any backup, or Delete all to clear every backup of that page at once. This is permanent and cannot be undone.
Template Basics developer
EditlyCMS finds editable pages by scanning all HTML files for the string cms-content. Any file that contains it will appear in the dashboard. Any element with the class cms-content and a unique id will be editable.
The two requirements
To make an element editable, it needs exactly two things:
- The class
cms-content - A unique
idattribute
<!-- Correct — has both class and unique id -->
<h1 class="hero-title cms-content" id="hero-title">
Welcome to Our Site
</h1>
<p class="cms-content" id="hero-subtitle">
We make great things happen.
</p>
<!-- Wrong — missing id -->
<p class="cms-content">This won't be editable</p>
<!-- Wrong — missing class -->
<p id="hero-text">This won't be editable either</p>
Which HTML elements can be editable
| Element | Works? | Notes |
|---|---|---|
<p> | Yes | Best choice for paragraphs |
<h1>–<h6> | Yes | Best for headings |
<span> | Yes | Good for inline text inside other elements |
<div> | Yes | Good for image containers (see Image Regions) |
<a> | Yes | Editable link text; href must be changed via the link modal |
<li> | Yes | Individual list items |
<img> | Limited | Visible in dashboard but save/publish won't update it — use a div wrapper instead (see Image Regions) |
<input> | No | Form inputs cannot be made editable |
Nesting editable elements
Avoid nesting cms-content elements inside each other. EditlyCMS saves the full innerHTML of the outermost editable element, which would overwrite any separately-saved child elements.
<!-- Avoid — nested cms-content will cause conflicts -->
<div class="cms-content" id="section-text">
<p class="cms-content" id="para1">Text here</p>
</div>
<!-- Better — flat siblings -->
<div class="text-block">
<p class="cms-content" id="para1">Text here</p>
<p class="cms-content" id="para2">More text here</p>
</div>
cms-content Rules developer
IDs must be unique across the entire page
This is the most common source of bugs. The PHP save engine uses the id attribute to find the right element in the HTML file. If two elements share an ID, the engine finds the wrong one and either skips saving or saves to the wrong place.
Critical: Never use the same ID on a cms-content element and a section anchor. For example, if you have <section id="gallery">, do not also use id="gallery" on a cms-content element. Use id="gallery1" or id="gallery-img-1" instead.
<!-- BROKEN — two elements with id="gallery" -->
<section id="gallery">
...
<div class="cms-content" id="gallery"> <!-- same id! -->
<img src="photo.jpg" />
</div>
</section>
<!-- FIXED — unique IDs -->
<section id="gallery">
...
<div class="cms-content" id="gallery1">
<img src="photo.jpg" />
</div>
</section>
Recommended ID naming convention
Use descriptive, section-prefixed IDs so they're easy to identify in the dashboard:
<!-- Hero section -->
<p class="hero-eyebrow cms-content" id="hero-eyebrow">...</p>
<h1 class="hero-title cms-content" id="hero-title">...</h1>
<p class="hero-subtitle cms-content" id="hero-sub">...</p>
<!-- About section -->
<h2 class="cms-content" id="about-title">...</h2>
<p class="cms-content" id="about-p1">...</p>
<p class="cms-content" id="about-p2">...</p>
<!-- Amenities -->
<div class="cms-content" id="am1-title">...</div>
<div class="cms-content" id="am1-desc">...</div>
Special characters in HTML source
EditlyCMS uses PHP's DOMDocument to save changes. This is generally reliable but one known behavior: when EditlyCMS saves a file, non-ASCII characters in HTML comments (like ═, ★, →) used to get converted to numeric HTML entities like ═. This has been fixed since v1.0.0 — the save engine decodes these back to UTF-8 characters automatically.
Image Regions developer
Making standalone images editable (gallery photos, hero images, etc.) requires a specific pattern. A plain <img class="cms-content"> will appear in the dashboard but won't save correctly because:
- The PHP regex that reads existing content requires a closing tag —
<img>is self-closing and has none - The CMS save engine uses
innerHTML— a plain<img>has no innerHTML - The image resize/delete buttons in the editor are injected into a parent element, not onto the img itself
The correct pattern
Wrap each image in a <div> with cms-content. The div is the editable container; the img lives inside it.
<!-- Correct — div wrapper with cms-content, img inside -->
<div class="img-cell cms-content" id="hero-photo">
<img src="images/hero.jpg" alt="Hero image" />
</div>
<!-- Wrong — img directly as cms-content -->
<img class="cms-content" id="hero-photo" src="images/hero.jpg" />
Gallery grid pattern
When using CSS Grid for galleries, the wrapper div needs height:100% to fill its grid cell, and the image fills the div:
.gallery-grid {
display: grid;
grid-template-columns: 2fr 1fr 1fr;
grid-template-rows: 280px 280px;
gap: 8px;
/* Allow CMS edit buttons to overflow the grid boundary */
overflow: visible;
padding: 32px;
margin: -32px;
}
.gallery-grid .img-cell {
display: block;
height: 100%; /* fills the grid row height */
overflow: hidden; /* clips the image to the cell */
}
.gallery-grid .img-cell img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
<div class="gallery-grid">
<div class="img-cell span2 cms-content" id="gallery1">
<img src="images/gallery1.jpg" alt="Main photo" />
</div>
<div class="img-cell cms-content" id="gallery2">
<img src="images/gallery2.jpg" alt="Photo 2" />
</div>
<div class="img-cell cms-content" id="gallery3">
<img src="images/gallery3.jpg" alt="Photo 3" />
</div>
</div>
The padding: 32px; margin: -32px trick on the grid gives the CMS edit buttons (which are position:absolute inside each cell) room to appear outside the grid cell boundaries without affecting the visual layout.
The edit-mode wrapper
While you edit, the CMS wraps every image in a small <span class="cms-iw"> so it can show the delete and resize buttons. The wrapper is inline-block, so an image styled with width:100%; height:100% can shrink or collapse in edit mode even though the live page looks fine. Tell the wrapper to fill its cell:
.img-cell .cms-iw {
display: block;
width: 100%;
height: 100%;
}
The wrapper exists only while editing. It is removed again when you save, so it never reaches your published HTML.
Common Gotchas developer
Files not appearing in the dashboard
EditlyCMS only lists files that contain the string cms-content. If a file doesn't appear:
- Confirm at least one element has
class="cms-content" - Confirm the file is
.html,.htm, or.php - Confirm the file is not inside the
cms/subfolder - Confirm the file is not a
.draft.htmlfile
Edits save but don't appear on the live site
You've saved a draft but haven't published. Click Publish in the toolbar. A Draft tag on the page card in the dashboard means unpublished changes exist.
Saves appear to work but nothing changes
Almost always caused by a duplicate ID. Check your HTML — if a cms-content element shares an ID with any other element on the page (especially section anchors like <section id="about">), the save engine finds the wrong element and skips it. Rename the cms-content element's ID to something unique.
Gallery images are visible but edit/delete buttons don't appear
The edit buttons require the image to be a child of a data-cms-editable element. If cms-content is directly on the <img> tag, the buttons won't appear. Use the div wrapper pattern described in Image Regions.
Images are invisible in the dashboard
The dashboard uses a regex to extract content previews — this regex requires a closing tag. Self-closing <img> elements are invisible to it. Wrap images in a <div class="cms-content">.
CSS Grid images collapsing to zero height
When an image wrapper div is inside a CSS Grid and the img is position:absolute, the div collapses to zero height because it has no content in normal flow. Fix: add height:100% to the wrapper div so it fills its grid row.
Images look wrong only while editing
In edit mode the CMS wraps each image in a <span class="cms-iw">, which is inline-block by default. An image with width:100%; height:100% can shrink or collapse inside it even though the live page looks fine. Make the wrapper fill its cell, as described in Image Regions.
Images work locally but not on server
Check case sensitivity. Linux servers are case-sensitive — if your folder is named Images/ but your HTML references images/, the image won't load. Rename folders to lowercase. Also check for spaces in filenames — Windows sometimes adds trailing spaces during rename operations.
Comments in HTML source getting converted to hex codes
This is caused by PHP's DOMDocument encoding non-ASCII characters as numeric HTML entities when serializing. EditlyCMS handles this automatically — no action needed.
File Structure developer
Draft files
When you save a draft of index.html, a file called index.draft.html is created in the same directory. Draft files are automatically excluded from the dashboard page list. They are deleted when you publish or discard.
Backup files
Backup files are stored in cms/backups/ with names like index_20250304_143022.html. The naming format is {safe-path}_{YYYYMMDD}_{HHmmss}.html.
Database schema
The SQLite database has two tables:
-- Tracks each discovered page
CREATE TABLE pages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT UNIQUE NOT NULL, -- relative path e.g. "index.html"
title TEXT, -- extracted from <title> tag
has_draft INTEGER DEFAULT 0,
last_edited DATETIME,
last_published DATETIME
);
-- Tracks backup files for each page
CREATE TABLE backups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
page_path TEXT NOT NULL,
backup_file TEXT NOT NULL, -- filename in cms/backups/
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
PHP API Reference developer
All API endpoints are routed through cms/index.php via the ?action= query parameter. All responses are JSON.
Authentication endpoints
| Action | Method | Parameters | Description |
|---|---|---|---|
login | POST | username, password | Start a session |
logout | POST | — | Destroy session |
change_password | POST | current, new | Change password (auth required) |
change_username | POST | password, username | Change username (auth required) |
check_auth | GET | — | Returns {"authenticated": true/false} |
Page endpoints
| Action | Method | Body | Description |
|---|---|---|---|
pages | GET | — | List all CMS pages with metadata and backups |
get_page | GET | ?path= | Get current contents of a page |
save_draft | POST | {path, contents} | Save changes to a draft file |
publish | POST | {path} | Publish draft to live (creates backup first) |
discard | POST | {path} | Delete draft, revert to live |
restore_backup | POST | {path, backup} | Restore a backup as the live version |
delete_backup | POST | {path, backup} | Permanently delete a backup file |
Media endpoints
| Action | Method | Parameters | Description |
|---|---|---|---|
upload_image | POST | file (multipart) | Upload an image, returns {success, url} |
upload_file | POST | file (multipart) | Upload any file, returns {success, url} |
delete_media | POST | {path} | Delete an uploaded file from disk |
The save_draft payload
The contents object is a map of element IDs to their new innerHTML:
{
"path": "index.html",
"contents": {
"hero-title": "New headline text",
"hero-sub": "Updated subheading here",
"gallery1": "<img src=\"/cms/uploads/photo.jpg\" alt=\"New photo\" />"
}
}
Key function: injectContents()
This is the core save logic in Pages.php. It reads the live HTML file, uses PHP's DOMDocument to find each element by ID, replaces its children with the new content, and serializes back to HTML. The key behaviors:
- Finds elements using XPath:
//*[@id="element-id"] - Verifies the found element has
cms-contentclass before modifying - Strips
contenteditableanddata-cms-editableattributes before saving - Strips the injected
<base href>tag - Decodes numeric HTML entities back to UTF-8 (fixes the
═comment issue)
Change Password user
Open Settings
From the dashboard, click Settings in the top-right header.
Go to the Password tab
Click the Password tab inside the settings panel.
Enter current and new password
Fill in your current password and your desired new password, then click Update password.
Change Username user
Open Settings
From the dashboard, click Settings in the top-right header.
Go to the Username tab
Click the Username tab inside the settings panel.
Enter your password and new username
Your current password is required to change the username. Enter the new username and click Update username.