WordPress Deployment Without Local, SSH, or FTP
When the only access you have is wp-admin in a browser, here's the playbook for deploying changes.
Decision tree
What are you deploying?
├── Edit existing theme/plugin file
│ └── Theme File Editor / Plugin File Editor (Appearance/Plugins)
├── Add new content (posts, pages, products)
│ └── wp-admin → Posts/Pages, OR custom plugin with activation hooks
├── New plugin (your own code)
│ └── ZIP it → Plugins → Add New → Upload Plugin
├── New theme (your own code)
│ └── ZIP it → Appearance → Themes → Add New → Upload (deactivate active first if same slug)
├── Just need to inject a snippet (analytics, schema, ads.txt)
│ └── "Code Snippets" or "Insert Headers and Footers" plugin
└── Bulk content (hundreds of posts)
└── WP REST API + a script, OR a custom seeder plugin you upload
Tactic 1: Theme File Editor
When: quick edit to functions.php, header.php, footer.php, etc.
Path: wp-admin → Appearance → Theme File Editor
Gotcha: If the editor is missing, your wp-config.php has DISALLOW_FILE_EDIT. Add this temporarily:
// In wp-config.php, comment out or remove:
// define('DISALLOW_FILE_EDIT', true);
(Re-add it after deploy for security.)
CodeMirror trap: Theme File Editor uses CodeMirror. If you're automating via JavaScript, setting textarea.value does NOT update CodeMirror's internal state. The save would write the OLD content.
// WRONG — only updates the underlying textarea, CodeMirror still has old value
document.getElementById('newcontent').value = newContent;
// RIGHT — use CodeMirror's API
const cm = document.querySelector('.CodeMirror').CodeMirror;
cm.setValue(newContent); // sets editor content
cm.save(); // syncs to underlying textarea
document.getElementById('submit').click();
Tactic 2: Plugin Upload (for new plugins)
When: you have a plugin packaged as a .zip.
Path: wp-admin → Plugins → Add New → Upload Plugin → Choose File → Install Now → Activate
Building the ZIP locally:
# Python is installed almost everywhere
python -c "
import os, zipfile
with zipfile.ZipFile('my-plugin.zip', 'w', zipfile.ZIP_DEFLATED) as zf:
for root, dirs, files in os.walk('my-plugin'):
for f in files:
fp = os.path.join(root, f)
zf.write(fp, os.path.relpath(fp, '.'))
"
Or PowerShell on Windows:
Compress-Archive -Path my-plugin -DestinationPath my-plugin.zip -Force
Or zip CLI on macOS/Linux:
zip -r my-plugin.zip my-plugin/
The ZIP must contain a folder with the plugin name, NOT the plugin files at the root.
Tactic 3: Use a content seeder plugin
When: you need to deploy posts, pages, custom post types, terms, options, or any database content.
Don't try to add 50 posts via the wp-admin UI. Build a small plugin that runs on activation:
<?php
/**
* Plugin Name: My Content Seeder
* Description: Activate once to create content.
*/
defined('ABSPATH') || exit;
function my_seed_on_activate() {
// Idempotent: skip if exists
if (get_page_by_path('my-page', OBJECT, 'page')) return;
wp_insert_post([
'post_title' => 'My Page',
'post_name' => 'my-page',
'post_type' => 'page',
'post_status' => 'publish',
'post_content' => 'Content here',
]);
// Set options too
update_option('my_setting', 'value');
}
register_activation_hook(__FILE__, 'my_seed_on_activate');
ZIP it, upload it, activate it — all your content lands in one click. Add a register_deactivation_hook if you want clean uninstall.
Tactic 4: Insert Headers and Footers / Code Snippets
When: you need to add <script>, <meta>, or PHP snippets without uploading code.
- Insert Headers and Footers (by WPBeginner) — UI for adding scripts and meta tags
- Code Snippets (by Code Snippets Pro) — UI for running PHP snippets safely
Both are free, install from Plugins → Add New → search. Faster than editing functions.php for one-off injections.
Tactic 5: Direct DB edits via phpMyAdmin
When: you need to fix corrupt data, change siteurl after a domain change, or manage options that aren't in any UI.
Path: Hosting panel → Databases → phpMyAdmin → run SQL.
Most useful queries:
-- Change site URL after domain migration
UPDATE wp_options SET option_value = 'https://newdomain.com' WHERE option_name = 'siteurl';
UPDATE wp_options SET option_value = 'https://newdomain.com' WHERE option_name = 'home';
-- Find a user by email
SELECT * FROM wp_users WHERE user_email = 'user@example.com';
-- Reset admin password (use with caution)
UPDATE wp_users SET user_pass = MD5('NEW_PASSWORD_HERE') WHERE user_login = 'admin';
-- List active plugins
SELECT option_value FROM wp_options WHERE option_name = 'active_plugins';
-- Disable all plugins (recovery from white screen of death)
UPDATE wp_options SET option_value = 'a:0:{}' WHERE option_name = 'active_plugins';
-- Switch active theme
UPDATE wp_options SET option_value = 'twentytwentyfive' WHERE option_name IN ('template', 'stylesheet');
Tactic 6: WordPress REST API (for content automation)
When: you want to script content creation from outside WordPress.
# Auth: create an Application Password (Users → Profile → Application Passwords)
APP_PASS="xxxx xxxx xxxx xxxx"
# Create a post
curl -X POST "https://YOUR-SITE.com/wp-json/wp/v2/posts" \
-u "USERNAME:$APP_PASS" \
-H "Content-Type: application/json" \
-d '{
"title": "Hello from REST",
"content": "Body content here",
"status": "publish"
}'
# List all posts
curl -u "USERNAME:$APP_PASS" "https://YOUR-SITE.com/wp-json/wp/v2/posts?per_page=100"
Application Passwords work with HTTPS and don't require any extra plugin since WP 5.6.
Tactic 7: All-in-One WP Migration
When: you DO have a working source (Local, staging, another live install) and want to push the entire site somewhere.
Path:
- Install plugin on source: Plugins → Add New → search "All-in-One WP Migration" → Install
- Source: Tools → All-in-One WP Migration → Export → File → download
.wpress - Destination: install same plugin → Import → Upload → upload
.wpress→ confirm overwrite - Save permalinks: Settings → Permalinks → Save (no changes, just save)
Free version limit: 512MB upload. For bigger sites:
- Use the "Export to FTP/Dropbox/GDrive" extensions (paid)
- Or split your site (export DB only, copy media via FTP separately)
Order of operations (for a real migration)
1. Backup the destination (always — All-in-One Backup → Create)
2. Edit any environment-specific files (wp-config.php) via Theme File Editor or hosting File Manager
3. Upload + activate plugins (any custom code in plugin form)
4. Upload + switch theme (Appearance → Themes → Add New → Upload Theme)
5. Run Permalinks save (Settings → Permalinks → Save)
6. Flush all caches at hosting and WordPress level
7. Test in incognito (NOT just your logged-in browser)
Verification checklist after deploy
- View source on the homepage and confirm
wp-content/themes/<your-theme>appears in CSS paths - Check the page title matches what you expect
- Test in incognito (cookies bypass cache; only incognito tells you what users see)
- Test on mobile (different cache, different user agent)
- Run
curl -sI https://YOUR-SITE.com/— confirm200 OKand check cache headers - If using a hosting panel CDN, FLUSH IT after deploy
What you cannot do without SSH
Realistic limits to know:
- Run wp-cli — needs shell access. Workaround: install "WP-CLI Login" plugin (interactive) or rely on REST API + custom plugins.
- Edit
wp-config.php— possible via File Manager in hosting panel, but NOT through wp-admin (security restriction). - Bulk file operations on uploads — slow via wp-admin. Use FTP/SFTP if available, or a Media Library bulk plugin.
- Cron jobs (real ones) — wp-cron runs in PHP per request. Real OS-level crons need SSH or hosting panel cron config.
- Server-level config (PHP version, memory limit, max upload size) — usually in hosting panel, not wp-admin.
Skill maintained at https://github.com/OmarEltak/wp-rescue-kit