Skip to content

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]].

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.

PathWhat it does
wp-content/themes/lovat-parks/inc/property-xml-feed.php:54property_xml_feed_init() — registers both URLs on init.
…:70property_xml_feed_output() — cache lookup, headers, echo.
…:107property_xml_feed_generate($park_id) — builds the XML.
…:233property_xml_feed_render_fields() — recursive ACF field renderer.
…:35property_xml_feed_get_excluded_fields() — the deny list.
URLReturns
/feed/property-feed/All published properties. Registered with add_feed().
/property-feed.xmlSame, 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=1Regenerates before serving.
  1. property_xml_feed_output() builds a cache key — property_xml_feed or property_xml_feed_park_<id> — and checks the transient.
  2. On a miss, property_xml_feed_generate() queries published property posts (filtered by park if asked), and for each one walks its ACF fields through property_xml_feed_render_fields(), which recurses into repeaters and groups.
  3. The result is stored for one hour (HOUR_IN_SECONDS) and served with Content-Type: application/xml and Cache-Control: public, max-age=3600.

Cache invalidation is event-driven as well as time-based:

HookEffect
save_post_propertyClears all feed caches.
delete_postClears 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.

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;
});
  • No constants, options or admin screen. Behaviour is code plus the property_xml_feed_excluded_fields filter.
  • 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.
  • Everything is exposed by default. Add an internal-only ACF field to the property group 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 EP code. Easy to mix up.
  • The nightly sync mass-drafts properties ([[ownership-property-listings]]) and then re-publishes them. save_post_property fires 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.xml 404s until permalinks are saved or wp rewrite flush is run. Documented in the file header.
  • There is no pagination and no limit. The feed grows linearly with stock.
  • delete_post is hooked unconditionally, so deleting any post clears the feed caches.
  • 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 whose grade_park is 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.