In this post, I share how I turned a WordPress blog with Cloudinary assets into a mobile app using StepZen and React Native. I started this project because I wanted to help a non-profit put their WordPress.com blog onto a mobile application. This application would update when the blog did, acting as a PR channel for the organization. This would achieve the goal of making an incredibly cheap and easy way for them to reach better market saturation.

Getting set up

Using StepZen and React Native with Expo makes this process easy. I started with the react-native-stepzen repo on the stepzen-samples GitHub page. As the readme instructed, I installed Expo globally first.

npm install -g expo-cli

After cloning the above repo, I installed dependencies by running

npm install

Next, I opened the root folder of the clone with my favorite IDE VSCode:

IDE VSCode

And added my StepZen credentials to App.tsx file in the main folder.

const client = new ApolloClient({
   link: createHttpLink({
       credentials: "same-origin",
       headers: {
           Authorization: `Apikey {add_key}`,
       },
       uri: "{add_uri}",
   }),
   cache: new InMemoryCache(),
});

When you are logged in to your StepZen account, your Apikey is available on your My Account page. The URI for the GraphQL endpoint is given to you by StepZen when you deploy your schema. Mine is:

uri: "https://HeadyFlamingoMouth.stepzen.net/api/native/__graphql",

Review the StepZen docs for tutorials on how to make your own endpoint.


Why am I injecting the Apikey and endpoint directly into app.tsx and not using environment variables?

The reason we are injecting the authorization key directly into the app.tsx is because using environment variables in react-native is not secure, so we avoided it to remove any confusion. React Native apps are entirely client-side, and require an “orchestration layer between your app and the resource”. Read more on storing sensitive information here.

When you publish the app to the web, such as GitHub, remember to remove your keys from app.tsx. If you would like to orchestrate a layer between your app and StepZen, reach out to the StepZen team on their Discord.


Next, I went into the folder named StepZen and added a config.yaml file. This is the file with all your Cloudinary and WordPress API keys, names, etc. and follows the format:

configurationset:
 - configuration:
     name: cloudinary_mutation
     api_key: <your cloudinary api key>
     api_secret: <your cloudinary api secret here>
     cloud_name: <your cloudinary name here>

As the data that I am getting from WordPress is public there is no need for configuration, but this is where it would go if you did need configuration. The Cloudinary configurations can be found in the Cloudinary dashboard.

Starting the app and running queries

Next, it was time to start the app for the first time to make sure the authorizations were correct and do a few sample queries. In your terminal, run these commands in your apps root directory.

davidgriffin@davids-Mac-mini step-cloud % npm install -g stepzen-cli
davidgriffin@davids-Mac-mini step-cloud % cd stepzen
davidgriffin@davids-Mac-mini stepzen % stepzen start

You are redirected to a nifty StepZen IDE — an embedded GraphiQL browser with http://localhost:5000/api/native loaded. This is effectively the backend of the project.

StepZen embedded GraphiQL browser

I used this IDE to run sample queries before hooking up the React-native frontend. Once I confirmed I could get data from calling all WordPress posts and calling all Cloudinary images, I had complete confidence in the backend and began implementing the React Native frontend.

Now leave the browser and return to your terminal where you ran stepzen start. In that terminal, you should see your StepZen endpoint is running with the details of where your endpoint currently exists. If the endpoint did not successfully spin up, reach out to the StepZen team.

http://localhost:5000/api/native

Deploying to StepZen...... done

Successfully deployed api/native at 4:06:36 PM

Your endpoint is available at https://HeadyFlamingoMouth.stepzen.net/api/native/__graphql

As you can see above, the true endpoint where the data can be retrieved with your apikey is https://HeadyFlamingoMouth.stepzen.net/api/native/__graphql. Copy this generated endpoint with your unique account name and inject it into your App.tsx that we mentioned earlier. Add the Apikey as well if you have not already done so.

const client = new ApolloClient({
   link: createHttpLink({
       credentials: "same-origin",
       headers: {
           Authorization: `Apikey {add_key}`,
       },
       uri: "https://youruniqueaccount.stepzen.net/api/native/__graphql",
   }),
   cache: new InMemoryCache(),
});

Implementing the React Native frontend

The StepZen React Native template came with a nice built-in two-tab navigation implementation which was perfect so I kept it.

🐒➔ tree
.
├── app.tsx
└── screens
    ├── NotFoundScreen.tsx
    ├── TabOneScreen.tsx
    └── TabTwoScreen.tsx

1 directories, 4 files

I decided to use TabOneScreen.tsx as the home/blog posts page and TabTwoScreen.tsx as the Cloudinary-only gallery with the option to add on some administrative functionality in the future, like taking/adding an image to your Cloudinary account from your phone or making a post to your WordPress blog from your phone.

I started with TabTwoScreen.tsx and set up my gallery. So I needed to clean out the template boilerplate and construct my code. I then imported my query dependencies:

import { useQuery } from "@apollo/client";
import {GET_IMAGES} from "../queries/content.queries.js";

And then just inside the TabTwoScreen function, I made the Cloudinary images call to the StepZen server:

