Django is great for building web products. WordPress is great for managing content. When you combine them into a distributed architecture, you get a system that excels in both areas.
Join Jeff Sternberg, VP Tech @Observer, as he walks through the technical architecture of the recently re-launched Commercial Observer, a leading source of news and information for commercial real estate professionals. The new site utilizes WordPress for editorial content and Django for the front end, loosely coupled together with the WordPress.com REST API. Jeff will explore the thought process behind the architecture, explain key code snippets involved in the system, and highlight some of the gotchas encountered along the way.
This presentation was originally given at the Django NYC Meetup hosted at Buzzfeed, August 12, 2015.
More than Just Lines on a Map: Best Practices for U.S Bike Routes
Django + WordPress.com REST API = Profit
1. Django
+ WordPress.com REST API =
PROFIT
Jeff Sternberg, VP Tech
Observer Media
jsternberg@observer.com
https://www.linkedin.com/in/jeffsternberg
@sternb0t
2. About me
- Lead engineer @Observer < 1 year
- NYC fintech startups ~ 2 years
- S&P Capital IQ ~ 10 years
- Once and future data scientist
- Python brings me joy, SQL makes me happy
- First code-for-pay: AppleScript
3. About Observer
- Founded in 1987
- We publish an actual
printed newspaper, the
weekly New York Observer
- Observer.com
- PolitickerNJ.com
- CommercialObserver.com
4. Commercial Observer
- Commercial real estate
- Leasing, sales, financing, construction,
infrastructure, industry players, features
- Weekly print edition
- Strong since inception 5 years ago
- Digital edition
- Needed some love
7. Setting the CO Scene
- Small in-house editorial team
- Handful of contributors (industry experts)
- WordPress codebase, launched years ago*
- Not modified much since launch
- Hosted on WordPress.com VIP
*I don’t actually know when it was launched
8. WordWhat?
Though it powers 25%
of the internet, I hadn’t
ever built anything with
WordPress.
I didn’t even know PHP.
9. WordPress is actually pretty good
- Great WYSIWYG post editor
- Easy for editors and writers to learn
- Decent media library
- Handles most publishing use cases
- Lots of useful plugins
- Large developer community
10. Except...
- Rigid data model: everything’s a Post
- Not MVC
- Plugin spaghetti
- Most WP sites are hobbyist/small blogs;
enterprise WP community is fairly small
- Locked-down VIP hosting optimized for
high traffic sites like observer.com
13. Django / WordPress integration
Option 1: Django connects to WordPress db
WordPress content db
(mysql)
WordPress web app (php) Django web app (python)
HTTP/HTML HTTP/HTML
TCP (data)
14. Django / WordPress integration
Option 1: Django connects to WordPress db
- sunlightlabs/django-wordpress
- agiliq/django-wordpress
- Or, roll your own models
- WordPress only has ~12 tables
15. Django / WordPress integration
Option 2: Coupled REST API
WordPress content db
mysql
WordPress web app (php) Django web app (python)
HTTP/HTML HTTP/HTML
HTTP/REST
16. Django / WordPress integration
Option 2: Coupled REST API
- WordPress.com REST API
- https://developer.wordpress.com/docs/api/
- WordPress.org: use WP REST API
- http://v2.wp-api.org/
17. Django / WordPress integration
Option 3: Decoupled REST API
WordPress content db
mysql
WordPress web app (php) Django web app (python)
HTTP/HTML HTTP/HTML
HTTP/REST
Django db
18. Django / WordPress integration
Option 3: Decoupled REST API
- Same WP REST APIs
- Django has its own copy of the content db
- Sync content with cron + webhooks
19. Shared db vs. REST API
Shared db
- Faster data access
- Django should be
in the same
network as WP
- Read/write?
REST API
- Django can use
separate hosting /
network
- Higher latency
- Data serialization /
deserialization
20. Coupled vs. Decoupled REST API
Coupled
- Only 1 copy of the
content db
- Site uptime
depends on both
Django and WP
Decoupled
- Can easily alter /
extend db schema
- Data sync scripts
can be tricky
- Hosting flexibility
22. WordPress.com REST API
- Sane data model
- /v1.1/sites/$site_id/posts
- /v1.1/sites/$site_id/tags
- etc.
- OAuth2 for private data (e.g. draft posts)
- Handy dev console
- Good documentation
- Some gotchas
23.
24.
25. Gotchas: WordPress.com REST API
Whitelist custom post types in theme code:
/** Allow additional post types in wp.com REST API */
function obs_rest_api_post_types( $allowed_post_types ) {
$allowed_post_types[] = 'guest-author';
$allowed_post_types[] = 'attachment';
$allowed_post_types[] = 'sponsored-post';
return $allowed_post_types;
}
add_filter( 'rest_api_allowed_post_types',
'obs_rest_api_post_types' );
26. Gotchas: WordPress.com REST API
- Custom taxonomies are not supported:
the only post terms you can fetch are
tags and categories
- So we can use post meta as a
workaround...
30. def load_posts(site_id, post_type):
# ...
# set modified_after to "continue where we left off"
latest = Post.objects.filter(post_type=post_type)
.order_by("-modified")
.first()
if latest:
params["modified_after"] = latest.modified.isoformat()
# get first page
response = requests.get(api_url, params=params,
headers=headers)
Django command (cron) continued
31. def load_posts(site_id, post_type):
# ...
while response.ok and response.text:
api_json = response.json()
api_posts = api_json.get("posts")
for api_post in api_posts:
load_wp_post(site_id, api_post) # helper function
next_page_handle = api_json.get("meta", {})
.get("next_page")
if next_page_handle:
params["page_handle"] = next_page_handle
else:
break # no more pages left
response = requests.get(api_url, params=params,
headers=headers)
32. from rest_framework.views import APIView
from rest_framework.response import Response
class LoadAPIStoryView(APIView):
def post(self, request):
try:
wp_id = int(request.POST["ID"])
except:
raise Http404("Post does not exist")
load_story.after_response(request, wp_id)
return Response({"status":
"loading wp_id: {}".format(wp_id)})
Webhook
33. import after_response
from django.conf import settings
from wordpress.wp_api import load_wp_api_one_post
@after_response.enable
def load_story(request, wp_post_id):
# let WP REST API catch up
time.sleep(1)
try:
load_wp_api_one_post(settings.WP_SITE_ID, wp_post_id)
except:
logger.exception("Fail! wp_post_id=%s", wp_post_id)
Webhook continued
34. django-wordpress-rest
This code is here:
https://github.com/observermedia/django-wordpress-rest
Contact me to contribute if interested!
(Or if you find bugs…)