Shipped Core 0.7.5 / 8 October 2026

Make every matching barber reachable

What the discovery features are for, how their custom WordPress code causes the bugs, and what private review photos mean. The changes shipped in Core 0.7.5 after independent package checks and browser verification.

Why this work comes next

Barber.Style aims to give barbers a portable public identity and help customers find them and reach a booking or contact destination. The ownership and private-photo protection slice has shipped. Discovery is the next priority in the existing sprint plan.

A complete profile is less useful if search silently leaves it out, a suburb hides its last shops, or a directory sends customers to an empty page. These changes correct those gaps before adding more integrations.

Why WordPress does not prevent these pagination bugs

WordPress handles pagination for its normal post queries. These directory pages use a custom plugin query instead. WordPress routes the request to the plugin, which searches GeoDirectory's shop data by distance from a city centre, applies service and suburb filters, counts matches, and renders its own cards and next-page links.

That design lets a city page cover shops across a metropolitan area rather than match only a city name. But the plugin must keep its page validation, database offsets, and navigation consistent. WordPress cannot automatically reconcile different page sizes or add a link that the template explicitly hides.

The defects below are in that custom application code. I have not found a recorded explanation for the original limits or conditions, so I cannot say why the previous developer chose them.

What each feature does, and how its bug happens

1. City directory pages

Who and why. A customer looking for a shop in a metropolitan area browses a city directory. The page lists nearby shops and can narrow them by service.

How it works. The plugin counts matching shops and displays 24 per page. A separate check decides whether the requested page exists.

The bug. That check used one shop per page, while the template used 24. With 25 shops, the check treated page 3 as valid because a third shop existed. The template skipped the first 48 shops and found nothing.

The fix. Both checks use 24. Page 2 shows the last shop. Page 3 returns a not-found response.

2. Suburb filtering

Who and why. A customer narrows a large city directory to a particular suburb to find a convenient shop.

How it works. The filtered list displays up to 96 shops per page. Its next-page address must retain the selected suburb.

The bug. The template explicitly showed pagination only when no suburb was selected. With 97 matching shops, it displayed 96 and offered no route to the last one. Page validation also ignored the suburb filter.

The fix. Suburb lists receive pagination, links retain the filter, and validation checks the same filtered results. Existing noindex behavior remains.

3. Finding an individual barber by location

Who and why. A customer may want a particular barber, whose personal profile is separate from a shop listing. Geographic search uses the barber's current shop association to find relevant profiles.

How it works. The public API is the structured data interface used by software. It searches personal profiles, returns a bounded page, and reports the total number of matches.

The bug. The code first collected at most 500 matching shop IDs. A barber linked to a matching shop outside that subset disappeared from both results and totals. WordPress then paginated an already incomplete set.

The fix. The database checks all qualifying published shop links before pagination. Response limits remain. This patch does not establish that the intended public interface exposes the full personal-barber discovery journey.

4. Service duration

Who and why. A shop owner records services, prices, and durations so customers and software can understand the shop's offerings. For example, a haircut may take 45 minutes.

How it works. The editor saves a numeric duration_minutes field. The API translates saved services into response data.

The bug. The translator copied only the older duration field. A correctly saved numeric duration never reached the response.

The fix. Search, shop detail, and service responses include the numeric duration and preserve older text. This repairs the data contract. It does not add booking slots or prove a new visible duration display.

What a private review photo is

This is a separate feature from discovery. Its cleanup fix shipped in Core 0.7.4. A customer writing a review of a shop can attach up to three photos. The purpose is to add visual context to the review, such as a photo of their haircut. That example explains the use case, rather than describing an actual customer upload.

"Private" describes the photo's visibility while it awaits moderation. The uploader agrees that they took the photo or have permission to share it, and that it may appear publicly on Barber.Style.

  1. The customer uploads the photo from the shop's review form. The plugin holds the file outside the public web directory. An upload that never becomes part of a review expires after one day.
  2. Submitting the review links the photo to that review and marks it pending. The administrator receives a moderation notification. Photo approval is separate from approval of the review text.
  3. A moderator approves or rejects the photo. Approval releases the file into public media and lets the site display it with the review. The code can also use an approved customer photo as the shop image when the shop has only generic placeholder images.
  4. A daily cleanup expires photos still pending after 90 days. Approved photos are outside this pending-photo cleanup.

Why cleanup was faulty. The old query selected the newest 100 pending records and only then checked their age. If 100 recent photos occupied that batch, older photos never entered the deletion loop. Repeating the job could keep selecting the same recent records.

What shipped. The query now selects overdue records first and processes the oldest first. If deletion fails, the record retains the references needed for another attempt. The fix enforces the existing retention policy. It does not establish that real customers were affected.

Shop-owner gallery uploads and a barber's portfolio have different purposes. This 90-day cleanup applies to pending customer review photos, not those galleries or portfolios.

Why the previous release also protected billing ownership

A shop owner manages a listing. A billing payer funds a subscription. Those can be different people after a listing changes hands. Transferring a listing must not give its new manager access to the previous payer's billing account.

The previous release protected that boundary for existing Stripe subscriptions. That work is separate from this discovery patch and does not decide whether to add Square.

How the patch stays small

FileResponsibility
class-location-pages.phpValidate the selected city or suburb against its actual page size. Share the suburb size with the template.
location-page.phpShow pagination for suburb results and carry the filter into previous and next links.
class-public-api.phpApply a complete geographic predicate to the specific barber query; map canonical duration alongside legacy text.

The geographic predicate is scoped to one query and removed afterward. Published-shop and current-team restrictions remain. This avoids building a growing list of shop IDs in application memory.

This slice adds no payment provider or booking integration. It does not introduce a native booking system, redesign profiles or change ownership rules.

What has been proved

22 checks against real WordPress and GeoDirectory. Ten fail on the unchanged 0.7.4 package. All 22 pass on the candidate.

The isolated fixture uses 501 synthetic shops and two personal barbers. It checks page boundaries, filter-preserving links, accurate totals, geographic pagination, state and country mismatch, explicit shop filtering, former team membership, unpublished shops and consistent service duration.

The staging runtime uses PHP 8.1.34, MariaDB 11.8.9 and WordPress 6.8.11 with the existing GeoDirectory and theme stack. A separate code review found no blocker.

These are regression examples, not measurements of affected production customers. No merchant data was changed to run the tests.

Staging HTTP returned the suburb page with the correct next link. Later release verification proved the actual Show more click on the 97-shop suburb fixture and ordinary navigation to its final card. On production, Sydney Show more expanded 24 cards to 48, page 24 displayed the final 10 shops, and page 25 returned 404.

Release verification and what remains

PR #2 is merged. All 77 live plugin files match the Core 0.7.5 release manifest. Fresh code, database and private-media backup checksums pass. Maintenance is off, photo cleanup is active, and API and page caches were invalidated. A guarded rollback package retains the previous Core 0.7.4 code.

The proposed geographic query plan was checked against production's schema for country-only and Sydney searches. Production currently has zero published personal-barber records, so this is not a realistic performance benchmark for a larger barber population.

Personal-barber discovery through the intended public interface, booking/contact click evidence and acquisition metrics remain sprint work. Authenticated production save journeys remain unverified. The 97-shop suburb boundary and numeric service duration were proved with isolated QA fixtures, rather than customer data.

Merged PR #2 and release evidence