export default function TabTwoScreen() {

 const {
   data,
   loading,
   error
 } = useQuery(GET_IMAGES);
 const pics = data?.cloudinaryImages;
 const files = {pics};

 if (loading) return <Text>Almost there...</Text>
 if (error) return <Text>{error?.message}</Text>

This call returns the data and also loading and errors, if any. You can make the loading screen as intricate as you would like. I kept it simple for demonstration purposes. The returned data is of the following form.

IDE VSCode

And so I converted the data to a const pics = data?.cloudinaryImages variable constant, but only if there is data returned and then shuffled pics into an object called files, const files = {pics}. This was for future expandability, and in case I wanted to make multiple calls to different endpoints and pass them as one prop into the children.

I then wanted to return this data to a child component to render the images. I called this component Gallery, for lack of imagination, and set it in a ScrollView for ease of navigation through the images.

 return (
   <ScrollView contentContainerStyle={styles.contentContainer}>
     <Text style={styles.title}>Gallery</Text>
     <Gallery props={files} />
   </ScrollView>
 );

And then, in the components folder, I created Gallery.js, which is relatively straightforward. Pass the props in the function call, destructure the pics object from props, map the object and display the images in a ScrollView.

import React from 'react';
import {  StyleSheet, Image, ScrollView } from 'react-native';

function Gallery({props}) {

   const {pics} = props;

   return (
       <>
           <ScrollView contentContainerStyle={styles.contentContainer}>
               {Object.values(pics).map((pic, index) => {
                   return (
                       <Image
                       style={styles.image}
                       source={{uri: `${pic.url}`}}
                       key={index}
                       >
                       </Image>
                   )}
                   )}
           </ScrollView>
       </>
   )}

And of course, I had to import Gallery into tab-two:

import Gallery from '../components/Gallery';

Moment of truth, I opened a terminal from the root directory and executed:

npm start

Which opens the expo screen on your browser:

expo screen

For testing, I mostly used the Run in web Browser option, and it went well. The Gallery looks something like this:

Gallery

So far, so good!

Rendering the blog posts object

Once TabTwoScreen.tsx and the Gallery component were functioning, it was time for TabOneScreen.tsx, which was only slightly more complex.

After clearing the intro boilerplate out of TabOneScreen.tsx, I again imported dependencies needed to run the GET_POSTS query of blog posts:

import { useQuery } from "@apollo/client";
import { GET_POSTS } from "../queries/content.queries.js";

Then I called the getPosts query just inside the TabOneScreen function just as in the Gallery before:

const {
    data,
    loading,
    error
} = useQuery(GET_POSTS);

const blogPosts = data?.wordpressPosts;
// console.log('tab1 posts', blogPosts);

if (loading) return <Text>Almost there...</Text>
if (error) return <Text>{error?.message}</Text>

I rendered the blogPosts object a bit differently than in the Gallery referenced previously. In this case, I mapped the blogPosts object and sent the individual post into a Posts component with props={post} so that in the component, we can assign each post an image from my projects Cloudinary account (discussed below), and the code looks like this:

return (
    <>
        <ScrollView contentContainerStyle={styles.contentContainer}>
        <Text style={styles.title}>Home</Text>
        {Object.values(blogPosts).map((post, idx) => {
            return (
                    <Posts
                    props={post}
                    key={idx}
                    />

            )}
            )}
        </ScrollView>
    </>
);

I then created the Posts.js file in the components folder like the Gallery. In the Posts component, I imported the dependencies necessary to call getImageByPublicId.

import { useQuery } from "@apollo/client";
import {GET_IMAGE} from "../queries/content.queries.js";

And then I called it just inside the function Posts({props}), but since it is for a single resource, we called it and passed in the images publicId.

And this is where I got clever and decided the easiest way to match up posts with an image was to name the Cloudinary image the same as the blog post’s title. So that props.title is the variable passed to the image query and returns the selected image.

If there is no image with the publicId of the post title, it returns a random picture. (You could also set it to a default image.)

function Posts({props}) {

   const {
       data,
       loading,
       error
     } = useQuery(GET_IMAGE,
                  {variables: {publicId:`${encodeURIComponent(props.title)}`}}
                  );

     if (loading) return <Text>Almost there...</Text>
     if (error) return <Text>{error?.message}</Text>

   let pic = "";
   if(data?.cloudinaryImage.url) {
       pic = data?.cloudinaryImage.url;
      };
      } else {
       pic="https://picsum.photos/200/300"
   };

Finally, I rendered the title and selected image in a regular View component. As props.content is in HTML with tags I had to use a WebView.

This is desirable because your WordPress blog content lets you embed all kinds of elements that are preset (like YouTube, and Spotify, or specific images you wish to marry to the post) and link to a call-to-action site, like an e-commerce platform or donation screen.

return (
  <>
      <View style={styles.container}>
          <Text style={styles.header}>{props.title}</Text>
          <Image
              style={styles.image}
              source={{uri: `${pic}`}}
              >
          </Image>
      </View>
      <WebView
          style={styles.content}
          originWhitelist={['*']}
          source={{html: `<h1 style="font-size:65px">${props.content}</h1>`}}
      />
  </>
)

That's a wrap!

And that’s it! The app is ready for final testing and deployment to the App stores!

Creating a GraphQL API on top of multiple systems allowed the non-profit to re-use those systems in a new application on a new platform. But if that API had required a lot of development effort and time, it might not have been something they were willing to do. Using a configuration-driven approach allowed me to deliver a solution quickly and cheaply.

I would like to give a big shout-out to Sam Hill from StepZen and the entire StepZen team, who were so helpful and just a joy to work with to hash out all the little details of a project like this. You guys are the best!