All posts by david

How to Post a Blog-Style Article on LinkedIn

I believe in the value of having a blog built into a business website, but there is also undeniable value in taking advantage of the publishing opportunities built into a couple of social networking sites: LinkedIn and Medium.

When you blog on your own site, you are enriching your site with content, giving visitors more insight into your business and business philosophy, and boosting your site’s search engine optimization score. In other words, you are trying to bring people to you and give them something to read when they come to you.

When you post on LinkedIn or Medium, you are going where the people are — or at least a rich concentration of people you might want to reach. These two approaches can work together, where you post something on LinkedIn or Medium that links back to your site’s blog or other content on your own site.

LinkedIn is particularly interesting for reaching an audience of people who are trying to get a new job or advance in their profession. I wrote this post partly as a guide for a friend who is starting a professional coaching business.

Because LinkedIn has been around since 2002 and most people know it as the social network stocked with interactive resumes and people sending “I would like to add you to my professional network” messages to each other, not everyone knows that it now essentially contains a blogging platform that is available to them.

As of the redesign that arrived in late 2016, here is where you find the blogging function at the top of the LinkedIn home screen:

LinkedIn article link
Link to “write an article”

You still have the option of writing a quick status post, which can optionally include a link, a photo, and one or more references to other users (type the @ symbol followed by the first few letters of the person’s name to get LinkedIn to search your contacts). This is useful for sharing something that would be interesting to people in your network, without taking the time to write an essay about it.

LinkedIn status post

When you click “write an article,” you instead get taken to a full screen editor that allows you to write a headline and the body of a post, with typical word processing controls for marking something bold or italic or adding bullets or numbered lists. If you copy and paste from Microsoft Word or another word processor, the formatting should translate fairly well (check it over carefully for any formatting or alignment glitches you might need to correct).

LinkedIn encourages you to add a large feature photo or image to be associated with your post, which you do by clicking the region at the top of the editor (the gray box with the + sign and image icons).

LinkedIn article editor

You can also add photos, video, and other media in the body of a post.

Much like the WordPress editor, the LinkedIn editor makes it relatively simple to add embedded media such as YouTube videos. Just paste in the url (web address) for an individual video on a blank line, and the video player will be added automatically.

Or you can click on this little icon in the left margin to display this row of media insert controls:

LinkedIn image and media insert controls

When you publish one of these articles, it is highlighted more prominently in the LinkedIn notifications scheme than if you had published a simple status post. A new article from someone in your network shows up in the same stream of notifications where LinkedIn shows that someone else has interacted with your profile or your content.

A notification of an article posted by someone in my network

Your latest LinkedIn post is also prominently displayed on your profile, along with your other most recent activity on the network.

A recent LinkedIn post and other activity displayed on my profile

 

One thing to be careful about is posting the identical content multiple places. For SEO, it’s better to drive all traffic to a single version of an article at a single web address. However, if you publish something on LinkedIn that you would also like to share on your own blog, you can do so without the SEO penalty if you remember to set the LinkedIn version as the canonical version of the article. See this explanation from the creators of the Yoast SEO plugin for WordPress.

Like all of social media, the LinkedIn publishing capability is something to explored in a spirit of experimentation. Find out what works for you and draws a positive reaction from your network.

Fixing WordPress Paragraph Spacing (When the Visual Editor Misleads You)

A client of mine who blogs with WordPress ran into an issue with new posts appearing on her site without the proper spacing between paragraphs. I’m sharing the solution in case others might find it useful.

Everything looked fine in the WordPress visual editor, but these posts looked wrong on the public website. I thought at first it was problem with the site theme / CSS, but older posts on the same site were displaying fine. The only way to figure out what was going wrong was to view these posts with the editor toggled from Visual to Text to reveal some code that shouldn’t have been there. Every paragraph was tagged as a div — which in HTML is a block of text with no default styling. Also, an empty div is not displayed at all, so the blank lines she thought she had in her copy disappeared on the public site.

I suspect the divs got in there because this content had been copied and pasted from some other source, probably some other web-based word processor or blog posting system. WordPress usually does a pretty good job of handling content posted in from Microsoft Word, but not necessarily other sources.

