Property XML feed
An XML feed of every published holiday home for sale, with all of its ACF fields, available at /feed/property-feed/ or /property-feed.xml. It can be filtered to a single park. It exists so third parties can syndicate Lovat Parks’ sales stock without anyone exporting a spreadsheet.
The data comes from the property posts maintained by [[ownership-property-listings]].
Why it exists
Section titled “Why it exists”Sales stock is refreshed nightly from Elite Parks, so any manual export is stale within a day. A cached, self-refreshing feed lets partners pull whenever they like and always get what the site is currently showing.
The design decision that shapes it: the feed is allow-everything-except. Every ACF field on a property is emitted unless explicitly excluded, so newly added fields appear automatically. Convenient, and the reason the exclusion list matters.
Entry points
Section titled “Entry points”| Path | What it does |
|---|---|
wp-content/themes/lovat-parks/inc/property-xml-feed.php:54 | property_xml_feed_init() — registers both URLs on init. |
…:70 | property_xml_feed_output() — cache lookup, headers, echo. |
…:107 | property_xml_feed_generate($park_id) — builds the XML. |
…:233 | property_xml_feed_render_fields() — recursive ACF field renderer. |
…:35 | property_xml_feed_get_excluded_fields() — the deny list. |
| URL | Returns |
|---|---|
/feed/property-feed/ | All published properties. Registered with add_feed(). |
/property-feed.xml | Same, via an explicit rewrite rule. |
…?park=<park_post_id> | Filtered to one park. Note this is the park post ID, not the EP park code. |
…?clear_cache=1 | Regenerates before serving. |
How it works
Section titled “How it works”property_xml_feed_output()builds a cache key —property_xml_feedorproperty_xml_feed_park_<id>— and checks the transient.- On a miss,
property_xml_feed_generate()queries publishedpropertyposts (filtered by park if asked), and for each one walks its ACF fields throughproperty_xml_feed_render_fields(), which recurses into repeaters and groups. - The result is stored for one hour (
HOUR_IN_SECONDS) and served withContent-Type: application/xmlandCache-Control: public, max-age=3600.
Cache invalidation is event-driven as well as time-based:
| Hook | Effect |
|---|---|
save_post_property | Clears all feed caches. |
delete_post | Clears all feed caches. |
acf/save_post (priority 20) | Clears all feed caches when the saved post is a property. |
property_xml_feed_clear_all_cache() deletes the global transient and loops every park to delete its per-park transient — so adding a park after a cache was warmed is handled.
Excluded fields
Section titled “Excluded fields”Presentation-only fields are stripped by default: text_colour, gradient_overlay, override_current_page_name, breadcrumbs. Extend via the filter rather than editing the array:
add_filter('property_xml_feed_excluded_fields', function ($fields) { $fields[] = 'internal_notes'; return $fields;});Configuration
Section titled “Configuration”- No constants, options or admin screen. Behaviour is code plus the
property_xml_feed_excluded_fieldsfilter. - Cache TTL is hard-coded to one hour at
inc/property-xml-feed.php:94. - The feed is public and unauthenticated. Anything on a published property is world-readable.
Invariants and gotchas
Section titled “Invariants and gotchas”- Everything is exposed by default. Add an internal-only ACF field to the
propertygroup and it lands in a public feed on the next cache clear. Adding to the exclusion list is the required second step, and it is easy to forget because nothing fails. ?park=takes a WordPress post ID, while most other park-filtered code on this site keys off the EPcode. Easy to mix up.- The nightly sync mass-drafts properties ([[ownership-property-listings]]) and then re-publishes them.
save_post_propertyfires per post during that run, so the feed cache is cleared hundreds of times and rebuilt on the next request. That is functional but wasteful, and it means a request landing mid-sync can see a partially drafted catalogue for up to an hour. - Rewrite rules need flushing after first activation —
/property-feed.xml404s until permalinks are saved orwp rewrite flushis run. Documented in the file header. - There is no pagination and no limit. The feed grows linearly with stock.
delete_postis hooked unconditionally, so deleting any post clears the feed caches.
Changing it safely
Section titled “Changing it safely”- New fields need no code — they appear automatically. Check whether they should, and add to
property_xml_feed_get_excluded_fields()if not. - Field rendering is recursive; changing the element naming in
property_xml_feed_render_fields()changes the schema for every consumer. Partners parse this; treat the element names as a public contract and coordinate before renaming. - Verify after any change:
curl -s 'https://<site>/property-feed.xml?clear_cache=1' | head -40, and diff element names against the previous output before shipping. - To check the park filter, compare
/property-feed.xml?park=<id>counts against the properties whosegrade_parkis that park. - Deliberately not abstracted: the allow-everything-except approach. It was chosen so marketing can add a field and have it syndicated without a developer. The exclusion list is the price of that, and it is why the gotcha above is the first thing to check in review.
None. Not covered by the PHPUnit suite, which is scoped to inc/lovat-checkout/src. Verification is the curl check above.