— Article — № 025

025 —Architecture

CMS identification by filesystem: a short field guide

You inherit an FTP login, no documentation, and 20 minutes before the client call. The five places to look that name a CMS before you open a single PHP file.

Brass key with twine loop on cream card stamped with serial number, red wax seal in corner, on bone linen.
Hero · staged still№ 025

The FTP credentials arrive in a Slack DM with no other context. The previous agency went dark in March, the client expects a fix tomorrow morning, and nobody on the thread can tell you whether this is WordPress, Drupal, or 'something custom that Greg built in 2014.' You have an hour before the call. You are not going to install the site locally to find out.

The good news: every major PHP CMS leaves enough breadcrumbs in the docroot that a competent inspection of file structure, plus one peek at .htaccess and the database, will name it inside sixty seconds. What follows is the field guide I wish I had ten years ago, when I was charging hourly to figure this out.

The root directory tells you almost everything

The fastest tell is the top-level listing. Each major PHP CMS leaves a folder signature there that survives across versions, hosting providers, and the worst kind of 'we cleaned it up' refactor by a previous freelancer.

WordPress. Look for wp-admin/, wp-content/, wp-includes/, plus wp-config.php at the root (or, on better-secured installs, one level above the docroot). If wp-config-sample.php is sitting alongside it, the site was installed manually rather than via a one-click installer.

Drupal. Depends on the major version. Drupal 7 puts sites/, modules/, themes/, includes/, misc/ and profiles/ at the root, with a recognisable CHANGELOG.txt opening with 'Drupal X.x.x'. Drupal 8 and later move most of the framework under core/ and add a vendor/ directory. Composer-based Drupal installs (which is almost all of them after 2019) put the public docroot under web/ and the project metadata one level up.

Joomla. The folders administrator/, components/, modules/, plugins/, templates/, libraries/, media/, language/, plus a configuration.php at the root. The dead giveaway is administrator/. Nothing else in this list uses that name.

Magento 1. app/code/, skin/, js/, media/, var/, plus app/Mage.php. If Mage.php exists, you are looking at Magento 1 and you have a security problem: no security patches since June 2020, per Adobe's end-of-life notice.

Magento 2. app/etc/env.php, pub/, bin/magento, vendor/magento/. The bin/magento CLI is the easiest tell.

PrestaShop. classes/, controllers/, override/, config/defines.inc.php, plus a front controller index.php that references the PrestaShop bootstrap.

Hidden tells when the root has been moved

Sometimes a previous developer aliased the docroot to public/, web/ or httpdocs/, and what you see at the top level looks like a generic Composer project. Drop into the actual docroot and run:

ls -la | head -30
head -5 index.php

The first five lines of index.php will almost always name the framework. From memory:

// WordPress
require __DIR__ . '/wp-blog-header.php';

// Joomla
define('_JEXEC', 1);
require_once JPATH_BASE . '/includes/defines.php';

// Drupal 8+
use Drupal\Core\DrupalKernel;

// Magento 2
require __DIR__ . '/app/bootstrap.php';

// October / Winter CMS
require __DIR__.'/bootstrap/autoload.php';

If index.php is empty or only contains a redirect, the rewrite logic lives next door.

The .htaccess fingerprint

The block comments in shipped .htaccess files are unusually honest. Search for these literal strings:

# BEGIN WordPress
# Joomla! .htaccess
## Mage_Core      (Magento 1)
## Magento        (Magento 2)
# Drupal

WordPress writes its rewrite rules between # BEGIN WordPress and # END WordPress markers, and rewrites them on every Permalinks save, so the block tends to be intact. Joomla's default ships with the literal comment 'Joomla! .htaccess' near the top. Magento 2's default opens with a long block explaining the MAGE_MODE environment variable.

If a site has a custom .htaccess with none of these markers but every request is mapped to a single front controller, you are most likely looking at a Laravel or Symfony app that hand-rolled the docroot. Confirm by reading composer.json.

Reading the database without opening the CMS

File structure plus one peek at the schema removes the last 5% of doubt. Connect with mysql or your client of choice and run SHOW TABLES;. The prefixes and a few well-known names are diagnostic:

-- WordPress
wp_options, wp_posts, wp_users, wp_postmeta

-- Drupal 7
node, node_revision, users, variable, system

-- Drupal 8+
config, key_value, node__body, users_field_data, watchdog

-- Joomla (prefix randomised at install)
_users, _extensions, _menu, _session

-- Magento 1
core_config_data, sales_flat_order, catalog_product_entity

-- Magento 2
core_config_data, sales_order, customer_entity, inventory_source_item

-- PrestaShop
ps_configuration, ps_product, ps_customer

If you find a migrations table next to clearly framework-y tables (no business fields, just timestamps and class names), you are looking at a Laravel app pretending to be a CMS, or a Laravel-based CMS like October. Read composer.json to confirm.

The five-second triage cheat sheet

For when you have a screen-share open and the client is watching:

  1. ls -la the docroot. Compare against the headline folders above.
  2. head -5 index.php. The framework usually names itself.
  3. Grep .htaccess for BEGIN WordPress, Joomla, Magento, Drupal. One match and you are done.
  4. If still unclear, cat wp-config.php app/etc/env.php configuration.php sites/default/settings.php 2>/dev/null | head -30. Whichever file responds, you have found your CMS.
  5. SHOW TABLES; and scan for the prefixes above.

Most sites give themselves up at step 1. The rest fold by step 4.

Why this matters beyond the party trick

Naming the CMS quickly is the difference between billing for the fix and billing for the discovery. It also tells you which patch level is exposed: a 2018 Joomla 3.x with administrator/ still public has a very different threat model from a 2022 Drupal 9 behind a CDN. Knowing the stack inside the first sixty seconds lets you set realistic expectations on the call. 'This is Magento 1, and that is why nobody will quote you for a hotfix under €4k' is a fast, defensible sentence.

When we built Pier for legacy site work, the first thing it does after an FTP or SFTP login completes is fingerprint the docroot using exactly the markers above, so the chat interface can pre-load the right context: where the config file lives, which database the CMS uses, which paths to never touch. The MySQL editor opens already pointing at the right schema, and every change you make through Pier is recorded in the version history, so the sixty-second sniff only happens once per project.

The smallest thing you can do today: open the last unfamiliar site you inherited, run ls -la and head -5 index.php, paste the result into a comment at the top of your project notes. Next time someone hands you those credentials, you will save a meeting.

— Questions —

Can I trust the wp-content folder name as a WordPress signature?

Mostly yes. It is configurable via WP_CONTENT_DIR in wp-config.php, but in practice fewer than 5% of installs rename it. A renamed wp-content alongside wp-admin and wp-includes is still the same fingerprint.

What if /administrator/ exists but the rest does not look like Joomla?

That is a Joomla install where someone moved or chmod'd the front-end docroot. The admin still works because it is self-contained. Run head -5 administrator/index.php to confirm before anything else.

How do I tell Drupal 7 from Drupal 9 quickly?

Drupal 7 has a sites/all/ directory and a CHANGELOG.txt at the root. Drupal 8 and later have core/, vendor/ and composer.json. If you see web/ at the top with core/ inside it, you are on Composer-managed Drupal 9 or 10.

Is the database table prefix reliable enough to identify a CMS on its own?

Combined with one other tell, yes. Alone, no. WordPress prefixes are configurable, Joomla's are randomised on install, and a renamed Magento install can hide most of its file fingerprint without renaming the schema.