Post with excess divs
Post with excess divs

In visual mode, WordPress identified all these divs as paragraphs, by the way, which was not helpful. Once you have paragraphs tagged as divs, WordPress keeps tagging any additional content you insert the same way.

To fix the posts, I had to manually remove the <div> and </div> tags. What a WordPress post normally looks like in the editor is a mix of text and HTML (for links and images) with a blank line between paragraphs. WordPress normally handles these formatting chores automatically — it just doesn’t handle an attack of the divs very well.

Properly formatted post in the WordPress editor.
Properly formatted post in the WordPress editor.

Notifier for Glip, a new WordPress plugin

This plugin will post a notification to a Glip team conversation whenever a new blog article is posted or a comment is posted to a blog that you maintain. The plugin uses the WebHooks interface to connect with Glip, the team collaboration and productivity platform from RingCentral.

The plugin is available through the WordPress.org plugins repository: https://wordpress.org/plugins/notifier-for-glip/

I do editorial consulting for Glip, so this came about because I was trying to cook up an example of using their support for WebHooks (a standard web services API). Commented code included below.

Target audience for this plugin:

  • Anyone who runs a WordPress website with multiple contributors and people posting comments.
  • Web developers and designers who need to keep tabs on a client’s blog posts and comment activity.
WordPress new blog post and comment activity, as recorded in the stream of comments in a Glip team conversation.
WordPress new blog post and comment activity, as recorded in the stream of comments in a Glip team conversation.

How It Works

The WordPress admin must set the WebHooks url for Glip on the Settings -> Glip screen.

Here is where you obtain that information from within Glip, after activating the WebHooks integration.

Glip WebHooks integration screen
Glip WebHooks integration screen

The plugin uses WordPress action hooks to detect new posts and comments and relay them to Glip in the JSON format specified above.

Here is the code I used for the initial release:

<?php
/*
Plugin Name: Notifier for Glip
Plugin URI: http://www.carrcommunications.com/notifier-for-glip/
Description: Post a notification to a Glip team conversation whenever a new blog article is posted or a comment is posted to a blog that you maintain. The plugin uses the WebHooks interface to connect with Glip, the team collaboration and productivity platform from RingCentral.
Author: David F. Carr
Version: 0.9
Author URI: http://www.carrcommunications.com
*/

function glip_webhook($webhook_url,$title, $body, $activity,$icon ='' ) {

$json = json_encode(array('icon'=>$icon,'activity'=>$activity,'title'=>$title,'body'=>$body) );

$ch = curl_init($webhook_url);                                                                     
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");                                                                     
curl_setopt($ch, CURLOPT_POSTFIELDS, $json);                                                                  
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);                                                                      
curl_setopt($ch, CURLOPT_HTTPHEADER, array(                                                                          
    'Content-Type: application/json',                                                                                
    'Content-Length: ' . strlen($json))                                                          
);                                                                                  
$r = curl_exec($ch);
return $r;
}

