loopress★ GitHub

Build, review and ship WordPress, in one loop.

Model your content types, write hooks and APIs in modern PHP, review every change in a PR and ship it to every environment. When the live site changes, pull it back and go around again. Your content never moves.

$ npm i -g @loopress/cliGet started

then lps project config connects your site and installs the plugin for you.

ReviewShip →Build← Pull backyour repoconfig + codeyour live sitewp-admin edits happen hereyour content never moves
the whole idea, on one hexagon.

Everything but the content, in Git.

Your repo holds how the site is built and behaves. What your client writes stays in WordPress, where it belongs.

$ tree client-site
client-site/
├── acf/
│   └── group_product_delivery.json  # field groups
├── menus/  options/  theme/  # configuration
├── cpt/  taxonomies/  meta/  roles/  # post types, taxonomies, meta keys, roles
├── hooks/
│   └── vat-notice.php  # one class per file
├── api/  apps/  pages/  patterns/  # code
├── composer.json  # plugins and themes, locked
└── loopress.json  # environments
ACF field groupsLight
Forms, menus, optionsLight
Custom post types, taxonomies, meta keys, rolesLight
Block theme stylesLight
Hooks, API routesFull
Apps, pages, block templates and patternsFull
Plugin lockfileFull
ComposerFull
Posts, pages content, orders, users, medianever

lps diff exits 0 in sync, 1 on drift. that's your CI gate.

One week, five client sites, the same loop every time.

Each site used to have its own ritual: FTP here, a plugin upload there, a note in a doc somewhere. Now they all go around the same loop.

