Building this blog, part 2 of 3

Sharing WordPress posts on LinkedIn with my own small plugin

Nazmul Hasan Robin Avatar

6 min read

When I publish a post here, I want it on my LinkedIn profile too. I did not want a large social sharing plugin for one network, so I wrote the feature into this blog’s own site plugin. It is one PHP file.

This is the second post about how this blog is built. The first one covers running WordPress at /blog on a Laravel site.

What it does

  • Automatic mode shares a post the first time it is published. That includes scheduled posts, which are shared when they go live.
  • Manual mode adds a “Share on LinkedIn” link under each published post in the Posts list.
  • A post is shared once. After that the link reads “Shared on LinkedIn” and opens the LinkedIn post.
  • Pages, drafts and password-protected posts are never shared.

The share is a link post: the post title and excerpt as the text, and the post URL as the article.

Part one: the LinkedIn app

This is the part that looks harder than it is. It took me about fifteen minutes.

  1. Create a LinkedIn Page. LinkedIn will not let you create an app without linking it to a Page, even when you only want to post to your own profile. A basic company page is enough.

  2. Create the app at linkedin.com/developers/apps and select that Page.

  3. Add two products on the app’s Products tab: “Share on LinkedIn” and “Sign In with LinkedIn using OpenID Connect”. Both were granted to me straight away.

  4. Add the redirect URL on the Auth tab. This is the URL LinkedIn sends you back to after you approve. Mine is an admin-post.php URL:

    https://example.com/blog/wp-admin/admin-post.php?action=myblog_linkedin_callback
  5. Copy the Client ID and Client Secret into the plugin’s settings page.

The first product gives the w_member_social permission, which allows posting as you. The second gives openid and profile, which the plugin needs to find out your member ID.

Part two: connecting

Connecting is a standard OAuth flow with three steps.

Send the user to LinkedIn. The state value is a WordPress nonce, so the callback can check that the request started here:

$url = add_query_arg(
    array(
        'response_type' => 'code',
        'client_id'     => rawurlencode( $settings['client_id'] ),
        'redirect_uri'  => rawurlencode( myblog_linkedin_redirect_uri() ),
        'state'         => wp_create_nonce( 'myblog_linkedin_oauth' ),
        'scope'         => rawurlencode( 'openid profile w_member_social' ),
    ),
    'https://www.linkedin.com/oauth/v2/authorization'
);

Swap the code for a token. LinkedIn redirects back with a code. The callback checks the nonce and the user’s capability, then exchanges it:

$token = wp_remote_post(
    'https://www.linkedin.com/oauth/v2/accessToken',
    array(
        'timeout' => 15,
        'body'    => array(
            'grant_type'    => 'authorization_code',
            'code'          => $code,
            'redirect_uri'  => myblog_linkedin_redirect_uri(),
            'client_id'     => $settings['client_id'],
            'client_secret' => $settings['client_secret'],
        ),
    )
);

Ask who the token belongs to. Posting needs your member ID. The userinfo endpoint returns it as sub:

$profile = wp_remote_get(
    'https://api.linkedin.com/v2/userinfo',
    array(
        'timeout' => 15,
        'headers' => array( 'Authorization' => 'Bearer ' . $token['access_token'] ),
    )
);

The plugin stores the token, its expiry time and the member ID in one option. That option is saved with autoload off, so the secret and token are not loaded on every page view.

Part three: sharing a post

Sharing is one request to the ugcPosts endpoint:

$response = wp_remote_post(
    'https://api.linkedin.com/v2/ugcPosts',
    array(
        'timeout' => 15,
        'headers' => array(
            'Authorization'             => 'Bearer ' . $settings['access_token'],
            'Content-Type'              => 'application/json',
            'X-Restli-Protocol-Version' => '2.0.0',
        ),
        'body'    => wp_json_encode(
            array(
                'author'          => 'urn:li:person:' . $settings['person_id'],
                'lifecycleState'  => 'PUBLISHED',
                'specificContent' => array(
                    'com.linkedin.ugc.ShareContent' => array(
                        'shareCommentary'    => array( 'text' => trim( $title . "\n\n" . $excerpt ) ),
                        'shareMediaCategory' => 'ARTICLE',
                        'media'              => array(
                            array(
                                'status'      => 'READY',
                                'originalUrl' => get_permalink( $post ),
                                'title'       => array( 'text' => $title ),
                                'description' => array( 'text' => $excerpt ),
                            ),
                        ),
                    ),
                ),
                'visibility'      => array( 'com.linkedin.ugc.MemberNetworkVisibility' => 'PUBLIC' ),
            )
        ),
    )
);

A 201 response with an id means it worked. The plugin saves that ID in post meta, which is how it knows not to share the post again and where the “Shared on LinkedIn” link points. On failure it saves the error message instead, and the link becomes “Retry LinkedIn share”.

Part four: when to share

Automatic mode hooks wp_after_insert_post, which runs after the post and its terms and meta are saved. It hands over the post as it was before the save, so “first publish” is a simple check:

add_action(
    'wp_after_insert_post',
    function ( $post_id, $post, $update, $post_before ) {
        if ( 'publish' !== $post->post_status || ( $post_before && 'publish' === $post_before->post_status ) ) {
            return;
        }

        if ( 'post' === $post->post_type && 'auto' === myblog_linkedin_settings()['mode'] ) {
            myblog_linkedin_share( $post_id );
        }
    },
    10,
    4
);

WordPress fires the same hook when its cron publishes a scheduled post, so those are covered without extra code. Editing a post that is already live does nothing, because the status before the save was already publish.

Manual mode is a link added with the post_row_actions filter. It goes to an admin-post.php handler that checks a nonce and the edit_post capability before sharing.

Things that caught me

  • The token lasts about 60 days and an app like this cannot renew it by itself. The plugin shows a warning in wp-admin from a week before, and reconnecting is one button.
  • It posts to a personal profile, not to the Page. Posting as a Page needs a different permission that LinkedIn reviews.
  • Notices can go missing. I run a plugin that removes everything hooked to admin_notices, so my own result messages vanished. They are hooked to all_admin_notices now.
  • Test with a mocked API first. I tested the logic by filtering pre_http_request to return a fake 201, which let me check the share-once rule and the scheduled-post path without posting test content to my real profile.

For one network and one author, this is about 500 lines including the settings page, and I know exactly what it does.

Was this useful?
Nazmul Hasan Robin Avatar

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Your first comment is reviewed before it appears. After that, your comments appear straight away.