function glip_post_published_notification( $ID, $post, $update ) {
	
	if ($post->post_date != $post->post_modified)
		return; // don't do this for edits to a previously published post
	$webhook_url = get_option('glip_webhook'); 
	if(empty($webhook_url))
		return; // won't work without it
	
    $author = $post->post_author; /* Post author ID. */
    $name = get_the_author_meta( 'display_name', $author );
    $email = get_the_author_meta( 'user_email', $author );
    $title = $post->post_title;
    $permalink = get_permalink( $ID );
    $edit = get_edit_post_link( $ID, '' );
	$start = substr(strip_tags($post->post_content),0,100);
	$title = $update.sprintf('New post to blog: [%s](%s) by %s %s',$title, $permalink,$name,$email);
	$body = sprintf('%s

%s

[Edit](%s)',$_SERVER['SERVER_NAME'],$start,$edit);
	$icon = plugins_url('wordpress-logo-32-blue.png',__FILE__);
	$activity = 'New Blog Post';
	glip_webhook($webhook_url,$title, $body, $activity,$icon);
}
add_action( 'publish_post', 'glip_post_published_notification', 10, 2 );

add_action('wp_insert_comment','glip_comment_inserted',99,2);

function glip_comment_inserted($comment_id, $comment_object) {

	$webhook_url = get_option('glip_webhook'); 
	if(empty($webhook_url))
		return; // won't work without it

	$permalink = get_permalink($comment_object->comment_post_ID);
	$title = sprintf('New comment on blog by %s %s',$comment_object->comment_author, $comment_object->comment_author_email);
	$body = sprintf('%s
	
	%s
	
	Post: [%s](%s)',$_SERVER['SERVER_NAME'],$comment_object->comment_content,get_the_title($comment_object->comment_post_ID),$permalink);
	$icon = plugins_url('wordpress-logo-32-blue.png',__FILE__);
	$activity = 'New Comment on Blog';
	glip_webhook($webhook_url,$title, $body, $activity,$icon);
}

add_action('admin_init', 'glip_options_init' );
add_action('admin_menu', 'glip_options_add_page');

// Init plugin options to white list our options
function glip_options_init(){
	register_setting( 'glip_webhook_options', 'glip_webhook', 'glip_options_validate' );
}

// Add menu page
function glip_options_add_page() {
	add_options_page('Glip', 'Glip', 'manage_options', 'glip_options', 'glip_options_do_page');
}

// Draw the menu page itself
function glip_options_do_page() {
	?>
<div class="wrap">
		<h2>Glip Options</h2>
		<form method="post" action="options.php">
			<?php settings_fields('glip_webhook_options'); ?>
			<?php $webhook = get_option('glip_webhook'); ?>
       WebHooks address:     <input name="glip_webhook" id="glip_webhook"value="<?php echo $webhook?>" />
			<p class="submit">
			<input type="submit" class="button-primary" value="<?php _e('Save Changes') ?>" />
			</p>
		</form>
		<p>You will find this web address in the settings screen for the WebHooks integration, which is included with every Glip account. A menu in the upper right hand corner allows you to change the team conversation updates will be posted to.</p>

<p><img src="<?php echo plugins_url('glip-webhooks.png',__FILE__); ?>" width="580" height="372" alt="Webhooks" /></p>

	</div>
	<?php	
}

// Sanitize and validate input.
function glip_options_validate($input) {

	if (!filter_var($input, FILTER_VALIDATE_URL) === false) {
		return $input;
	} else {
		return '';
	}

}

function glip_admin_notice () {
$w = get_option('glip_webhook');
if(empty ($w) )
	printf('<div class="error">%s <a href="%s">%s</a></div>',__('WebHooks URL must be set for ','glipnotifier'),admin_url('options-general.php?page=glip_options'), __('Glip integration','glipnotifier'));
	
}

add_action('admin_notices', 'glip_admin_notice');
?>

My First Redbooth API Demo

Shortly after joining Redbooth as an employee, I began playing with the collaboration platform’s application user interface. As a writer who also writes code, I was curious to see whether I could come up with something useful.

The Redbooth API makes use of the OAuth2 authentication method, which provides a standard way for a user of one application to give permission for another application to access protected resources. For example, once the test is past, an external application that has received the user’s blessing can retrieve access files stored in a Redbooth workspace or add tasks to a task list. The application retrieves a cryptographic authentication token from Redbooth, which it then uses as proof of its right to access these resources.

One of the first ideas I had was to display some sort of interactive user directory. I can imagine a few different scenarios where it might be handy to store some supplemental information about employee performance or vacation schedules in your own app, using the data structure you define rather than one provided within Redbooth.

This version shown below (which you can access live here) pulls the names and profile photos for all the other users in your organization and allows you to add notes to each of these mini-profiles.

Note that you’re welcome to use this application for real, as long as you’re willing to do so at your own risk; it’s not running on Redbooth’s secure infrastructure, and the data you add goes into a MySQL database on my server. If you have a serious application for something like this, you could use the code shown below as a starting point (if you’re working in PHP) or as inspiration for an app you might implement in the language of your choice, on your own server.

API demo
Once a redbooth user authorizes account access, this app displays a basic user directory with the option of adding notes to each user record.

The preliminary setup starts in initialize.php, which sends a request to redbooth.com to start the OAuth authentication process:

initialize.php


<?php

$apikey = 'KEY_GOES_HERE';
 
// make sure to url encode the redirect URL
$q = urlencode('http://tabmgr.com/rb/auth.php'); //provide the url for auth.php on your server
 
// construct the authorization query with our apikey and the redirect URL
$endpoint = 'https://redbooth.com/oauth2/authorize?client_id=' . $apikey . '&redirect_uri=' . $q . '&response_type=code';
 
//redirect to authorization URL
header("Location: " . $endpoint);

exit;
 
?>

The main workhorse functions are in auth.php. In the previous step, we queried redbooth.com for a code to be used to kick off the authentication process. The code is returned as a GET query parameter appended to the redirect uri specified in initialize.php. The API key and API secret are used to verify that the user has given permission for access to his or her redbooth.com account. The remainder of the code in this script is devoted to sorting the user records by last name, formatting them for display, and setting up a JQuery/AJAX routine to post notes about each user to a local database.

Any existing notes are retrieved as part of a database routine that runs right before we start iterating through the user records retrieved from redbooth.com.

auth.php

<?php
 
session_start();

?>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>User Directory Demo</title>
<style>
textarea {
width: 600px;
border: thin solid blue;
height: 2em;
}
body {
background-color: #CC0000;
}
#content {
width: 700px;
padding-left: 15px;
padding-right: 15px;
margin-left: auto;
margin-right: auto;
background-color: #eee;
}
</style>
</head>

<body>
<div id="content">
<h1>Dave's unofficial API demo</h1>
<p>Once authorized, the app pulls a list of the other users in your organization from Redbooth. You can add a note to any user record by typing it into the blank and pressing enter.</p>
<?php

function rb_authorize () {

$code = $_GET["code"];
 
$apikey = 'KEY_GOES_HERE';
 
$appsecret = 'SECRET_GOES_HERE';

// make sure to url encode the return URL
$q = urlencode('http://tabmgr.com/rb/auth.php');  //ex: http://www.mytest.com/auth.php

// construct the authorization query with our apikey and the returned code to get the access_token
$endpoint = 'https://redbooth.com/oauth2/token?client_id=' . $apikey . '&client_secret=' . $appsecret . '&code=' . $code . '&grant_type=authorization_code&redirect_uri=' . $q;
 
// setup curl to make a call to the endpoint
$session = curl_init($endpoint);

// indicates that we want the response back
curl_setopt($session, CURLOPT_RETURNTRANSFER, true);
// post
curl_setopt($session, CURLOPT_POST, true);
 
// exec curl and get the data back
$data = curl_exec($session);

// remember to close the curl session once we are finished retrieveing the data
curl_close($session);
 
// decode the json data to make it easier to parse the php
$auth_result = json_decode($data);
 
// check for empty data
if ($auth_result === NULL) die('Error parsing json for auth result');

// Get the access_token
$_SESSION["access_token"] = $access_token = $auth_result->access_token;
return $access_token;
}

function get_me($access_token) {
// construct the query with the access_token to get the user's account details
$endpoint = 'https://redbooth.com/api/3/me?access_token=' . $access_token;

// setup curl to make a call to the endpoint
$session = curl_init($endpoint);
 
// indicates that we want the response back
curl_setopt($session, CURLOPT_RETURNTRANSFER, true);
 
// exec curl and get the data back
$data = curl_exec($session);
 
// remember to close the curl session once we are finished retrieveing the data
curl_close($session);
 
// decode the json data to make it easier to parse the php
$me = json_decode($data);
return $me;
}

if($_SESSION["access_token"] && !$_GET["new"])
	$access_token = $_SESSION["access_token"]; // todo - check expiration
else
{
// Get the returned code
$access_token = rb_authorize ();
}

$me = get_me($access_token);

// check for empty data
if ($me === NULL)
{
// try again to get current user data
$access_token = rb_authorize ();
$me = get_me($access_token);
}

if ($me === NULL) die('Error parsing json for /me');

// construct the query with the access_token to get the user's account details
$endpoint = 'https://redbooth.com/api/3/users?access_token=' . $access_token;
 
// setup curl to make a call to the endpoint
$session = curl_init($endpoint);
 
// indicates that we want the response back
curl_setopt($session, CURLOPT_RETURNTRANSFER, true);
 
// exec curl and get the data back
$data = curl_exec($session);

// remember to close the curl session once we are finished retrieveing the data
curl_close($session);

// decode the json data to make it easier to parse the php
$users = json_decode($data);
 
// check for empty data
if ($users === NULL) die('Error parsing json for users listing: '.$data);

$count = sizeof($users);

echo "<p><em>Fetched $count users</em></p>";

// get any status notes about the users from the database and put them in an array
// keyed to the redbooth username
include "db.php";
$sql = "SELECT * FROM status ORDER BY timestamp DESC";
foreach ($conn->query($sql) as $row) {
	$stat[$row['username']] .= '<p>'.$row['text'] . "<br /><em>posted by @" . $row["by"] .' at ' . date('F j, Y',strtotime($row["timestamp"]))."</em><p>\n";
}

foreach($users as $user)
	{
	$uindex = strtolower(preg_replace('/[^A-Za-z]/','',$user->last_name.$user->first_name.$user->email));
$user_directory[$uindex] = sprintf("
<div style=\"float: right; clear:both\"><img src=\"%s\"></div>
<p>
<strong>First Name</strong>: %s<br />
<strong>Last Name</strong>: %s<br />
<strong>Email</strong>: %s
</p>
<p>
<textarea class=\"status\" id=\"get_%s\" subject_username=\"%s\" me_username=\"%s\"></textarea>
</p>
<div id=\"postresult_%s\" ></div>
%s
",$user->avatar_url, $user->first_name, $user->last_name, $user->email, $user->username, $user->username, $me->username, $user->username, $stat[$user->username]);
	}

ksort($user_directory);
foreach ($user_directory as $profile) 
	echo $profile;
?>
</div>
<script src="https://code.jquery.com/jquery-1.10.2.js"></script>
<script>
$( ".status" )
  .keypress(function() {
     if(event.keyCode == 13)
       {
	PostRbStatus(this.getAttribute("subject_username"), this.getAttribute("me_username"), this.value)
       }
  })  

function PostRbStatus(subject_username, me_username, message) {
	
$.post( "/rb/poststatus.php", {subject_username: subject_username, me_username: me_username, message: message})
	.done(function( data ) {
  $( "#postresult_" +  subject_username).html( data )
	})
	.error(function( ) {
		alert("error");

});

	$('#get_' + subject_username).blur();
	$('#get_' + subject_username).value('');
	
}

$( "textarea" )
  .on( "mouseenter", function() {
    $( this ).css({
      "border": "medium solid #CC0000",
	  "height": "5em"
    });
  })
  .on( "mouseleave", function() {
    var styles = {
      "border": "thin solid blue",
	  "height": "2em"
    };
    $( this ).css( styles );
});
</script>
</body>
</html>

The db.php file is a standard database initialization using the PDO class.

<?php
$servername = "localhost";
$username = "USERNAME_GOES_HERE";
$password = "PASSWORD_GOES_HERE";

try {
    $conn = new PDO("mysql:host=$servername;dbname=tabmgr_rb", $username, $password);
    // set the PDO error mode to exception
    $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    //echo "Connected successfully"; 
    }
catch(PDOException $e)
    {
    echo "Database connection failed: " . $e->getMessage();
    }
?>

The database access a simple table with this structure.


CREATE TABLE IF NOT EXISTS `status` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) NOT NULL,
  `by` varchar(255) NOT NULL,
  `text` text NOT NULL,
  `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

Here is the server-side script that processes the data submitted for notes on each user. The output is captured by the client-side JavaScript and placed in a div above the previous entries in the database.

<?php
include "db.php";
if($_POST["subject_username"])
	{
		$username = $_POST["subject_username"];
		$by = $_POST["me_username"];
		$message = $_POST["message"];
		$sql = sprintf("INSERT INTO status (`username`, `by`, `text`) VALUES('%s', '%s', '%s') ", $username, $by, $message);
		$count = $conn->exec($sql);
		if($count)
			printf('<p>%s <br /><em>added by %s %s</em></p>',$message,$by,date('F j, Y'));
		else
			echo "<p>Error adding note</p>";
	}
?>

Leica Cameras, Leitz and Zeiss Lenses, etc.

I’m currently seeking offers or advice on how to value several vintage cameras, mostly Leicas, as well as lenses and accessories. This collection also includes movie and portrait cameras. Most are from the 1950s and 1960s and some may go back to the 1940s. Full set includes an assortment of lens filters and adapters not shown here. These were handed down from my father and grandfather.

My grandfather was a veteran of World War I, so it’s possible some of these such as the Verascope stereo camera are older than I realize.

If interested, contact david@carrcommunications.com.

Lenses

5cm 1:2 Serial #1587970
5cm 1:2 Serial #1587970

leitz5cm-1to2-1318695

5cm 1:2 Serial #1318695
(purchased 1956)

3.5cm 135 Serial#1290163
3.5cm 135 Serial#1290163 (from 1955)
LEITZ ELMARIT 90mm f/2.8 from 1962 Serial #1760690
LEITZ ELMARIT 90mm f/2.8 from 1962 Serial #1760690
135mm 1:4 Serial #1825635
Leitz Elmar 135mm 1:4 Serial #1825635

DSC_0134

This may have collectors value. Serial #196867 10.5 cm 1:63

I believe this is an Alpine Elmar from the 1930s

serial # matches models manufactured in 1934, according to this reference http://www.kenrockwell.com/leica/lens-serial-numbers.htm

Leitz 5cm  1:2 Serial #380257 from 1937
Leitz 5cm 1:2 Serial #380257 from 1937

 

Leitz 3.5cm 1:3.5 Serial #1519731
Leitz 3.5cm 1:3.5 Serial #1519731 (wide angle lens, mfg 1957, purchased 1961)

 

Leitz 50mm? 1:4.5 Focotar (don't see serial #)
Leitz 50mm? 1:4.5 Focotar (don’t see serial #)
DSC_0153
M3 #1
DSC_0152
M3 #1 – based on serial #, this was manufactured in 1959

M3 dates based on this list https://www.cameraquest.com/mtype.htm

DSC_0150
M3 #2 with case
DSC_0149
M3 #2 – based on serial #, this is from 1955
DSC_0166
M3 #3
DSC_0165
M3 #3 – based on serial #, from a batch manufactured 10/11/61
DSC_0164
Leica IIf, paperwork says 1955

Leica IIf according to this serial # lookup https://www.cameraquest.com/ltmnum.htm

Verascope movie camera
Verascope stereo camera

See http://camera-wiki.org/wiki/Richard_%28Jules%29 or
http://licm.org.uk/livingImage/Verascope.html

Bolex 8mm movie camera
Bolex 8mm movie camera – model B8, 1953

Reference http://www.ebay.com/gds/Top-7-Bolex-8mm-Vintage-Movie-Cameras-/10000000178130396/g.html

DSC_0158

DSC_0156

DSC_0154

DSC_0151

DSC_0146

DSC_0145

DSC_0142

DSC_0139

DSC_0138

DSC_0135

 

DSC_0131

DSC_0130

DSC_0129

DSC_0127

DSC_0167

DSC_0162

DSC_0160

DSC_0159

WordPress for Toastmasters Project

One of my recent web development projects is a customization of WordPress for use by Toastmasters clubs, available as software as a service at wp4toastmasters.com.

This is an extension of RSVPMaker, my most successful and widely used WordPress plugin. I added Toastmasters-specific features while serving as Vice President of Education (and later President) at Club Awesome Toastmasters. Now I’m taking what I developed for my own selfish purposes and making it more widely available.

The Toastmasters extensions may or may not see the light of day as a published plugin, but I’m offering a free version of the hosted software to those who want a subdomain such as myclub.wp4toastmasters.com – with the possibility of upgrades for those who want to host at their own domain.

You can see a bit of how it works in the video below.