Client site
  1. Build · Monday 9:10 · bakery

    “Can we show a delivery date, and say prices include VAT?”

    One request, two kinds of change. The delivery date is an ACF field on the product, the notice is a WooCommerce hook. Both land as files in the same repo, next to each other.

    // hooks/vat-notice.php
    use Loopress\Hooks\Attribute\Filter;
    
    class VatNotice
    {
        #[Filter('woocommerce_get_price_html')]
        public function notice(string $html): string
        {
            return $html . ' <small>incl. VAT</small>';
        }
    }

    no add_filter, no functions.php. one class per file.

  2. Review · Monday 11:30 · bakery

    Open the PR, let CI look first.

    The diff is only configuration and code. Products, orders and customers are never in it, so it stays short enough to actually read.

    $ lps diff --env staging

    exit 1 on drift, so CI says no before you do.

  3. Ship · Tuesday 14:00 · bakery

    The client approved staging.

    Push what you reviewed, then promote staging to production. Every push takes a snapshot first.

    $ lps push --env staging
    $ lps promote staging production

    same two commands on every site.

  4. Pull back · Wednesday 15:40 · bakery

    The shop manager renamed the delivery date field in wp-admin.

    Fair enough, it's their shop. The diff shows the field group moved on the live site, one pull brings the new label into the repo before anyone builds on the old one.

    $ lps acf diff --env production
    $ lps acf pull --env production --type field-groups
    $ git commit -am "chore: sync delivery date label"

    the repo follows the site, not the other way round.

  5. Ship · safety net · Friday 17:48 · bakery

    “The price looks weird on the cart page.”

    Roll the hook back to the snapshot taken before Tuesday's push. Loopress first checks nobody changed it on the site since, so the undo can't clobber someone else's edit.

    $ lps hook rollback --env production

    undo now, the proper fix waits for Monday.

  1. Build · Monday 10:00 · law-firm

    “We need an intake form, and practice area pages with the same fields.”

    Build the WPForms form and the ACF field group on staging, where you can click through them. Then pull both down: they are JSON files in the repo from now on.

    $ lps form pull --env staging
    $ lps acf pull --env staging --type field-groups
    $ git status --short
    ?? acf/field-groups/group_practice_area.json
    ?? forms/14-client-intake.json

    built in the admin, kept in Git.

  2. Review · Tuesday 9:30 · law-firm

    “Can the partners check the form before it goes live?”

    They read the PR, not a screenshot. The diff against production shows the new form and the new field group, nothing else.

    $ lps diff --env production --only form --only acf

    one question in the PR instead of three calls.

  3. Ship · Tuesday 15:00 · law-firm

    Approved, the intake goes to the new partner.

    A form push leaves notification settings alone by default, so a stray push can't redirect where submissions go. Here the new recipient is the point, so you say so.

    $ lps acf push --env production
    $ lps form push --env production --allow-notifications

    recipients are validated, the sender stays on the site's own domain.

  4. Pull back · Wednesday 10:05 · law-firm

    The client added a menu item in wp-admin, on their own.

    That is WordPress, the live site moves between your releases. The diff shows the drift, one pull brings it back into the repo, and the next Build starts from the real site.

    $ lps diff --env production
    $ lps pull --env production
    $ git commit -am "chore: sync client menu edit"

    this is why it's a loop and not a pipeline.

  1. Build · Monday 14:20 · shop

    “WooCommerce wants to update itself again.”

    Plugins are a lockfile in loopress.json. Bump the pin in the repo instead of clicking update on the site, and every environment installs the exact same version.

    $ lps plugin add woocommerce --version 9.5.0
    $ git diff loopress.json
    -    "woocommerce": "9.4.2",
    +    "woocommerce": "9.5.0",

    the update is a one-line diff.

  2. Review · Monday 14:45 · shop

    CI checks the new set before anyone merges.

    The audit checks every pinned version against a vulnerability database and WordPress.org's health signals. Status shows how far staging is from the lockfile.

    $ lps plugin audit
    $ lps plugin status --env staging

    the audit exits non-zero on a known vulnerability.

  3. Ship · Wednesday 9:00 · shop

    Staging ran the new version for a day, all good.

    Install the lockfile on staging, then promote it: production gets the versions staging actually ran, installed by Composer on the site, no SSH.

    $ lps plugin push --env staging
    $ lps promote staging production

    same plugins, same versions, everywhere.

  4. Pull back · Thursday 11:15 · shop

    Someone installed a plugin by hand on production.

    Status spots it. Keep it, and a pull pins it into the lockfile at the version running on the site. Or don't, and the next push with --prune deactivates it.

    $ lps plugin status --env production
    Active but untracked: hello-dolly
    $ lps plugin pull --env production

    keep it or prune it, never leave it untracked.

  5. Ship · safety net · Friday 16:30 · shop

    “Checkout feels slower since Wednesday.”

    Pin the previous version back. Downgrades need --force, because the old files come back but the database migrations the new version ran don't.

    $ lps plugin add woocommerce --version 9.4.2
    $ lps plugin push --env production --force

    read the changelog before you downgrade.

  1. Build · Monday 16:00 · school

    “Parents want their kid's timetable, on the site and on their phone.”

    A custom REST route, written as a plain class. The file path is the route, the bracketed segment is a parameter, and the attribute opens it to visitors.

    // api/timetable/[class_id].php
    use Loopress\Api\Attribute\Permission;
    
    class Timetable
    {
        #[Permission(public: true)]
        public function get(WP_REST_Request $request): array
        {
            $class = $request->get_param('class_id');
            return get_option("timetable_{$class}", []);
        }
    }

    answers at /wp-json/loopress-api/v1/timetable/6b.

  2. Review · Tuesday 10:00 · school

    The timetable page is a small Vue app, reviewed like the rest.

    The route file and the app's source sit in the same PR. CI builds the app and diffs the route against staging, so the reviewer sees both halves of the feature.

    $ npm --prefix apps/timetable run build
    $ lps diff --env staging --only api

    front and back in one review.

  3. Ship · Wednesday 13:00 · school

    Ready for the parents' evening.

    Push the route, then the app. The app mounts in any page with a shortcode, the office can place it themselves.

    $ lps api push --env production
    $ lps app push timetable --env production
    
    [loopress_app name="timetable"]

    apps have their own build, so they ship with their own command.

  4. Pull back · Thursday 9:20 · school

    A contractor hot-fixed the route on the server, over SFTP.

    The fix works, but only on production. The diff shows it, one pull puts it in the repo, so the next push doesn't quietly undo it.

    $ lps api diff --env production
    $ lps api pull --env production
    $ git commit -am "fix: keep the timetable hotfix"

    the hotfix lives in Git now, not just on the server.

  1. Build · Monday 11:00 · studio

    “New brand: a new accent color, and no sidebar on blog posts.”

    Global Styles are one JSON file, the Site Editor's Styles screen in text. Templates are block markup files, the same HTML the Site Editor saves.

    // theme/twentytwentyfive-global-styles.json
    {
      "settings": {
        "color": {
          "palette": {
            "custom": [
              {"slug": "brand", "name": "Brand", "color": "#ff5a36"}
            ]
          }
        }
      }
    }

    plus templates/single.html, without the sidebar.

  2. Review · Tuesday 11:00 · studio

    The client's designer reviews the PR.

    The diff covers the styles file and the template, and it includes edits someone made in the Site Editor on staging, so nothing gets overwritten by surprise.

    $ lps diff --env staging

    a color change reads as a color change.

  3. Ship · Wednesday 17:00 · studio

    Signed off.

    Theme push sends templates and parts as a child theme of the active one, then the Global Styles. The parent theme stays untouched and keeps its updates.

    $ lps theme push --env production

    the child theme is activated only when you say so, never on its own.

  4. Pull back · Thursday 14:30 · studio

    The designer nudged the heading size in the Site Editor.

    Their tweak is good, keep it. The diff shows it, one pull writes it into the styles file, and the next push doesn't undo it.

    $ lps diff --env production
    $ lps theme pull --env production

    templates have no pull: Site Editor edits show in the diff, you copy them over.

Config first.
Code later.

Both freeBoth open source, GPL

Light puts your configuration in Git. It is on WordPress.org.

Full is the same plugin with the code half switched on, with conventions instead of functions.php. lps project config installs it for you.

WordPress.org doesn't host plugins that deploy code, that's the whole reason there are two. The full story.

Loopress Light · WordPress.orgLoopress Full · loopress.dev

configuration

  • ACFlps acf
  • Formslps form
  • Menuslps menu
  • Post typeslps cpt
  • Taxonomieslps taxonomy
  • Post metalps meta
  • Roleslps role
  • Optionslps option
  • Global Styleslps theme style

your code · switched off

  • Hookslps hook
  • API routeslps api
  • Appslps app
  • Pageslps page
  • Templateslps theme template
  • Patternslps pattern
  • Pluginslps plugin
  • Composerlps composer

Open source, and yours to keep.

$ npm i -g @loopress/cli