Skip to content

Latest commit

 

History

History
149 lines (105 loc) · 7.64 KB

web-platform.md

File metadata and controls

149 lines (105 loc) · 7.64 KB

Tumblr's New Web Platform

Hello! In 2020, we're rolling out a new version of the Tumblr web app. It's fresh and fancy, built on React and other fun things. This documentation is a guide to how to develop on top of it - browser extensions and that sort of thing. All of the following documentation assumes you are using JavaScript in the console or in the context of a browser extension.

This is fairly short at the moment, but it's a work in progress.

How do I tell which version of the site I'm on?

A good question! This rollout is not going to happen all at once - different pages on the site will be converted at different times. There's a simple way to tell the difference, though. Just check to see if window.tumblr is defined. Note the lowercase - it's the real distinction here, as the other version of the site does define a window.Tumblr.

What can I do with window.tumblr?

getCssMap

One major difference in our new version of the site is the way our stylesheets and class names are compiled. No longer are the class names engineers write in the source code the ones that end up in the actual version of the site. This makes DOM manipulation a tricky proposal, from an extension's point of view. This function is intended to bridge that gap.

In TypeScript terms, the function is typed like this:

() => Promise<{ [Key: string]: string[] }>

So it's an async function, returning a promise that resolves to an object. That object has string keys - semantic class names - and values that are arrays of strings. Those strings in the array are the actual CSS class names as they exist on the page.

Let's have an example. Say you wanted to make every post currently on the page turn upside down. That could look like this:

window.tumblr.getCssMap().then(cssMap => {
  // Always make sure the class name you're looking for actually exists on the object!
  if (!cssMap.post) {
    return;
  }

  const classNames = cssMap.post.map(className => `.${className}`);
  const selector = classNames.join(', ');
  const posts = document.querySelectorAll(selector);

  posts.forEach(post => {
    post.style.transform = 'rotate(180deg)';
  });
});

cssMap.post in there could look something like this:

['_2OqKn', '_2-mU3', '_1yQrs', '_1Wz4U']

An array of all class names that are, in our source code, labeled as 'post'. A lot of the values are going to be arrays with a length of 1, but in some cases, like this, we have more than one style of post. That snippet flips 'em all.

languageData

If you want to avoid dealing with class names entirely, one way of querying for elements consistently might be to look for other element properties, like aria-label, that don't get hashed. Only problem there is that they are localized to each user's display language.

This object provides a map of English root strings to the equivalents in the current language. For a user looking at Tumblr in Spanish, it would look something like this:

{
  code: 'es_ES',
  translations: {
    // Truncated - it's a large object
    Edit: 'Editar',
    'Edit Appearance': 'Modificar apariencia',
    'Eh?': '¿Eh?',
  },
}

If you wanted to query for all the edit buttons on a page, that could look like this:

document.querySelectorAll(`button[aria-label="${window.tumblr.languageData.translations['Edit'] || 'Edit'}"`)

When a user has their language set to English, the translations object will be empty - hence the || 'Edit' in the example above.

apiFetch

This function is a helper, used to make requests to the Tumblr API in the exact same way all our in-house web code does.

Here's a simple example of how it would work.

window.tumblr.apiFetch('/v2/tagged', { method: 'GET', queryParams: { tag: 'furby' }}).then(response => {
  console.log(response);
})

The first parameter is the path for the API route, without the host. You can see documentation of our API routes right here.

Internally, this is using the native fetch function, and so the second parameter object accepts all keys that the native one does, with two major differences:

  1. queryParams - As in the example above, apiFetch accepts a queryParams object. This will encode the params and append them to the request URL, so you don't need to do that yourself.
  2. body - When making POST requests, apiFetch accepts an object as the body param, and stringifies it for you, unlike native fetch.

It's also important to know that, because Tumblr's API works with JSON objects, apiFetch returns the JSON response to you in the then() block of the Promise. Because it does some response processing to get that JSON, in certain cases it will reject the Promise or even throw. Here is a matrix that describes apiFetch()'s behavior:

response.headers.get('content-type') response.ok What apiFetch() does
application/javascript or application/json true Resolves the promise with the JSON of the response
application/javascript or application/json false Rejects the promise with an error object
any non-json content-type true Resolves the promise with {}
any non-json content-type false throws an error object

navigate

Another key difference from the old website is that the modern web app handles the loading of different pages on the client, rather than forcing a new document to be fetched from the server when any link is clicked. This is great for the default experience, but anchors added by third-party tools won't trigger this by default.

This function bridges that gap, allowing you to perform client-side navigations. It takes one parameter: the path of the destination page.

window.tumblr.navigate('/explore/trending');    // Loads the trending topics page
window.tumblr.navigate('/staff');               // Loads the Staff blog as a modal

For a more complete example, we can use this to construct a fancy anchor that loads the #photography hub on the client when clicked:

const handleClick = event => {
  if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
    // Allow default behaviour of modifier key + click
    return;
  }

  event.preventDefault();
  window.tumblr.navigate(event.currentTarget.href);
};

const anchor = document.createElement('a');
anchor.href = '/tagged/photography';
anchor.textContent = '#photography';
anchor.addEventListener('click', handleClick);

Please note that this function has no return value and will not throw if given an invalid argument. It also cannot facilitate navigation to pages that exist under www.tumblr.com but not within the web app. For example, a plain anchor with href="/help" will go to Tumblr's Help Center, while navigate('/help') will not.

on() and off()

These are used to add and remove callbacks for various events that the website sends. The first parameter is the event name, and the second parameter is a function to call when the event happens. This function will return true or false to indicate whether the addition or removal was successful.

Right now, navigation is the only supported event name. Here is an example of how to monitor client-side navigation changes:

const navigationHandler = () => console.log(`The page just changed to ${window.location.href}.`);
window.tumblr.on('navigation', navigationHandler);

// Later, when you no longer want to listen to navigation events
window.tumblr.off('navigation', navigationHandler);

Help

If something's not working as expected, or you've got feature requests, please create an issue in this here repo!