Post to Dev.to & Medium through API

Intro

Over the past year i have started to enjoy Blogging and hopefully soon Vlogging, but one thing has driven me crazy whilst trying to drive more traffic to my site and twitter page was copying and pasting my posts to Hashnode, Medium and Dev.to..... enough was enough, time to setup the API posting

First of all i need to say thank you to someone and also acknowledge where i found some help in order to get the Dev.to API working with my Laravel application.

So first of all thanks to Tom, a work colleague of mine who is only just starting to look at tech Twitter but is an absolute genius of web development!, Tom helps me daily without being judgemental and can build absolutely anything with Laravel!, hes a top bloke and you can all expect amazing things from him!

The second person i want to give credit to is Cody Bontecou for writing the article i came across for posting to Dev.To via API, although it didn't give me the how, it definitely gave me the where to find the info to post to Dev.to via API.

So without wasting anymore time lets get into how to post to Dev.to from a Laravel Application at the click of a button!.

DEV.TO

I've had two twitter accounts over the years... one that got hacked by someone who was pretending to be a very attractive Japanese woman looking for a "wealthy white man" to settle down with and my latest one. When i joined the "Tech Twitter" scene all i wanted to do was help people and learn from people... and a few of my first followers introduced me to Dev.to... i have since discovered Hashnode and Medium but Dev.to was "there for me" at the beginning so i'll be covering that first!

Lets get started!

So first of all, Lets get our API keys from Dev.to go to at https://dev.to/settings/account

and scroll down to DEV Community API Keys

We will need this soon.

Now i could go through the testing stage of this API using the Insomnia app but i'm going straight into the Laravel bit.

Livewire Component

I'm using Livewire as i want to be able to add some more components to the admin page i'm setting up.

so open up your terminal and type:

php artisan livewire:make SocialPosts

