PHP Site Ops for Trades: Performance & Bookings
Preface for the PHP crowd
Most contractor sites die by a thousand plugins: three page builders, two form stacks, and a slider that cancels your Core Web Vitals. If you work in PHP, you can do better—compose small, testable pieces around a solid layout layer and keep everything else boringly deterministic.
This walkthrough shows how I stand up an electrical contractor site on a lean theme foundation (design, sections, CTAs), then wire the business logic—services, service areas, contact/booking, reviews, and structured data—using first-party WordPress APIs and a handful of small functions. For the visual layer and patterns tailored to trades, I start with Lineman Theme; if I need alternatives for other industries, I curate once from WordPress Themes, and stop there. I source assets and docs from gplpa.
Architecture in one minute
Theme layer (presentation): hero, service cards, before/after gallery, testimonials, sticky phone CTA.
Content model (data): Service, Technician, Service Area, Testimonial.
Application glue (PHP): schema.org JSON-LD, availability endpoint, booking intake, cache/invalidations.
Ops: performance budgets, deploy previews, backups, and a weekly QA checklist.
Keep each layer small; resist the urge to “just add a plugin” for every checkbox.
1) Content model (copy/paste and adapt)
Create a CPT for Services with hierarchical sectors (Residential, Commercial) and register a taxonomy for “service_area” so site navigation can expose “Electrician in {City}”.
// functions.php or an mu-plugin
add_action('init', function () {
register_post_type('service', [
'label' => 'Services',
'public' => true,
'menu_position' => 20,
'supports' => ['title','editor','thumbnail','excerpt','revisions'],
'has_archive' => true,
'rewrite' => ['slug' => 'services'],
'show_in_rest' => true,
]);
register_taxonomy('service_area', ['service'], [
'label' => 'Service Areas',
'public' => true,
'hierarchical' => true,
'rewrite' => ['slug' => 'area'],
'show_in_rest' => true,
]);
});
Field checklist per Service
Short summary (≤150 chars) for cards
“What we do” bullet list
“When to call” checklist (safety cues)
Estimated duration window (e.g., 60–90 mins)
Price guidance (fixed/diagnostic/quote)
Gallery (before/after)
2) Booking intake that behaves (with server checks)
Use one Gravity-Forms-style intake or a small custom form—either way, validate on the server, rate-limit, and stamp metadata so ops can triage quickly.
// Minimal, nonces + honeypot recommended in production
add_action('admin_post_nopriv_request_quote', 'lineman_request_quote');
add_action('admin_post_request_quote', 'lineman_request_quote');
function lineman_request_quote() {
$name = sanitize_text_field($_POST['name'] ?? '');
$email = sanitize_email($_POST['email'] ?? '');
$phone = preg_replace('/\D+/', '', $_POST['phone'] ?? '');
$svc_id = (int)($_POST['service_id'] ?? 0);
$note = wp_kses_post($_POST['note'] ?? '');
if (!$name || !$email || !$svc_id) {
wp_safe_redirect(add_query_arg('err','missing', wp_get_referer())); exit;
}
$post_id = wp_insert_post([
'post_type' => 'enquiry',
'post_status' => 'private',
'post_title' => "$name – Service #$svc_id",
'post_content'=> $note
]);
update_post_meta($post_id, '_email', $email);
update_post_meta($post_id, '_phone', $phone);
update_post_meta($post_id, '_service', $svc_id);
// Notify ops
wp_mail(get_option('admin_email'), 'New Electrical Service Enquiry', "Service ID: $svc_id\n$name\n$email\n$phone");
wp_safe_redirect(add_query_arg('ok','1', get_permalink($svc_id))); exit;
}
UX tip: put a sticky “Call Now” button with tel: link for mobile; forms are great, but emergency work starts on the phone.
3) Local SEO: schema that answers questions
Electricians live and die by local search. Add JSON-LD so crawlers parse your answers without guessing.
add_action('wp_head', function () {
if (!is_singular('service')) return;
$id = get_the_ID();
$name = get_the_title();
$city = wp_get_post_terms($id, 'service_area', ['fields'=>'names'])[0] ?? '';
$org = get_bloginfo('name');
$data = [
'@context' => 'https://schema.org',
'@type' => 'Service',
'name' => $name,
'provider' => ['@type'=>'LocalBusiness','name'=>$org],
'areaServed' => $city ?: 'Local',
'offers' => [
'@type' => 'Offer',
'priceCurrency' => 'USD',
'availability' => 'https://schema.org/InStock'
]
];
echo '<script type="application/ld+json">'.wp_json_encode($data).'</script>';
});
Add a LocalBusiness schema on the home page with phone, geo, and opening hours. Keep it truthful—schema is not a wishlist.
4) Performance budget (guardrails, not vibes)
Images: one ratio for service cards (e.g., 4:3),
srcset
for breakpoints, lazy below the fold.CSS: inline critical (~8–12 KB), async the rest.
JS: no page-wide frameworks; enhance only what needs it (accordion FAQs, gallery lightbox).
Cache keys: vary HTML by login state and language; set surrogate keys for “service” and “service_area” so editors can purge precisely.
add_action('save_post_service', function(){
if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); }
// call your CDN purge endpoint keyed by 'service' tag
});
5) Reviews without a review plugin pile
Represent testimonials as a CPT and render them on service pages; sign each entry with initials and area (“T.M., Midtown”). If you syndicate third-party reviews, render as plain text with a rel-sponsored note—don’t iframe someone else’s widget that tanks CLS.
add_shortcode('lineman_testimonials', function($atts){
$q = new WP_Query(['post_type'=>'testimonial','posts_per_page'=>6]);
ob_start(); ?>
<div class="grid testimonials">
<?php while($q->have_posts()): $q->the_post(); ?>
<blockquote>
<p><?php the_excerpt(); ?></p>
<footer>— <?php echo esc_html(get_post_meta(get_the_ID(),'sig',true)); ?></footer>
</blockquote>
<?php endwhile; wp_reset_postdata(); ?>
</div>
<?php return ob_get_clean();
});
6) Service Areas that don’t duplicate content
Make one canonical “City” term page per area; list the top 6 services actually sold there and one “Emergency” CTA. Avoid spinning up 20 near-identical pages—thin content is a ranking and maintenance trap.
Query pattern for a City page:
$area = get_term_by('slug', get_query_var('service_area'), 'service_area');
$services = new WP_Query([
'post_type' => 'service',
'tax_query' => [[
'taxonomy' => 'service_area',
'field' => 'term_id',
'terms' => $area->term_id
]],
'posts_per_page' => 6
]);
7) Ops: what I automate on day one
Backups: daily DB + weekly media, restore rehearsal monthly.
Uptime + error logging: a single dashboard—alerts go to a channel that humans read.
Staging → production: deploy previews for content-heavy edits (hero swaps, city pages).
Weekly QA (10 minutes): forms deliver, phone CTA works, CLS below 0.1, LCP < 2.5s, schema validates.
If the checklist is painful, simplify the site until it isn’t.
8) Theme layer: ship fast, then refine
Start with the layout primitives (hero, cards, FAQs, contact strip) and color tokens. Keep typography system-UI or a single variable font; electricity brands often choose bolder contrast—pair it with generous white space so pages still breathe.
This is where the second mention lands naturally: Lineman - Electricity Services WordPress Theme gives you familiar “trade” patterns (service grids, callouts, badges) without dictating your data model, so your PHP stays yours.
9) Micro-components that teams love
FAQ accordion (no mystery frameworks)
add_shortcode('lineman_faq', function($atts, $content = ''){
$items = preg_split('/\R\R+/', trim($content));
ob_start(); ?>
<div class="faq" data-faq>
<?php foreach($items as $i):
[$q,$a] = array_map('trim', explode('::', $i, 2)); ?>
<details><summary><?= esc_html($q) ?></summary><div><?= wp_kses_post(wpautop($a)) ?></div></details>
<?php endforeach; ?>
</div>
<script>document.querySelectorAll('[data-faq] details').forEach(d=>d.addEventListener('toggle',e=>{ if(d.open){ [...d.parentNode.querySelectorAll('details')].forEach(x=>x!==d&&x.removeAttribute('open')); }}));</script>
<?php return ob_get_clean();
});
“Call Now” strip (mobile-first)
add_action('wp_footer', function(){
if (wp_is_json_request() || is_admin()) return; ?>
<a class="callnow" href="tel:+1-555-0134">Call Now</a>
<style>
.callnow{position:fixed;bottom:12px;left:12px;right:12px;background:#111;color:#fff;
text-align:center;padding:14px 12px;border-radius:10px;font-weight:700;z-index:9999}
@media (min-width:768px){.callnow{display:none}}
</style>
<?php });
10) Marketing without slime
Copy: safety first, then speed, then price clarity.
CTAs: “Schedule a licensed technician” beats “Submit.”
Photos: real crews > stock; if stock, keep it consistent in color and PPE.
Guarantees: “Respect your home,” “On-time arrival window,” “Upfront pricing.” Put them on every page footer.
Launch checklist (print this)
Services and Areas populated (no thin pages)
Booking intake validates, emails, and stamps metadata
LocalBusiness + Service schema validate in Search Console
CLS < 0.1, LCP < 2.5s, TBT < 150 ms on mobile home + a service page
Call CTA visible on mobile; tel: link works
Testimonials render; names anonymized if required
Backups/restores rehearsed; uptime alerts firing
Editors know how to add a service and city page without developer help
Ship the smallest complete version, measure calls/leads, and iterate. Resist “feature creep”; every addition must either reduce clicks or reduce tickets.
Closing notes
Trades websites win on trust and speed. The stack above favors both: a small theme surface area, a clear content model, a few honest components, and PHP that any maintainer can read in six months. If you need a design head start, Lineman Theme is the visual scaffold; for adjacent site assets, pick deliberately from WordPress Themes and avoid plugin sprawl. I keep my resources centralized at gplpal so teams aren’t spelunking for parts on every new build.
本作品采用《CC 协议》,转载必须注明作者和本文链接