| name | php-website-development |
| description | Build and maintain PHP websites with Bootstrap, MySQL, nginx, and Cloudflare — admin panels, product catalogs, inquiry systems, SEO, and performance optimization. |
| triggers | ["php website","admin panel","product catalog","bootstrap website","mysql website","nginx php","cloudflare cache","inquiry system","RFQ form","international trade website","外贸网站","后台管理","FK constraint","foreign key constraint","schema mismatch","column not found","og:image relative","canonical url http"] |
PHP Website Development
Build production-ready PHP websites with Bootstrap 5, MySQL, nginx, and Cloudflare CDN.
Architecture Pattern
project/
├── config/database.php # DB config + dynamic settings loader
├── includes/functions.php # Shared utilities (auth, sanitize, upload)
├── includes/seo.php # SEO meta + JSON-LD generator
├── templates/header.php # Shared header (nav, CSS, meta)
├── templates/footer.php # Shared footer
├── admin/auth.php # Admin auth + sidebar layout template
├── admin/index.php # Dashboard
├── admin/[module].php # CRUD pages (products, news, etc.)
├── api/[endpoint].php # JSON API endpoints
├── assets/css/bundle.min.css # Merged + minified CSS
├── assets/js/ # JS files
├── uploads/ # User uploads (deny PHP execution)
├── logs/ # Error logs + login attempts
└── scripts/ # Maintenance scripts
Critical Pitfalls
CSS Variable Naming Conflicts
PROBLEM: Multiple CSS files use different variable names for the same concept:
style-v3.css: --primary-color, --dark-color, --success-color
enhance-v3.css / product-focus-v4.css / visual-v5c.css: --primary, --accent, --gray-100, --success
If only style-v3.css defines variables, the other files' references to --primary, --accent, --gray-* resolve to empty strings, causing invisible text and broken styles.
FIX: Create a unified CSS file (e.g., color-harmony.css) that defines ALL variable variants:
:root {
--primary: #0f2b5b;
--accent: #c8a23c;
--gray-50: #f9fafb; --gray-100: #f3f4f6;
--gray-900: #111827;
--primary-color: var(--primary);
--dark-color: var(--gray-800);
--secondary-color: var(--accent);
--success-color: var(--success);
}
White Text on White Background
PROBLEM: Original CSS designed for dark backgrounds (color: white on feature-box h4/p), but background was changed to white in a later CSS file. Text becomes invisible.
FIX: When changing section backgrounds from dark to light, ALWAYS update text colors in the same change. Use !important in override CSS to ensure precedence.
Scroll Reveal Animation Fallback
PROBLEM: .reveal { opacity: 0 } elements depend on JS IntersectionObserver adding .revealed class. If JS fails or doesn't fire (slow connection, browser quirk), all content stays invisible.
FIX: Add CSS animation fallback:
@keyframes reveal-fallback {
to { opacity: 1; transform: translateY(0); }
}
.reveal { animation: reveal-fallback 0.01s 2s forwards; }
.reveal.revealed { animation: none; }
Cloudflare Cache Busting
PROBLEM: After modifying CSS/JS files, Cloudflare serves stale cached versions. cf-cache-status: HIT even after file changes.
FIX: Use query string versioning in header.php:
<link href="/assets/css/bundle.min.css?v=<?= filemtime(__DIR__ . '/../assets/css/bundle.min.css') ?>" rel="stylesheet">
Increment version manually when Cloudflare ignores filemtime: ?v=2, ?v=3, etc.
Cloudflare HTTPS Protocol Detection
PROBLEM: Behind Cloudflare Flexible SSL, $_SERVER['HTTPS'] is not set. getCurrentUrl() returns http:// breaking canonical, og:url, and all absolute URLs.
FIX: Check HTTP_X_FORWARDED_PROTO in every getCurrentUrl() definition:
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') $proto = 'https';
elseif (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') $proto = 'https';
else $proto = 'http';
Foreign Key NULL vs 0
PROBLEM: HTML <select> sends empty string → PHP (int)("") = 0 → FK constraint fails silently.
FIX: !empty($_POST['field']) ? (int)$_POST['field'] : null for all FK columns.
Schema Mismatch Silent Failures
PROBLEM: PHP code references columns that don't exist (e.g., status when table has is_read/replied). INSERT/UPDATE silently fails or throws 500.
DIAGNOSIS: Compare DESCRIBE table_name with PHP INSERT/UPDATE statements.
Missing nginx Routes
PROBLEM: New PHP pages return 404 because nginx doesn't have clean URL routes.
FIX: For each new page, add to nginx config:
location = /page-name {
try_files $uri /page-name.php?$query_string;
}
Always run sudo nginx -t && sudo systemctl reload nginx after changes.
Upload Directory PHP Execution
PROBLEM: Uploaded files in /uploads/ could contain malicious PHP.
FIX: Add nginx rule to deny PHP execution in uploads:
location ~* /uploads/.*\.php$ {
deny all;
return 403;
}
Heading Hierarchy for SEO
PROBLEM: Product cards use <h5> under <h2> section titles, skipping H3/H4. Search engines flag this as poor structure. Visual size is controlled by CSS, not heading level.
FIX: Use correct semantic level with visual class override:
<h2 class="section-title">Featured Tractors</h2>
<h3 class="h5"><?= escape($product['name']) ?></h3>
Pattern: <h{level} class="h{visual-size}"> — semantic level for SEO, visual class for display.
Perl Regex Breaking PHP Ternary Expressions
PROBLEM: Using perl -pi -e to replace hardcoded English text with t('...') calls fails when the text is inside PHP ternary expressions:
<?= $action === 'add' ? 'Add Product' : 'Edit Product' ?>
<?= $action === 'add' ? '<?= t('Add Product') ?>' : '<?= t('Edit Product') ?>' ?>
FIX: For ternary expressions, use direct file edit (patch tool or manual) instead of perl regex:
<?= $action === 'add' ? t('Add Product') : t('Edit Product') ?>
Safe to use perl for simple HTML text outside PHP blocks: >Text< → ><?= t('Text') ?><
Wrong Include Path in New Pages
PROBLEM: New pages created by subagents sometimes use __DIR__ . '/includes/header.php' instead of __DIR__ . '/templates/header.php', causing 500 errors.
FIX: Always verify include paths in new PHP files:
grep -n 'include\|require' /path/to/new-page.php
Should show /templates/header.php and /templates/footer.php, not /includes/.
Admin Panel Architecture
Shared Auth Template (admin/auth.php)
Every admin page includes this single file which provides:
- Login check via
requireLogin()
- Database connection (
$db)
- CSRF token (
$csrfToken)
- Flash messages (
$flashMsg, $flashType)
- Complete sidebar navigation with active state
- Header with user info
- Opens
<div class="content-area">
Each page sets $pageTitle and $currentPage before including auth.php:
<?php
$pageTitle = 'Products';
$currentPage = 'products';
require_once __DIR__ . '/auth.php';
echo '</div></div><script src="bootstrap.bundle.min.js"></script></body></html>';
Admin i18n (Language Switching)
Add Chinese/English switching to the admin panel:
Files:
admin/lang/zh.php — Chinese translations (200+ terms)
admin/lang/en.php — English (identity map)
admin/lang_helper.php — t($key) function + session-based language toggle
Implementation:
function t($key) {
static $lang = null;
if ($lang === null) {
$langCode = $_SESSION['admin_lang'] ?? 'zh';
$langFile = __DIR__ . '/lang/' . $langCode . '.php';
$lang = file_exists($langFile) ? require($langFile) : [];
}
return $lang[$key] ?? $key;
}
In auth.php sidebar footer: Add language toggle buttons:
<a href="?lang=zh" class="btn btn-sm <?= ($langCode==='zh')?'btn-primary':'btn-outline-secondary' ?>">中文</a>
<a href="?lang=en" class="btn btn-sm <?= ($langCode==='en')?'btn-primary':'btn-outline-secondary' ?>">EN</a>
Coverage: Translate sidebar nav, page titles, table headers, form labels, button text, flash messages, empty-state messages, status badges. Use t() calls in all admin pages.
Database-Driven Settings
Load site settings from DB into PHP constants at startup:
function loadSiteSettings() {
$db = getDBConnection();
$settings = $db->query("SELECT setting_key, setting_value FROM settings")->fetchAll(PDO::FETCH_KEY_PAIR);
if (!defined('SITE_URL')) define('SITE_URL', $settings['site_url'] ?? 'https://default.com');
}
Call loadSiteSettings() in functions.php after require_once database.php.
Inquiry/RFQ System Pattern
- Database table:
inquiries with status enum (new/replied/quoted/closed)
- API endpoint:
api/inquiry.php — POST JSON, validate, insert, return JSON response
- Product page form: Embedded RFQ form with country dropdown, pre-filled message
- Admin page: List with status filters, detail view, status update, admin notes
- JavaScript:
fetch() to API, show success/error alerts, WhatsApp quick link on success
Full pattern: See foreign-trade-operations skill → references/rfq-inquiry-system-pattern.md
Business Pages Pattern
For international trade sites, create these additional pages:
Export/Trade Process (export.php)
- 6-step export process (Inquiry → Quotation → Confirmation → Production → QC → Shipping)
- Trade terms cards (FOB/CIF/EXW with descriptions)
- Payment terms (T/T 30%+70%, L/C, Western Union)
- Shipping info (container sizes, packaging, port, delivery by region)
- FAQ accordion (Bootstrap collapse)
Why Choose Us (why-us.php)
- Company strengths (years, factory, capacity, countries)
- Quality assurance process (4 steps)
- Certifications (dark gradient bg with glassmorphism cards)
- Global services (training, parts, support, installation)
- Customer testimonials
Navigation Additions
- Add "Why Us" and "Export" links to navbar
- Add "Get Quote" button (gold accent) linking to /contact
- Add WhatsApp button (green) linking to wa.me/{number}
Navigation Active State
Each page sets $currentPage before including header.php. The header uses it to highlight the current nav item:
<a class="nav-link<?= ($currentPage ?? '') === 'about' ? ' active' : '' ?>" href="/about">About Us</a>
CSS underline effect on active/hover (in bundle):
.nav-link::after { content:''; position:absolute; bottom:0; left:50%; width:0; height:2px;
background: linear-gradient(90deg, var(--primary), var(--accent)); transition: all 0.3s ease;
transform: translateX(-50%); border-radius: 1px; }
.nav-link:hover::after, .nav-link.active::after { width: 60%; }
Contact Info Consistency
PROBLEM: Header top bar has hardcoded phone/email, footer uses PHP constants, contact page has different values. Social links use # (dead links).
FIX: Use PHP constants from config/database.php everywhere:
<span><i class="fas fa-phone"></i> <?= CONTACT_PHONE ?></span>
<span><i class="fas fa-envelope"></i> <?= CONTACT_EMAIL ?></span>
<a href="<?= SOCIAL_FACEBOOK ?>" target="_blank" aria-label="Facebook"><i class="fab fa-facebook-f"></i></a>
Never hardcode phone numbers, emails, or social URLs — always use the constants.
Footer Additions
- Trade terms row (FOB/CIF/EXW)
- Payment methods (T/T, L/C, Western Union)
- Certification badges (pill style)
nginx Routes
location = /export { try_files $uri /export.php?$query_string; }
location = /why-us { try_files $uri /why-us.php?$query_string; }
CSS Build Pipeline
Create scripts/build-css.sh that runs a PHP minifier:
- Concatenate CSS files in order: base → enhance → product-focus → visual → responsive → color-harmony
- Remove comments, collapse whitespace
- Output to
assets/css/bundle.min.css
- Use
filemtime() for cache-busting version
CSS Bundle Deduplication
PROBLEM: Over multiple iterations, similar CSS files accumulate (style-v2.css, style-v3.css, style-responsive.css) with identical :root variables and base styles. Concatenating them creates a bundle with the same rules repeated 2-3 times (58KB+ of bloat).
DIAGNOSIS: Check for duplicates before rebuilding:
cd assets/css && md5sum style-*.css visual-*.css
FIX: Rebuild bundle with only unique source files:
cat style-responsive.css enhance-v3.css product-focus-v4.css visual-v5c.css > bundle.min.css
perl -0777 -pe 's|/\*[^*]*\*+(?:[^/*][^*]*\*+)*/||gs' bundle.min.css | \
perl -pe 's/\s+/ /g' | perl -pe 's/^\s+//; s/\s+$//' > bundle.min.tmp && mv bundle.min.tmp bundle.min.css
Archive duplicates: mv style-v2.css style-v2.css.bak (don't delete until verified).
color-harmony.css Must Be Explicitly Loaded
PROBLEM: color-harmony.css defines brand colors (--primary: #0f2b5b, --accent: #c8a23c) but is NOT included in the bundle. The bundle's :root has generic blue --primary-color: #2563eb. Result: site renders with wrong brand colors.
FIX: Add AFTER bundle.min.css in templates/header.php (later file wins in CSS cascade):
<link href="/assets/css/bundle.min.css?v=<?= filemtime(__DIR__ . '/../assets/css/bundle.min.css') ?>" rel="stylesheet">
<link href="/assets/css/color-harmony.css?v="<?= filemtime(__DIR__ . '/../assets/css/color-harmony.css') ?>" rel="stylesheet">
Verify with browser console: getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() should return #0f2b5b.
Security Checklist
Performance Optimization
- CSS bundle (5 unique files → 1, ~38KB after deduplication)
- nginx gzip compression
- Browser caching: images 30d, CSS/JS 7d, fonts 30d
- Image lazy loading (
loading="lazy")
aspect-ratio for responsive images instead of fixed heights
- Preconnect for external CDNs (Google Fonts, Bootstrap, Font Awesome)