WordPress Full vs View: Two Git Repos, Two Different Jobs
One WordPress install, two Git repos: a full Forgejo repo for recovery, a GitHub view repo carrying only source I wrote myself.
It started as a trivial question: what should a WordPress .gitignore look like? The question slowly turned into something more fundamental: one WordPress install, two Git repositories, two completely different payloads. A full repo on Forgejo for recovery, a public view repo on GitHub for source code. The hard part was never git itself; it was deciding what belongs where.
The full repo is a recovery set, not a dump
The common assumption: a private "full" repo means every byte goes in. Two things break that assumption. First, plaintext secrets. A wp-config.php or an env file with real values stays out even from the private repo. The reason is simple: once a secret lands in git history, the first move is rotating or revoking it, not deleting the file; rewriting history requires coordination and carries real side effects[4].
Second, raw database dumps. They are not source code, they are backup artifacts: password hashes, user emails, plugin settings. So even in the full repo, *.sql files stay blocked; what enters git is an encrypted snapshot under backups/db/. Cache, logs, and other runtime files are dropped too, because they are regenerable, not recovery data.
What deliberately stays in: media in uploads/, WordPress core, even official plugins. That is the point of a size-unlimited self-hosted repo. The full repo must be able to restore the entire site, not just the code. The trade-off is weight: history grows every time media changes. But storage is cheap compared with losing assets when a server dies; media that changes daily belongs in object storage instead of being forced into git.
The view repo carries only what I wrote
The public GitHub repo gets a different slice: custom themes and plugins, mu-plugins, plus Composer manifests. Official plugins like Elementor or WooCommerce stay out, not because it is forbidden, but because they are dependencies, not source. A dependency is declared through composer.json pointing at WPackagist, the WordPress package index for Composer[5].
Elementor Pro is a different case: the package is delivered as a download after purchase, via email or the My Elementor dashboard[3], not as a wordpress.org package. The license alone means that file should never be committed anywhere.
Filling the view repo works with a deny-by-default rsync allowlist. A third-party plugin installed today cannot leak tomorrow, because it is not in the list. Order matters too, since rsync applies first-match: parent directories must be listed before their contents. Of the patterns I tried, this one is the hardest to get wrong:
+ /wp-content/
+ /wp-content/themes/
+ /wp-content/themes/<custom-theme>/
+ /wp-content/themes/<custom-theme>/**
+ /wp-content/mu-plugins/
+ /wp-content/mu-plugins/**
+ /composer.json
+ /composer.lock
Publishing to GitHub is always a single snapshot commit that references the production hash in its message, then a normal push. Never a force-push; history rewrites are exactly the operation that is expensive to coordinate and rich in side effects[4].
Page designs live in the database
One thing that gets missed when splitting repos: if the site is built with a page builder, the design does not live in files. Elementor stores page data as JSON in the wp_postmeta table, as a custom field[2]. Exporting a Template produces a *.json file that can move between installs[2].
That JSON export is a portable design artifact, so it legitimately belongs in git; the view repo may carry it as long as the content is safe to publish. But it is not a backup replacement. The database remains the single source of truth[1][2], and the rule of thumb stands: keep 3-5 recent backups in different locations[1].
In the end the two repos answer two different questions. The full repo answers: if the server dies, what do I need to rebuild? The view repo answers: if someone reads my code, what should they see? Once those two questions are separated, the ignore list and the include list simply follow.
Sources