(I'm calling it SocialPost.php as we will expand to use Hashnode and Medium in the same controller)

This will make you a view and a controller.... in this tutorial we are going to look at the Controller more than the view... you can do what ever you want with the view, but the Controller follows a pretty solid process.

The View component...

as previously stated this consists of two parts... a view and a Livewire controller. so in my Example i want to see the posts on my website that i can post to Dev.to....

I'm using the amazing Laravel Wink so i want to use that to grab my latest 3 posts and display them on my "admin page"

the Livewire controller comes with a function as standard which is called render... this is the first function hit when the <livewire:dev-to-posts/> is called, and the information displayed is based on the content of the "render" function.

so here is the view i get when i log into my admin screen on my blog.

and the controller function to do this is incredibly simple and looks like this:

public function render()
{
    $latestPost = WinkPost::with("tags")
        ->live()
        ->orderBy("publish_date", "DESC")
        ->simplePaginate(3);
    return view('livewire.dev-to-posts', [
        "posts" => $latestPost,
    ]);
}

So what's this function doing?

Well quite simply its looking for all WinkPost with Tags that are live (Published) and orders it by the latest post first and then paginates it by 3 posts.

It the returns the view with the $posts variable.

The front end is just as simple... We simply @foreach through the posts passed through to the blade and display the posts to the admin, the Blade code is as follows (i have removed the classes as they are all custom tailwind classes and would be irrelevant to this tutorial). Also i know the Anchor Tags don't have anything on them yet, were going to update that shortly.

@foreach($posts as $post)
    <section class="mb-2 mt-2">
        <div>
            <div class="flex flex-wrap">
                <div class="w-full lg:w-3/4">
                    <div class="relative flex flex-col h-full lg:p-6 ">
                        <h3>
                            {{ $post->title }}
                        </h3>
                        <div class="flex flex-row">
                            <p class="pb-6 text-white text-justify">{{ $post->excerpt }}</p>
                        </div>
                        <div class="flex flex-row">
                            <p class="pb-6 text-fuchsia-400 text-justify">{{ implode(', ', $post->tags->map->name->toArray()) }}</p>
                        </div>

                        <div class="flex flex-wrap items-center mt-4 ">
                                <a href="#" >
                                    Post to Dev.tp</a>
                                <a href="#" >
                                    Post to Medium</a>
                                <a href="#" >
                                    Post to Hashnode</a>
                            </div>
                        </div>
                    </div>
    
                </div>
            </div>
        </section>
    @endforeach
    
    <div class="mx-auto mb-8">{{ $posts->links() }}</div>

Whats being passed through?

So lets dd the $post variable in the view to see what were actually getting.

({{ dd(posts) }})

So the DD is bringing back a collection of post arrays which include all of the info required to display, excerpts, Cover image and Title ETC. We need to foreach through them in order to get the individual information, as a completely un-styled example you can do the following in your blade;

@foreach($posts as $post)
    {{ $post->title }}
    {{ $post->excerpt }}
    {{ $post->slug }}
    {{ implode(', ', $post->tags->map->name->toArray()) }}
@endforeach

If i DD the above $post variables inside the foreach i get the following

"Elementary OS - Dev Environment Setup"

"So every now and then i see something that catches my eye. Recently that seems to be Linux Distributions, over the last few months i have changed my distribution's a few times to try and find one i'm comfortable with. I moved from my iMac (getting a bit sluggish) to Ubuntu originally and it worked but i didn't "enjoy it", i then tried Manjaro, then Pop!_OS and then finally thought id have a go with Elementary OS. ◀"

"valet-linux-elementary-os"

"Linux, PHP, MySQL"

Note: I'm using Laravel Wink by Mohamed Said and its brilliant, it takes out the stress of building the blog functionality yourself (although that is quite fun to do also :P)

The buttons

For the buttons we are going to "Livewire them up" to make it easier to call a Public function from our Livewire controller which also means we don't need the Routes in the web.php file.

So i want the Post to DEV.TO button to call a function and post to DEV.TO, so i'll name the button now and make my controller function to match it next, Also from the previous DD we saw that the slug was available in the $post variable which were going to use to pass the slug/id through to the Livewire Function when we click "Post to DEV.TO"

<button wire:click="postToDevTo('{{ $post->slug }}')"
        class="inline-flex items-center px-6 py-3 text-base font-semibold md:mb-2 lg:mb-0">
    Post to DEV.TO
</button>

Lets make a service...

i decided i didn't want to add the API key directly to my Controller and decided to add it as a Config Item.. so how do we do this? simple.. open up the services.php from the config/services.php and add the following lines

'devto' => [
    'api-key' => env('DEV_TO_API_KEY'),
],

and then in your .env file add the following line to the bottom:

DEV_TO_API_KEY="yourDev.TO API key from earlier"

that's your Config item set up!

Next, Lets build our function!

The three Functions we will eventually build will be fairly similar across the board, with the exception of the Hashnode API which uses GraphQL and requires a bit of work to be used with Guzzle.

So to start with when we made the button with the wire:click we past the $post->slug to the function which means we can just call $slag inside our SocialPosts.php like so:

public function postToDevTo($slug)
{
  // Code will go here
}

Now that we have the function and the slug we can start building everything up.

$post      = WinkPost::whereSlug($slug)->firstOrFail();
$postImg   = "https://www.raspada-blog.co.uk" . $post->featured_image;
$converter = new HtmlConverter();
$converter->getConfig()->setOption('strip_tags', true);
$postBody = $post->body;
$stringsReplaced = sanitizeBody($postBody);
$markdown = $converter->convert($stringsReplaced);

So whats happening?

  • The $post is grabbing all information about the post using the $slug as its reference.
  • $postImg is building up the URL for the image, so when we pass it over to Dev.to it displays correctly.
  • $converter - Im using this as when i build my site i set Wink to use Rich text instead of markdown which is HTML in the DB and i need Markdown for all 3 other blogging platforms. You can checkout the converter i used here if you need it: https://github.com/thephpleague/html-to-markdown
  • $converter->getConfig()->setOption('strip_tags', true); is just telling the Converter to strip out any standard tags.
  • $postBody = $post->body is just making it a sing array instead of a key pair as I'll be passing it to my helper next.
  • $stringReplaced is going to be the variable for my sanitizeBody Helper (I'll add the contents of my helper below.
  • $markdown is used to turn the HTML code left into Markdown with the converter.

One of the weird things i learnt when trying to work with the converter and the Rich Text editor was, the converter didn't always strip the tags out. This meant i needed to do a SHED load if str_replace (17 in total)... and this just looked a bit horrible in my controller, so i added it to my helpers.php file, if you want to use my helper the code is below:

if (!function_exists('sanitizeText')) {
    function sanitizeBody($postBody)
    {
        $html1    = str_replace("src=\"/storage/wink", 'src="https://www.raspada-blog.co.uk/storage/wink', $postBody);
        $html2    = str_replace('<pre class="ql-syntax" spellcheck="false">', '<pre> <code>', $html1);
        $html3    = str_replace('<span class="hljs-keyword">', '', $html2);
        $html4    = str_replace('<span class="hljs-string">', '', $html3);
        $html5    = str_replace('<span class="hljs-meta">', '', $html4);
        $html6    = str_replace('<span class="hljs-title">', '', $html5);
        $html7    = str_replace('</span>', '', $html6);
        $html8    = str_replace('</pre>', '</pre> </code>', $html7);
        $html9    = str_replace('<span class="hljs-class">', '', $html8);
        $html10   = str_replace('<span class="hljs-function">', '', $html9);
        $html11   = str_replace('<span class="hljs-params">', '', $html10);
        $html12   = str_replace('<span class="hljs-number">', '', $html11);
        $html13   = str_replace('<span class="hljs-attr">', '', $html12);
        $html14   = str_replace('<span class="hljs-attribute">', '', $html13);
        $html15   = str_replace('<span class="hljs-built_in">', '', $html14);
        $html16   = str_replace('<span class="hljs-variable">', '', $html15);
        $html17   = str_replace('<span class="hljs-comment">', '', $html16);
        return $html17;
    }
}

Gross isn't it! But it makes my controller look neater.

Now onto the API

Were going to be using GuzzleHttp to send our post across to Dev.to. This is probably the most difficulty i had in posting to dev.to with the API because i didn't know how to use Guzzle... but essentially its split into two parts...the header with the API key and content type and then the main JSON payload.

The payload itself was pretty simple to work with as soon as you understand how Guzzle/http uses json.

you need to specify the client with the headers and then the response with some sections in it, Mainly telling guzzle its JSON.... i missed this out when trying to do it originally. but reading through the Guzzle Docs i got there in the end.

The standard parts required to post to Dev.To are Title, Published, body_markdown, main_image and Tags, and in Laravel when you put it all together you get this:

$client   = (new \GuzzleHttp\Client([
    'headers' => [
        "api-key"      => config('services.devto.api-key'),
        "Content-Type" => "application/json"
    ]
]));
$response = $client->post('https://dev.to/api/articles',
    ['json' =>
         ["article" =>
              [
                  "title"         => $post->title,
                  "published"     => true,
                  "body_markdown" => $markdown,
                  "main_image"    => $postUrl,
                  "tags"          => [implode(', ', $post->tags->map->name->toArray())]
              ]
         ]
    ]
);

Now if you look at the request above, you can see the API Config item we setup earlier.

Now put it all together and you get

public function postToDevTo($slug)
{
    $post      = WinkPost::whereSlug($slug)->firstOrFail();
    $postImg   = "https://www.raspada-blog.co.uk" . $post->featured_image;
    $converter = new HtmlConverter();
    $converter->getConfig()->setOption('strip_tags', true);
    $postBody = $post->body;
    $stringsReplaced = sanitizeBody($postBody);
    $markdown = $converter->convert($stringsReplaced);

    $client   = (new \GuzzleHttp\Client([
        'headers' => [
            "api-key"      => config('services.devto.api-key'),
            "Content-Type" => "application/json"
        ]
    ]));
    $response = $client->post('https://dev.to/api/articles',
        ['json' =>
             ["article" =>
                  [
                      "title"         => $post->title,
                      "published"     => true,
                      "body_markdown" => $markdown,
                      "main_image"    => $postImg,
                      "tags"          => [implode(', ', $post->tags->map->name->toArray())]
                  ]
             ]
        ]
    );
    toastr()->success('Success, Posted to Dev.to');
    return redirect()->to('/admin');
}

Now when you click the button for "Post to DevTo" it will hit the function we have built and send the post across.

Medium API

The initial setup of the Function is exactly the same as the devto one the only difference is you need to get your Author ID from an API call first. I use Insomnia client to test with usually as its easy to use, you can use whichever client suits your needs.

With your client you need to make a GET Request from the https://api.medium.com/v1/me endpoint with a few extra bits. Using Insomnia as my example i add a Header of Content-Type with a value of application/json (see screenshot)

Now before we move on to the Bearer Tap be need to go grab our access token from Mediums website. Simply login to you account, Click on your avatar top right and select settings from the drop-down. Once the page loads, scroll down to the integration token section and generate your token.

Copy the generated token as we will need that in a second.

Back in Insomnia Click on the "Auth" tab and it will display a drop-down for you. From this list, Select Bearer. All you need to do here is paste the long integration token from Mediums website into the TOKEN field and then press send.

{
  "data": {
    "id": "THIS-IS-YOUR-AUTHOR-ID",
    "username": "itmike2018",
    "name": "Mike Jones",
    "url": "https://medium.com/@itmike2018",
    "imageUrl": "https://cdn-images-1.medium.com/fit/c/400/400/0*KG_bikEM6ojnAGif"
  }
}

the only thing we need from this is the id which is what we will use in the next stage of posting to Medium.

For the sake of Time i'm going to post the entire function for Medium here as i think you'll get the idea from what we discussed previously.

public function postToMedium($slug)
{
    $post      = WinkPost::whereSlug($slug)->firstOrFail();
    $converter = new HtmlConverter();
    $converter->getConfig()->setOption('strip_tags', true);
    $postBody = $post->body;
    $stringsReplaced = sanitizeBody($postBody);
    $markdown = $converter->convert($stringsReplaced);

    $client   = (new \GuzzleHttp\Client([
        'headers' => [
            "Authorization" => 'Bearer ' . config('services.medium.bearer'),
            "Content-Type"  => "application/json"
        ]
    ]));
    $response = $client->post('https://api.medium.com/v1/users/THIS-IS-YOUR-AUTHOR-ID/posts',
        ['json' =>
             [
                 "title"         => $post->title,
                 "contentFormat" => 'markdown',
                 "content"       => $markdown,
                 "tags"          => [implode(', ', $post->tags->map->name->toArray())
                 ]
             ]
        ]
    );
    toastr()->success('Success, Posted to medium!');
    return redirect()->to('/admin');
}

In order to submit your post to medium you need to make a POST request to https://api.medium.com/v1/users/YOUR-AUTHOUR-ID/posts changing out the YOUR-AUTHOUR-ID with the ID we got earlier on.

And that's it! as you can see i added the bearer token to a config item also to make things look a little neater.

Final Words

I am going to be writing one up for Hashnode but I'm still ironing out a kink with the Tags submissions, The rest of the submission works perfectly... just not the tags. Also there may be cleaner ways to make these Functions but im still on my learning Journey.

if you found this post useful please consider following me on twitter @skino2020 and if you found it really helpful consider buying me a coffee here.