Content History in Joomla
You spend twenty minutes rewriting an article, click Save, and only then notice that you overwrote three paragraphs you meant to keep. On most systems that work is gone. On Joomla, if one option is switched on, every earlier version is still sitting in the database, one click away.
That safety net is Content History (com_contenthistory), Joomla's built-in version control for content. This article explains how it works from three sides: the basics for owners and editors who just want an undo button, the setup for administrators who decide what gets saved, and the technical details for developers who want to know where the versions live and how to add the feature to their own extensions.
A quiet "time machine" for your articles that ships with Joomla and is already switched on out of the box.
The goal is simple: help you understand Content History well enough to trust it the day you need to roll an article back.
1. The Basics
1.1 What is Content History?
The Content History component (com_contenthistory) records a snapshot of a content item every time you save it. Joomla introduced it in version 3.2 (2013), and it has shipped in core ever since.
What it does in short:
- Each save writes one snapshot (a "version") into the
#__historydatabase table. - A snapshot stores the full data of the item at that moment, plus who saved it, when, and an optional note.
- From the edit screen you can view, compare, restore, or delete any earlier version.
- You can mark a version Keep Forever so Joomla never prunes it.
Think of it as a built-in undo history that survives logout, server reboots, and even a closed browser. It is not a backup of your whole site - it tracks one item at a time - but for the everyday "I need yesterday's version of this article" question, it is exactly the right tool.
1.2 What Content History Is Not
Three quick clarifications save a lot of confusion later:
- It is not a site backup. It versions individual content items, not files, templates, or the database as a whole.
- It is not the same as the Action Logs. Action Logs record that someone saved an article; Content History records what the article looked like at each save.
- It is not something you have to switch on for core articles. Joomla's installer already enables it for Articles, Contacts, News Feeds, Banners, Tags, and User Notes. Modules ship with it off, and your own extensions must opt in (see section 2).
1.3 Where Do I Find It?
Content History has no menu of its own. You reach it from inside the item you are editing. Open any article:
Content → Articles → (open an article) → Versions (toolbar button)
The Versions button only appears when history is enabled for that component and at least one version exists. Clicking it opens a popup that lists every saved version of that single article. The component itself lives at administrator/components/com_contenthistory/ and acts entirely as a helper to other components - it never shows its own list of "all versions everywhere".
2. Turning Content History On
2.1 The Two Options
Content History is controlled by two options in each component's Options screen:
| Option | Internal name | Meaning |
|---|---|---|
| Save History | save_history |
Switch versioning on (Yes) or off (No). Joomla's installer seeds this to Yes for most core components; the field's own config.xml default is No, which is what a custom component inherits. |
| Maximum Versions | history_limit |
How many versions to keep per item. Default is 10. Set 0 for unlimited. |
For articles the path is:
Content → Articles → Options → Editing Layout → Save History = Yes
Maximum Versions = 10
Once Save History is Yes, every save of every article from that point on creates a version. Items saved before you switched it on have no history - versioning is not retroactive.
2.2 Which Components Support It?
Content History is not limited to articles. In Joomla 6 the following core components ship the Save History option:
| Component | What gets versioned |
|---|---|
com_content |
Articles |
com_contact |
Contacts |
com_newsfeeds |
News feeds |
com_banners |
Banners |
com_modules |
Modules |
com_tags |
Tags |
com_users |
User notes |
Each component has its own independent setting. Turning history on for articles does nothing for contacts; you switch each on where you need it.
2.3 What Is On by Default?
This trips up almost everyone, so it is worth stating plainly: on a fresh Joomla install most of these components arrive with history already on. The installer seeds Save History = Yes for com_content, com_contact, com_newsfeeds, com_banners, com_tags, and com_users (a limit of 10 for articles, contacts, and banners; 5 for news feeds, tags, and user notes). Only com_modules ships with history off.
So if you open a brand-new site and find Save History = Yes on Articles, that is expected - you did not turn it on, the installer did. The config.xml field carries default="0" (No), but that default only takes effect when no value is stored for the component. Joomla's install SQL stores "save_history":"1" for the six components above, which overrides the field default. The default="0" is therefore the value a fresh custom component inherits, not what core articles get.
2.4 A Sensible Starting Point
Because articles already arrive with history on and a limit of 10, the real decisions are different from "should I switch it on". For content you edit heavily, consider raising the limit (10 to 20 versions for articles is comfortable). For components you never roll back, you can switch history off to slow table growth - more on that trade-off in section 5. Modules are the one core component that ships with history off; turn it on there if you tune module settings often and want a way back.
Back to top3. Working with Versions
3.1 The Versions Popup
Open an article and click Versions. The popup lists every saved version, newest first, with these columns:
| Column | Meaning |
|---|---|
| Checkbox | Select one or two versions to act on. |
| Date | When the version was saved. Click it to preview that version. |
| Version Note | The optional label you typed when saving (empty if none). |
| Saved By | The editor who created the version. |
| Keep Forever | A lock icon. Highlighted means this version is protected from pruning. |
The toolbar above the list holds the actions: Restore, Compare, Keep On/Off, and Delete.
3.2 Preview a Single Version
Click a version's date and Joomla opens a read-only Preview. It shows the field values that version held - title, text, publishing dates, and so on. Preview does not change anything; it just lets you look before you decide.
3.3 Compare Two Versions
Tick exactly two versions and press Compare. Joomla shows a side-by-side diff: fields that differ are highlighted, so you can see at a glance that the intro text changed and the publish date did not. This is the fastest way to answer "what actually changed between Monday and Tuesday?".
3.4 Restore a Version
Tick one version and press Restore. Joomla loads that version's data back into the edit form. Two things matter here:
- Restore fills the form. It does not save by itself. You still have to click Save to make the rollback permanent.
- When you save the restored data, that save creates a new version. Nothing is lost - the version you rolled back from stays in the list too.
3.5 Add a Version Note
The edit form has a Version Note field (often under the Publishing tab or near the toolbar). Type a short label like "before rewrite" or "approved by client" before you save, and that note appears in the Versions list. Notes turn a wall of timestamps into a readable timeline.
Back to top4. How a Version Gets Saved
4.1 The Versionable Behaviour Plugin
The magic that turns a normal save into a version is a small system plugin: Behaviour - Versionable (plg_behaviour_versionable). It ships enabled in core. It listens for one table event:
onTableAfterStore fires right after any record is written to the database
When the event fires, the plugin checks three things in order:
- Does the table implement
VersionableTableInterface? If not, ignore it. - Did the save succeed? A failed save creates no version.
- Is
save_historyenabled for that component? If not, stop.
Only when all three are true does the plugin call Versioning::store() to write the snapshot. Disable this plugin and Content History goes silent across the whole site, no matter what the component options say.
4.2 A Full Snapshot, Not a Diff
The snapshot is the item's full data, JSON-encoded, exactly as it was stored. For an article that includes the title, alias, intro and full text, state, publishing dates, access level, metadata, and custom field values. Restoring brings all of it back together, not just the body text.
This is a deliberate design choice. Joomla does not store the difference between versions (a "diff"); it stores a complete copy of the item each time. The trade-off is simple:
- Upside: restoring is trivial and instant - Joomla just loads one row back into the form, with no chain of diffs to replay.
- Downside: each version is a full copy, so the
#__historytable grows faster than a diff-based store would. The version limit (section 5) exists precisely to keep that growth in check.
4.3 The Version Note Travels with the Save
When you type something in the Version Note field, Joomla reads it from the submitted form (jform[version_note]), cleans it, and stores it alongside the snapshot. That is why the note is tied to one specific save and not to the item as a whole.
5. The Version Limit and Keep Forever
5.1 How Pruning Works
After each new version is written, Joomla enforces the history_limit. If you keep 10 versions and save an eleventh time, the oldest version is deleted so the count stays at 10. The logic is "keep the newest N by save date, delete the rest".
Set history_limit = 0 and Joomla keeps every version with no pruning. That is powerful but grows the table without bound on a busy site - use it deliberately, not by accident.
5.2 Keep Forever Beats the Limit
Pruning never touches a version flagged Keep Forever. The cleanup query explicitly skips any row where keep_forever = 1. So a milestone version - "the text the client signed off on" - survives even after a hundred later edits push past the limit.
To set it, tick a version in the popup and press Keep On/Off. Toggling Keep Forever does not change the version's author or date; Joomla preserves those so the flag is purely a protection marker.
5.3 Choosing a Limit
| Limit | Good for |
|---|---|
| 5 to 10 | Small sites, casual editing. Enough to undo recent mistakes. |
| 20 to 50 | Active editorial teams that revise the same articles often. |
| 0 (unlimited) | Compliance or legal cases where every version must be kept - pair with Keep Forever and a pruning task. |
6. Under the Hood: the Database
6.1 The #__history Table
Every version is one row in a single table, #__history. Its core columns are:
version_id auto-increment primary key
item_id the item this version belongs to, e.g. com_content.article.42
version_note the optional note you typed
save_date when the version was saved
editor_user_id the user who saved it
character_count length of the JSON snapshot
sha1_hash fingerprint of the snapshot (see section 7)
version_data the full item data as JSON
keep_forever 1 if protected from pruning, else 0
The most important column is item_id. It is not just a number - it is a string made of the content type alias plus the item id. An article with id 42 has item_id = 'com_content.article.42'. That one string is how Joomla knows which versions belong to which item, across every component that supports history.
6.2 Content Types and the UCM
Content History is built on Joomla's Unified Content Model (UCM). Every versionable thing is registered in the #__content_types table. Each row maps a type alias such as com_content.article to its database table and, crucially, to a JSON column called content_history_options.
That options column tells Joomla's history engine how to treat the type - in particular, which fields to ignore when deciding whether anything really changed. We look at that next.
6.3 Useful SQL
List every version of one article (replace jos_ with your real prefix):
SELECT version_id, save_date, version_note, editor_user_id, keep_forever
FROM jos_history
WHERE item_id = 'com_content.article.42'
ORDER BY save_date DESC;
Find which items have the most versions, to spot heavy storage users:
SELECT item_id, COUNT(*) AS versions
FROM jos_history
GROUP BY item_id
ORDER BY versions DESC
LIMIT 20;
Back to top7. The SHA1 Hash: No Duplicate Versions
7.1 Why Saving Twice Does Not Always Add a Version
Open an article, change nothing, click Save. You might expect a new version. Joomla creates none. The reason is the sha1_hash column.
Before storing a version, Joomla builds a SHA1 fingerprint of the snapshot. If a version with the same item and the same hash already exists, and the version note is unchanged, Joomla skips the write. No duplicate, no wasted row. This keeps the history clean and stops the limit from filling with identical copies.
7.2 ignoreChanges: Fields That Do Not Count
Some fields change on every save even when the content is identical - the modified date, the modified-by user, the hit counter. If those counted toward the hash, every save would look "different" and the deduplication would never trigger.
So before hashing, Joomla strips a list of volatile fields. The default ignoreChanges list includes:
modified modified_by modified_user_id modified_time
checked_out checked_out_time version hits path
A content type can override this list through the content_history_options JSON in #__content_types. There is also a convertToInt list (for fields like publish_up, ordering, and featured) that normalises values so a "0" and a 0 produce the same hash. The practical result: Joomla detects meaningful changes and ignores cosmetic ones.
8. Restoring in Detail
8.1 What Restore Actually Does
Restore reads the chosen version's version_data JSON, decodes it, and loads those values into the edit form. It is a load, not a save. Until you click Save, the live item is unchanged and any reader on the site still sees the current text.
This two-step design is deliberate. It lets you restore an old version, then adjust a detail or two before committing, instead of blindly overwriting the current content.
8.2 Restore Is Non-destructive
Restoring never deletes the version you came from. Imagine the timeline:
v1 Monday "First draft"
v2 Tuesday "Client edits" ← current live text
v3 ...you restore v1 and Save
After saving, the list holds v1, v2, and a new v3 that is a copy of v1. You can still jump back to v2 if the client changes their mind. Nothing is ever truly lost while it stays inside the limit and is not pruned.
8.3 The Check-out Connection
Restoring works on the open edit form, so the normal editing rules apply. The item is checked out to you while you work, and the restored data is only persisted when you Save and Close. If another editor has the article locked, you cannot restore over their session - Joomla's Check-in system still guards the row.
Back to top9. Permissions and Access
9.1 History Follows the Item, Not a Separate ACL
Content History has no permission screen of its own. Whether you may view, restore, or delete a version is decided by your permission on the parent item. The rule Joomla applies is:
If you can
core.editthe article, you can manage its history.
So an editor who is allowed to edit an article can open its Versions popup, preview, compare, restore, and delete versions. A user who cannot edit the article cannot touch its history either. There is no way to "see the history but not edit the item".
9.2 Edit-own Counts Too
The check is slightly broader than a plain core.edit. If a user has Edit Own rights and the article is one they are currently editing, Joomla also grants access to that item's history through the active edit session. Authors managing their own drafts therefore get history for their own work without needing global edit rights.
9.3 Deleting a Version
Deleting a single version uses the same permission as editing. One safeguard: a version flagged Keep Forever cannot be deleted until you turn the flag off first. This stops a careless click from wiping the one milestone you meant to protect.
Back to top10. Web Services API
10.1 Reading History over REST
Joomla's Web Services API can return the version history of an item as JSON. The Content History API view is read-only: it lists versions and their data, but you create versions by saving the item itself, not by posting to the history endpoint.
A request needs the content type alias, its type id, and the item id, and the usual API token:
curl -H "X-Joomla-Token: <token>" \
"https://example.test/api/index.php/v1/contenthistory/history?type_alias=com_content.article&type_id=1&item_id=42"
10.2 What Comes Back
Each version in the response carries the same fields you see in the backend:
id the version id
version_note the note, if any
save_date when it was saved
editor_user_id who saved it
character_count size of the snapshot
sha1_hash the fingerprint
version_data the full item data, decoded
keep_forever protection flag
This is enough to build an external dashboard that shows "who changed what, and when" across your content, or to archive snapshots into another system. Because the endpoint is read-only, you cannot accidentally corrupt the history through the API.
Back to top11. Add Content History to Your Own Extension
11.1 The Recipe
If you build a component, you can get Content History almost for free. Three steps:
1. Make your Table versionable. Implement VersionableTableInterface and return your type alias:
use Joomla\CMS\Versioning\VersionableTableInterface;
class ItemTable extends Table implements VersionableTableInterface
{
public function getTypeAlias()
{
return 'com_myext.item';
}
}
The versionable behaviour plugin checks for exactly this interface on onTableAfterStore. With it in place, your saves are eligible for versioning.
2. Register the content type. Add a row to #__content_types for com_myext.item, pointing at your table and supplying a content_history_options JSON if you want a custom ignoreChanges list:
{
"ignoreChanges": ["modified", "modified_by", "checked_out", "checked_out_time", "hits"],
"convertToInt": ["publish_up", "publish_down", "ordering"]
}
3. Add the two options. Put save_history and history_limit fields in your component's config.xml, exactly like core does. Add the Version Note field and the Versions toolbar button to your edit view. Note that core ships these fields with default="0" but turns the feature on through its install SQL (the seeded #__extensions params hold "save_history":"1"). Your own component inherits the default="0" unless you seed a value the same way, so history will be off until an admin sets Save History = Yes.
11.2 What You Get
Once those three pieces are in place, your component reuses the entire core engine: snapshots, SHA1 deduplication, the limit, Keep Forever, preview, compare, restore, the ACL rule, and the REST endpoint. You write no versioning code of your own - you only declare that your item is versionable and let the behaviour plugin do the work.
11.3 A Note on the Future
The current versioning classes (Versioning, the behaviour plugin's table-event handlers) are marked deprecated in Joomla 6, with a new versioning concept planned for a later major release. The feature and the database table are not going away; the internal API that developers call may change. If you wire history into a custom extension today, keep an eye on the migration notes for Joomla 8.
12. Content History vs Check-in vs Action Logs
Joomla has three small features that people often confuse because they all answer "what happened to my content". They do different jobs:
| Feature | Answers | Stores |
|---|---|---|
| Content History | What did this item look like before? | Full snapshots in #__history |
| Action Logs | Who did what, when, and from where? | One audit line per action in #__action_logs |
| Check-in | Who is editing this right now? | A lock in the item's own row |
They complement each other. Action Logs tell you that Alice edited the homepage at 14:00. Content History lets you see what the homepage said before her edit and roll it back. Check-in stops Bob from overwriting Alice while she is still typing. Turn all three on for a site you genuinely care about.
Back to top13. SEO, Metadata, and the Frontend
A common question: does Content History affect SEO or what visitors see? The short answer is no, and that is by design.
- Versions are backend only. They never render on the frontend. There is no public "version history" page and no duplicate URLs, so there is nothing for search engines to index.
- Restoring a version changes the live article only after you Save. At that point the normal SEO rules apply to the restored content - the metadata, alias, and text that the version held all come back together.
- Because a snapshot includes metadata fields, Content History is a quiet safety net for SEO too: if someone wipes a carefully written meta description, you can restore it without retyping.
In other words, Content History is an editorial tool, not a publishing feature. It protects your work without adding a single byte to the pages your visitors load.
13.1 Know the Limits
It also helps to be clear about what Content History is not. People sometimes expect it to behave like Git, and it does not:
- There is no branching and no merging. History is one straight line of saves per item; you cannot maintain two parallel drafts and combine them.
- The comparison is a simple field-by-field view, not a full source-style diff with inline word changes.
- It versions one content item at a time. It is not a substitute for a real site or database backup.
Think of it as editorial version control: a reliable undo-and-recover history for the content you edit in Joomla, not a developer-grade source control system.
Back to top14. Common Mistakes and Pitfalls
14.1 "There is no Versions button"
Symptom: The Versions button is missing from the article toolbar.
Fix: Save History is off for that component, or this item has no versions yet. Set Content → Articles → Options → Save History = Yes, then save the item once to create the first version.
14.2 "Old articles have no history"
Symptom: You enabled history but existing articles show nothing.
Fix: Versioning is not retroactive. History starts at the first save after you switch the option on. There is no way to recover versions of edits made before that.
14.3 "Saving again did not create a version"
Symptom: You saved twice and only one version exists.
Fix: This is correct behaviour. The SHA1 hash blocks duplicate snapshots. If nothing meaningful changed, Joomla reuses the existing version instead of writing a copy. Change a real field, or add a different version note, to force a new row.
14.4 "My milestone version disappeared"
Symptom: An important old version is gone after many edits.
Fix: It was pruned by the limit. Always press Keep On/Off on versions you must retain. Keep-Forever rows are never pruned and cannot be deleted until you unflag them.
14.5 "I restored a version but the site still shows the old text"
Symptom: Restore seemed to do nothing on the frontend.
Fix: Restore only loads data into the form. You must click Save (and clear any page cache) for the change to go live.
14.6 "The history table is huge"
Symptom: #__history has grown to hundreds of thousands of rows.
Fix: A component is set to unlimited (history_limit = 0) on a high-edit site, or you version many large items. Set a sane limit, or prune old rows with a Scheduler SQL task, keeping Keep-Forever rows safe.
15. Best Practices
If you remember only a few things from this article, remember these:
- Switch Save History on for
com_contentat least - it is the cheapest insurance you will ever buy. - Set a real limit (10 to 20 is a good start). Avoid 0 unless you also prune and use Keep Forever.
- Use Version Notes. "Before client rewrite" beats a bare timestamp every time.
- Flag milestone versions Keep Forever so the limit never eats them.
- Remember restore is a two-step action: it loads the form, you still press Save.
- History access follows item edit rights - there is no separate permission to configure.
- Pair Content History with Action Logs and Check-in for a complete "what happened" picture.
16. Quick Reference
ENABLE Content → Articles → Options → Save History = Yes
SET LIMIT Options → Maximum Versions (0 = unlimited)
OPEN VERSIONS Edit an item → Versions (toolbar)
PREVIEW Click a version's date
COMPARE Tick two versions → Compare
RESTORE Tick one version → Restore → then Save
PROTECT Tick a version → Keep On/Off
ADD A NOTE Type in the Version Note field before saving
WHERE IT LIVES #__history (item_id = com_content.article.42)
THE ENGINE plg_behaviour_versionable on onTableAfterStore
DEDUP sha1_hash, with ignoreChanges from #__content_types
PERMISSION core.edit on the parent item (no separate ACL)
SINCE Joomla 3.2 (2013)
Back to top17. Summary
Content History is one of Joomla's quietest, most useful features. Switch on a single option and every save of your content becomes a recoverable point in time, stored as a full snapshot you can preview, compare, restore, and protect.
The design is clean and predictable:
- One behaviour plugin watches every save and writes a snapshot to
#__history. - A SHA1 hash keeps duplicates out, so the history stays meaningful.
- A per-component limit prunes the oldest versions, while Keep Forever protects the ones that matter.
- Access follows the item's edit permission, so there is nothing extra to configure.
- A read-only REST endpoint exposes the same data to external tools.
It is not a backup and it is not retroactive, so switch it on early and set a limit you can live with. If you run a Joomla site where content matters - and on most sites it is the whole point - turning on Content History today means that the next time an article goes wrong, the fix is one click away instead of a long evening of retyping. If you are unsure how your site is configured, or you want versioning wired into a custom component, it pays to have someone look at it before you need it.
Back to top

Joomla specialist and Linux admin for fast, secure and scalable websites.


