HR Hanse Holding manages properties in Hamburg and runs their portfolio on onOffice enterprise – the market-leading real estate CRM in Germany (30,000+ users, 6,000+ companies). The properties needed to appear on their WordPress website. Custom design, filters, working detail pages.
Three agencies had tried before me. None succeeded. Zero properties on the website. Detail pages returning 404. Default plugin layout that didn't match the brand.
The typical diagnosis: "The plugin doesn't work."
In reality, the plugin works fine. The configuration around it was broken.
The Starting Point: Zero Listings
The onOffice plugin for WordPress was installed, the API key entered – but not a single property showed up. Clicking a detail page link returned a 404. The standard layout didn't match the corporate design anyway.
Two root causes. Both classic. Both missed by the previous agencies.
Problem 1: API Permissions (The Silent Killer)
The API user in onOffice had "Read properties" set to "own only" instead of "all".
Small toggle, massive impact: the API only returns properties personally assigned to the API user. A technical API account typically has zero properties assigned – so the API returns an empty list.
Here's the nasty part: the API doesn't return an error.
// API response with wrong permissions ("own only"):
{
"status": { "code": 200 },
"data": {
"records": [], // <-- empty, NOT an error
"countabsolute": 0
}
}
// API response with correct permissions ("all"):
{
"status": { "code": 200 },
"data": {
"records": [ ... ], // <-- 22 properties
"countabsolute": 22
}
}
Status 200, empty list. If you don't know where the toggle is in the onOffice admin panel, you'll search for the bug in the plugin code forever.
Fix: onOffice admin → API settings → API user → Set "Read properties" to "all".
Problem 2: Permalink Structure Breaks Routing
The onOffice plugin registers custom rewrite rules for detail pages. These rules only work when WordPress permalinks are set to "Post name" (/%postname%/).
With any other setting – "Plain", "Day and name", "Numeric" – the plugin's routing logic simply doesn't kick in. Detail page links lead to 404.
Quick diagnostic: If the list view shows properties but clicking on individual listings returns a 404 → go to Settings → Permalinks → select "Post name" → click "Save Changes" once (even if nothing appears to change). WordPress regenerates the rewrite rules, and the detail pages start working.
Once both problems were fixed: 22 properties in the list view, detail pages reachable. But the default design was still generic – and that's where the real work started.
Custom Templates in templates.dist/
Here's something most WordPress developers don't know: the onOffice plugin loads its templates from a templates.dist/ directory inside the plugin folder. You place your custom template files there alongside the defaults and select them in the plugin settings.
The catch: during a plugin update, the entire plugin folder gets replaced – including templates.dist/. Your custom templates are gone.
That's exactly what happened with the previous agencies. They either:
- Lost their templates on every update, or
- Worked with CSS overrides that broke whenever the plugin's HTML structure changed
The clean approach: build custom templates and secure them with a recovery mechanism.
Project Structure
wp-content/plugins/
├── onoffice-for-wp-websites/ ← main plugin (don't touch)
│ └── templates.dist/
│ ├── estate/
│ │ ├── default_list.php
│ │ ├── default_detail.php
│ │ ├── cardgrid.php ← custom list view
│ │ └── custom_detail.php ← custom detail page
│ └── onoffice-style.css ← custom stylesheet
│
└── onoffice-template-guard/ ← recovery plugin
├── onoffice-template-guard.php
└── backup/ ← protected backup copies
└── templates.dist/
└── ...
What the Custom Templates Do
List view (cardgrid.php): 3-column card grid (responsive → 2 → 1 column) with:
- Main image with hover zoom
- Marketing type badge (e.g. "BUY" / "RENT")
- Property title, meta line, fact boxes (rooms, area)
- Price display and detail link
- Location filter via TomSelect multiselect
- Pagination via
paginate_links()
![]()
3-column card grid with location filter and pagination on verwaltung-hh.com
Detail page (custom_detail.php): Full exposé presentation with:
- Image gallery with lightbox (arrow navigation, keyboard support, image counter)
- Dynamic highlight badges via
getFieldLabel()(balcony, elevator, parking, etc.) - Key data from onOffice field configuration
- Property description with clean typography
- OpenStreetMap via Leaflet.js (more on GDPR below)
- Sticky contact person sidebar (avatar, phone, email)
![]()
Detail page: image gallery, key data, OpenStreetMap map, contact person box
The Template Guard: Surviving Plugin Updates
Custom templates in templates.dist/ work great – until the next plugin update wipes them. The solution: a custom WordPress plugin that automatically restores them.
The onOffice Template Guard hooks into upgrader_process_complete – it only fires when WordPress performs a plugin update. After every onOffice update, it checks whether the custom files are still present and restores them from a protected backup if needed.
// Simplified recovery logic (onOffice Template Guard)
add_action('upgrader_process_complete', function($upgrader, $options) {
if ($options['type'] !== 'plugin') return;
$templates = [
'templates.dist/estate/cardgrid.php',
'templates.dist/estate/custom_detail.php',
'templates.dist/onoffice-style.css',
];
$plugin_dir = WP_PLUGIN_DIR . '/onoffice-for-wp-websites/';
$backup_dir = WP_PLUGIN_DIR . '/onoffice-template-guard/backup/';
foreach ($templates as $tpl) {
if (!file_exists($plugin_dir . $tpl)) {
// Template missing → restore from backup
wp_mkdir_p(dirname($plugin_dir . $tpl));
@copy($backup_dir . $tpl, $plugin_dir . $tpl);
}
}
}, 10, 2);
In the WordPress admin there's a status page under Tools → onOffice Template Guard: which templates are present, whether the backup is intact, and a manual restore button.
The plugin doesn't run on every page load – only after updates. Zero performance overhead.
On maintainability: The Template Guard isn't a technical highlight – and that's the point. A team maintaining this site two years from now will understand what it does in thirty seconds. The admin page shows green or red. The hook only fires after updates. The backup lives in a fixed location. Sometimes the best solution is the most boring one.
GDPR: OpenStreetMap Instead of Google Maps
Property pages without a map are like exposés without an address. Google Maps is the default, but has GDPR implications in the EU: on load, IP addresses and browser data are sent to Google. This requires cookie consent, and many users decline – so the map is hidden behind a "Load external content" overlay.
The alternative: Leaflet.js with OpenStreetMap tiles.
- 42 KB gzipped
- Open source
- OpenStreetMap servers don't process personal data under GDPR
- Map is immediately visible – no consent banner, no overlay
- Coordinates come directly from the onOffice dataset (no extra geocoding needed)
For users who need navigation, there's an "Open in Google Maps" button that passes coordinates as an external link – without embedding Google Maps on the page itself.
Results
After the integration:
- 22 properties live, fully synchronised from onOffice
- List view: 3-column card grid with location filter and pagination
- Detail pages: Gallery, key data, OpenStreetMap map, contact person
- Update-safe: Template Guard auto-restores after plugin updates
- GDPR-compliant: No Google Maps, no external trackers
- Responsive: Mobile-optimised across all breakpoints
Live result: verwaltung-hh.com
Key Takeaways
If you're working on an onOffice WordPress integration:
When "the plugin doesn't work" → check API configuration first. The permission toggle between "own only" and "all" is the #1 cause of empty listings. The API returns 200 with an empty array – no error message to guide you.
Permalink structure is a prerequisite, not a detail. "Post name" (
/%postname%/) is required for the plugin's routing to work. Everything else → 404 on detail pages.Custom templates go in
templates.dist/– this is the intended way to build custom designs. Yet most developers don't know about it and resort to fragile CSS overrides.Protect your templates with
upgrader_process_complete. It sounds like overkill until the next update wipes your work.OpenStreetMap + Leaflet.js is a drop-in Google Maps replacement that eliminates GDPR complexity entirely.
Top comments (0)