Blog

Confluence Panel Macro

A macro for Atlassian Confluence that mimics a Bootstrap 3 Panel.


Overview

This is a simple macro I developed that mimics the Bootstrap 3 Panel. The macro takes an optional Title, an optional Style, and a rich text macro body. It renders results as shown in the examples section below.

I did this as a learning exercise in preparation to do a more sophisticated plugin for Confluence. You might find the code useful if your doing something similar. You can find the source code in the following public repository on GitHub:

https://github.com/codyburleson/confluence-panel-macro


Tested on:

  • Atlassian Confluence 6.0.5
  • Google Chrome Version 57.0.2987.98 (64-bit) on Mac OS

If you find any issues, please open an issue on GitHub.

Examples


This panel has no title filled out and no style selected. It shows an example of using another macro within the body; the STATUS MACRO .

No Style Selected

 This one has a title, but no style selected. It defaults to the 'default' style.

Default

This panel is configured with the style 'default'.

Primary

 This panel is configured with the style 'primary'.

Success

 This panel is configured with the style 'success'.

Info

 This panel is configured with the style 'info'.

Warning

 This panel is configured with the style 'warning'.

Danger

 This panel is configured with the style 'danger'.

Attachments

The current version of the macro that I've got deployed here in this wiki is attached. If you want to try it out, you can upload the jar through Manage add-ons in the Confluence admin area.

  File Modified
Java Archive panelMacro-1.0.0-SNAPSHOT.jar yesterday at 4:36 AM by Cody Burleson

License

MIT License

Copyright (c) 2017 Cody Burleson

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


This page is a never-ending work-in-progress where I've decided to capture handy little tips for enhancing workflow in WebStorm. Shoot me an email if you know of any other handy little things that I can add to this list.

Auto-Import (TypeScript)

Put cursor at end of red text object name and click CTRL + SPACE (auto-complete) and select the object from the pop-up context menu.

Comment Code

Select code, then CMD + /

Multiple Cursors

ALT + CLICK to set multiple cursors manually on the page. (on mac that includes the function key)
or...
Select a word and press CTRL + G 


Did you know that Emmet's built-in to WebStorm? Emmet takes the snippets idea to a whole new level: you can type CSS-like expressions that imply the HTML structure you want, press Tab, and then WebStorm spits out the desired HTML.

Try It!

Here's a simple example of a basic expression in the Emmet syntax. Try typing the following in an HTMl page in WebStorm:

ul>li*10

At the end of the expression, just hit the Tab key. WebStorm will use Emmet to parse the text and spit out the intended HTML which, in this case, will be an unordered list with 5 list items (shown below):

<ul>
   <li></li>
   <li></li>
   <li></li>
   <li></li>
   <li></li>
</ul>


For details, you can refer to the Emmet Documentation, but for convenience, I'm including the most common stuff below. There's a lot more to it though, so if you like what you see here, be sure to RTFM.

Emmet Syntax

Nesting

You can use > operator to nest elements inside each other:

div>ul>li
Result
<div>
    <ul>
        <li></li>
    </ul>
</div>

Sibling

Use + operator to place elements near each other, on the same level:

div+p+bq
Result
<div></div>
<p></p>
<blockquote></blockquote>

Multiplication

With * operator you can define how many times element should be outputted:

ul>li*5
Result
<ul>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
</ul>

Grouping

Parenthesises are used by Emmets’ power users for grouping subtrees in complex abbreviations:

div>(header>ul>li*2>a)+footer>p
Result
<div>
    <header>
        <ul>
            <li><a href=""></a></li>
            <li><a href=""></a></li>
        </ul>
    </header>
    <footer>
        <p></p>
    </footer>
</div>

If you’re working with browser’s DOM, you may think of groups as Document Fragments: each group contains abbreviation subtree and all the following elements are inserted at the same level as the first element of group.

You can nest groups inside each other and combine them with multiplication * operator.

ID and CLASS


In CSS, you use elem#id and elem.class notation to reach the elements with specified id or class attributes. In Emmet, you can use the very same syntax to add these attributes to specified element:

div#header+div.page+div#footer.class1.class2.class3
Result
<div id="header"></div>
<div class="page"></div>
<div id="footer" class="class1 class2 class3"></div>

Custom attributes

You can use [attr] notation (as in CSS) to add custom attributes to your element:

td[title="Hello world!" colspan=3]
Result
<td title="Hello world!" colspan="3"></td>

Item numbering

With multiplication * operator you can repeat elements, but with $ you can number them. Place $ operator inside element’s name, attribute’s name or attribute’s value to output current number of repeated element:

ul>li.item$*5
Result
<ul>
    <li class="item1"></li>
    <li class="item2"></li>
    <li class="item3"></li>
    <li class="item4"></li>
    <li class="item5"></li>
</ul>

You can use multiple $ in a row to pad number with zeroes:

ul>li.item$$$*5
Result
<ul>
    <li class="item001"></li>
    <li class="item002"></li>
    <li class="item003"></li>
    <li class="item004"></li>
    <li class="item005"></li>
</ul>

Text

You can use curly braces to add text to element:


a{Click me}
Result
<a href="">Click me</a>

Greek Text

You know - that filler text that designers use when they can't think of real words that are relevant to a design...

lorem5
Lorem ipsum dolor sit amet.


Holy Shit Example

Here's a crazy example to give you an idea of what you can accomplish if you get bad ass with Emmet.

Emmet Abbreviation
nav#menuSystem.navMenu.isOpen>div#hotelLogo>div.navMenuIcon.logoIcon+div#arrowPointer+ul#navMenuMain>li.navMenuItem.navMenuItem$$$*10>div.navMenuIcon{Item $}+a{Item $}
Result
<nav id="menuSystem" class="navMenu isOpen">
   <div id="hotelLogo">
      <div class="navMenuIcon logoIcon"></div>
      <div id="arrowPointer"></div>
      <ul id="navMenuMain">
         <li class="navMenuItem navMenuItem001">
            <div class="navMenuIcon">Item 1</div>
            <a>Item 1</a></li>
         <li class="navMenuItem navMenuItem002">
            <div class="navMenuIcon">Item 2</div>
            <a>Item 2</a></li>
         <li class="navMenuItem navMenuItem003">
            <div class="navMenuIcon">Item 3</div>
            <a>Item 3</a></li>
         <li class="navMenuItem navMenuItem004">
            <div class="navMenuIcon">Item 4</div>
            <a>Item 4</a></li>
         <li class="navMenuItem navMenuItem005">
            <div class="navMenuIcon">Item 5</div>
            <a>Item 5</a></li>
         <li class="navMenuItem navMenuItem006">
            <div class="navMenuIcon">Item 6</div>
            <a>Item 6</a></li>
         <li class="navMenuItem navMenuItem007">
            <div class="navMenuIcon">Item 7</div>
            <a>Item 7</a></li>
         <li class="navMenuItem navMenuItem008">
            <div class="navMenuIcon">Item 8</div>
            <a>Item 8</a></li>
         <li class="navMenuItem navMenuItem009">
            <div class="navMenuIcon">Item 9</div>
            <a>Item 9</a></li>
         <li class="navMenuItem navMenuItem010">
            <div class="navMenuIcon">Item 10</div>
            <a>Item 10</a></li>
      </ul>
   </div>
</nav>




Here's how to avoid tracking page views in Google Analytics when you're logged in as an administrative or other specific user.


This solution presupposes that your Google Analytics tracking snippet is pasted into the field labelled "At end of the HEAD" in the Custom HTML section in the Confluence Administration Area as shown below...

Before modification, your Google Analytics tracking code should look something like this.

<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');

ga('create', 'UA-21819432-1', 'auto');
ga('send', 'pageview');


</script>

Now, to avoid tracking pages for an administrative user (or any particular user or set of users, for that matter), you can wrap the ga() function calls in an IF check. In the following code, we use the Confluence AJS object to determine whether or not the authenticated user is an administrative user. If that's true, we do not reach the ga() functions and thus, the pageview is not tracked. In all other cases, however, the pageview will be tracked.

<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');

if(! AJS.params.isConfluenceAdmin) {
	ga('create', 'UA-21819432-1', 'auto');
  	ga('send', 'pageview');
}
</script>

You could use console.log statements to test this in your local browser.

If you want to avoid logging page views for specific users, you can check for specific user names with AJS.params.remoteUser.



In my digital journal, I've kept a page called "Bucket List", but I've kicked it out in favor of a new title: "Goals".

I've learned that the language you use and the story you tell yourself is fundamentally important to what you actually achieve and do. And maybe it goes even deeper than that. Maybe it's not just the language you use, but the personal meaning that you've ascribed to the concepts in that language. The Merriam-Webster online dictionary defines a bucket list as a list of things that one has not done but wants to do before dying. That's how I think of it too, but there's something about the concept for me that is far more forgiving than a list of goals. I think of the bucket list as a list of frilly things or nice-to-dos - just icing on the cake of life, which you never have to eat. Nobody's going to hold it against you if you don't do the things that are on your bucket list - not even yourself. Death is your deadline, after all.

I don't think that works for me, so instead, I've decided to change the title of my Bucket List to "Goals". The meaning of the two concepts are entirely different to me. Goals are much more real. For me, a goal implies a stronger intent to achieve it. A goal begs an action plan. A goal begs to be SMART:

  • Specific (and Significant)
  • Measurable (and Meaningful)
  • Attainable (and Action-oriented)
  • Realistic, Relevant (and Rewarding)
  • Time-Based (and Time-bound or Trackable)

To me, the things on a bucket list are just ideas. Possibilities. Opportunities. I'd rather think of them as "draft goals". They're worth writing down. But give them any careful thought and they might just as easily fall off the list as get prioritized and planned. So, now I have my list of goals - some prioritized and planned. Some SMART. Some, just draft ideas. But I don't have a bucket list anymore.

There are simply things I've done, things I will do, and things I might do - goals I've achieved, goals I will achieve, and then all the things that might or might not become goals. If they become goals, they'll get an action plan and evaluation against the principles of SMART. If they don't make it to that point, then they probably are just frilly ideas that don't deserve to stay on the list.

 

This week, I've been listening to the audiobook, Benjamin Franklin: An American Life by Walter Iscaacson. My favorite part, so far is where he recounted thirteen virtues that Benjamin Franklin recorded in his autobiography. I think Ben Franklin wrote these in 1726, at the mere age of twenty.

  1. Temperance. Eat not to dullness; drink not to elevation.
  2. Silence. Speak not but what may benefit others or yourself; avoid trifling conversation.
  3. Order. Let all your things have their places; let each part of your business have its time.
  4. Resolution. Resolve to perform what you ought; perform without fail what you resolve.
  5. Frugality. Make no expense but to do good to others or yourself; i.e., waste nothing.
  6. Industry. Lose no time; be always employ'd in something useful; cut off all unnecessary actions.
  7. Sincerity. Use no hurtful deceit; think innocently and justly, and, if you speak, speak accordingly.
  8. Justice. Wrong none by doing injuries, or omitting the benefits that are your duty.
  9. Moderation. Avoid extremes; forbear resenting injuries so much as you think they deserve.
  10. Cleanliness. Tolerate no uncleanliness in body, cloaths, or habitation.
  11. Tranquillity. Be not disturbed at trifles, or at accidents common or unavoidable.
  12. Chastity. Rarely use venery but for health or offspring, never to dullness, weakness, or the injury of your own or another's peace or reputation.
  13. Humility. Imitate Jesus and Socrates.

Angular 2 provides a Title service that you can use to set the title of a page. This is good, of course, for SEO. Here's how to use it.

import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser'; 

@Component({
	selector: 'my-app',  
	viewProviders: [Title],  
	template: `<h1>MyApp</h1>` 
}) 
export class MyAppComponent {
	constructor(title: Title) {
		title.setTitle('My App - Page Title');
	}
}

The service automatically creates the title element in the head if needed and also sets the value.  The service also has a getter method to get the title.

Pink Hearts

My sister tried to kill herself. Or, at least, I think she did.

I was eleven years old. I don't remember much about it; it was a long time ago - kind of a blur at this point. The thing that sticks out is all the little pink hearts - speed pills. She'd swallowed a shit-load, then sprayed them all over the upholstery of the car with her vomit.


Mom rolled down the windows. The stench was bad.

I watched those little pills drip off the top of the car as we raced to the hospital to have her stomach pumped.

She was thirteen years old.

She survived it.

To this day, I don't know why she did that.

I was too young to understand much, but it makes me wonder now.

What could have been so troublesome for her at that age? I don't even recall her having a boyfriend at the time. She never did drugs before then. It's not like we lived on skid row or anything - we were at my sweet grandmother's house for Christ's sake. Where in the hell does a thirteen year old girl find a bunch of speed? 

Maybe they were just caffeine pills. Maybe it was just a ploy for attention or a cry for help. Maybe not.

I'll never know.



In my last two posts about Carbon LDP, I used a very simple Create Blog Postexample to show how to save a document and then how to save a document treeusing the JavaScript SDK. In this post, I build upon those examples by showing how to save a document using established vocabularies.

A Linked Data Platform best practices is to use re-use established linked data vocabularies instead of (re-)inventing duplicates. In my last two posts, we saw that you can create persist any arbitrary JavaScript object to Carbon, such as the one shown below.

content = {
	title: contentTitle,
	body: contentBody,
	publishedDate: pubDate
};

As a matter of convenience, Carbon will automatically generate vocabulary for the properties of such an object. That is to say, it will create a fully qualified URI for the property that is based on the application context you are working in. If the application istest-app/, for example, the following URIs are generated for the properties title, body, and publishedDate found in the JavaScript object shown above.

In most cases, however, you'll want to adhere to the LDP best practice by using terms from pre-existing, well-established vocabularies. The dcterms vocabulary from the Dublin Core®Metadata Initiative, for example. Since the Dublin Core terms are intended for describing media resources, it makes sense to use dcterms:title for the title of blog post content. Also, in order to make my data more flexible with different systems, I'd also like to store the title using the predicate (or property) type, rdfs:label. To do this, only minor modifications to the code we've been using are required.

We can add vocabulary for Carbon to use with the Carbon.extendObjectSchema method. Following is one way that works.

carbon.extendObjectSchema( {
    "dcterms": "http://purl.org/dc/terms/",
    "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
    "xsd": "http://www.w3.org/2001/XMLSchema#",
    "title": {
        "@id": "dcterms:title",
        "@type": "xsd:string"
    },
    "label": {
        "@id": "rdfs:label",
        "@type": "xsd:string"
    }
} );

 

In the example above, we are defining the namespace prefixes and URIs of the vocabularies to use. Then we are telling the system to use specific terms within each vocabulary whenever a JavaScript object's property is found with the specified matching name. For example, anyObject.title would submit dcterms:title to the Carbon server. Likewise,anyObject.label would submit rdfs:label.

However, because we have specified no particular object types, these vocabulary terms will be used globally for any object found with matching properties, which we may not always want. For example, we may decide to use a #title term from one vocabulary for a given object type, and a #title term from some other vocabulary for another. Instead of defining the use of these terms globally, we can alternatively be more specific in the following way. We can define the vocabularies globally, but then specify the use of those vocabularies more specifically to a given object type as shown below.

// Define common prefixes in the general object schema
carbon.extendObjectSchema({
    "dcterms": "http://purl.org/dc/terms/",
    "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
    "xsd": "http://www.w3.org/2001/XMLSchema#"
} );

// Use the specified terms only in the case of 
// BlogPost object type
carbon.extendObjectSchema( "http://example.org/ont/web#BlogPost", {	
    "title": {
        "@id": "dcterms:title",
        "@type": "xsd:string"
    },
    "label": {
        "@id": "rdfs:label",
        "@type": "xsd:string"
    }
} );

 

We also need to modify our JavaScript object to declare it has the type of BlogPost. This is done with the types property as shown below.

content = {
	title: contentTitle,
	label: contentTitle,
	body: contentBody,
	publishedDate: pubDate,
	types: ["http://example.org/ont/web#BlogPost"]
};

Complete Example

Here's all the code from the Create a BlogPost example exercise now.

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
	<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
	<meta name="description" content="A chronological log of thoughts, personal reflections, discoveries, and random musings.">
	<meta name="author" content="Cody Burleson">
	<title>Create Blog Post</title>
	<link rel="icon" href="/images/favicon.ico">
	<link rel="author" href="https://plus.google.com/100082232329079808865"/>
	<link href="/css/bootstrap.min.css" rel="stylesheet"/>
	<link href="/css/styles.css" rel="stylesheet"/>
	<link id="link-rel-common" rel="import" href="/components/cb-common/common.html">
	<script src="/ckeditor/ckeditor.js"></script>
	<script src="/_head-script.js"></script>
	
</head>

<body>

	<article id="content">
		<div id="pageContainer" class="container">

			<div class="row">

				<div class="col-md-12">

					<h1>Create Blog Post</h1>

					<p class="lead">
						Share your thoughts with the world. 
					</p>
					
					<form>
						<div class="row">
							<div class="form-group col-xs-6">
								<label for="userName">Email (Login ID)</label> 
								<input type="text" class="form-control" id="userName" value="cody@base22.com">
							</div>
							<div class="form-group col-xs-6">
								<label for="password">Password</label>
								<!-- Change to type="password" in a real-world scenario... -->
								<input type="text" class="form-control" id="password" value="password">
							</div>
						</div>
						<div class="form-group">
							<label for="contentTitle">Title</label> 
							<input type="text" class="form-control" id="contentTitle" placeholder="Content Title">
						</div>
						<div class="form-group">
						    <label for="editor1">Body</label>
						    <textarea class="form-control" id="editor1" name="editor1" rows="10" cols="80">
						     Write your brilliant words here.
						    </textarea>
						</div>
						<div class="form-group">
							<label for="slug">Slug</label> 
							<input type="text" class="form-control" id="contentSlug" placeholder="content-uri-slug">
						</div>

			            <script>
			                // Replace the <textarea id="editor1"> with a CKEditor
			                // instance, using default configuration.
			                CKEDITOR.replace( 'editor1' );
			            </script>
			            <p></p>
			            <div class="alert alert-success" role="alert" id="alertSuccess" style="display:none">
						  <p><strong>Document saved successfully!</strong><br/></p>
						  <a href="#" class="alert-link" id="alertSuccessDocId"></a>
						</div>
			            
			            <button type="button" class="btn btn-primary" id="submitButton">Submit</button>
			        </form>

				</div>

				<!-- ***** RIGHT PANEL ***** -->

				<div id="right" class="col-md-4">


				</div><!-- /.col-md-4 -->

			</div><!-- /.row -->
		</div><!-- /.container -->
	</article>

	<script src="/javascript/all.js"></script>
	<script src="/_footer-script.js"></script>
	<!-- Source: ./src/main/typescript/create-content.ts -->
	
	
	<script src="/javascript/moment.min.js"></script>
	
	<!-- IE required polyfill -->
	<!-- Copied by gulp copy-resources from node_modules/core-js/client/shim.js -->
	<script src="/javascript/shim.min.js"></script>
	
	<script src="/javascript/Carbon.sfx.js"></script>
	
	<script>
	
	(function() {

		var APP_SLUG = 'test-app/';
		
		var carbon = new Carbon();
		carbon.setSetting( "domain", "localhost:8083" );
		carbon.setSetting( "http.ssl", false );
		
		// Define common prefixes in the general object schema
		carbon.extendObjectSchema({
		    "dcterms": "http://purl.org/dc/terms/",
		    "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
		    "xsd": "http://www.w3.org/2001/XMLSchema#"
		} );
		
		// Use the specified terms only in the case of 
		// BlogPost object type
		carbon.extendObjectSchema( "http://example.org/ont/web#BlogPost", {	
		    "title": {
		        "@id": "dcterms:title",
		        "@type": "xsd:string"
		    },
		    "label": {
		        "@id": "rdfs:label",
		        "@type": "xsd:string"
		    }
		} );
		
		// Returns the slug (document identifying portion) of the given URI.
		function getSlug(uri) {
			// Snip off the trailing slash, if the uri has one...
			if(uri.endsWith('/')) {
				uri = uri.substring(0,uri.length-1);
			}
			// Return all characters after the last remaining slash if one 
			// exists, otherwise return the given string
			if ( uri.indexOf('/') != -1 ) {
				var lastSlashNdx = uri.lastIndexOf('/');
				return uri.substring(lastSlashNdx + 1, uri.length);
			} else {
				return uri;
			}
		}

		function getOrCreateAndGetChildDocument(myAppContext,parentDocId,docId,doc) {
			console.log(">> getOrCreateAndGetDocument()");
			var slug = getSlug(docId);
			return myAppContext.documents.exists( docId ).then(
	    		function( promiseResult ){
					if( promiseResult[0] ) {
						console.log("Document exists; fetching it...");
						return myAppContext.documents.get( docId );
					} else {
						console.log("Document does not exist; creating it...");
						return myAppContext.documents.createChildAndRetrieve( parentDocId, doc, slug );
					}
	    		}
	    	);
		}
		
		
		function submitContent() {
			
			// Get all the values from the UI Form...
			// (No form validation going on here yet)
		    var contentTitle = document.getElementById("contentTitle").value;
		    var contentBody = CKEDITOR.instances.editor1.getData();
		    
		    var pubDate = moment().format();
		    var pubYear = moment().format('YYYY');
		    var pubMonth = moment().format('MM');
		    var pubDay = moment().format('DD');
		    
		    var contentSlug = document.getElementById("contentSlug").value;
		    var userName = document.getElementById("userName").value;
		    var password = document.getElementById("password").value;

		    var myAppContext,
		    	content,
		    	blogContainer,
		    	yearContainer,
		    	monthContainer,
		    	dayContainer;
		    
		    return carbon.auth.authenticate( userName, password ).then(
			    	function( token ) {
			    		//console.log( "Authenticated Agent: %o",carbon.auth.authenticatedAgent ); // Yourself!
			    		return carbon.apps.getContext( APP_SLUG );
			    	}
			    ).then(
			    	function( appContext ) {
			    		myAppContext = appContext; // retrieving your app context
			    		
			    		// First, lets create the blog container
			    		
			    		// ID of the Carbon document you want to retrieve
						//var containerId = "http://localhost:8083/apps/" + APP_SLUG + "blog2/";
			    		var containerId = "blog/";
						
			    		blogContainer = {
			    			title: "Blog"
			    		};
			    		
			    		// Create the Blog container if it doesn't exist...
						return getOrCreateAndGetChildDocument(myAppContext,"/",containerId,blogContainer);
					}
				).then(
					function( result ) {
						blogContainer = result[0];
						console.log("blogContainer: %o",blogContainer);

						yearContainer = {
			    			title: pubYear
				    	};

						return getOrCreateAndGetChildDocument(myAppContext,blogContainer.id,blogContainer.id + pubYear + "/",yearContainer);
					}
				).then(
					function( result ) {
						yearContainer = result[0];
						console.log("yearContainer: %o",yearContainer);

						monthContainer = {
				    			title: pubMonth
					    };
						
						// Create the month container if it doesn't exist
						return getOrCreateAndGetChildDocument(myAppContext,yearContainer.id,yearContainer.id + pubMonth + "/",monthContainer);
					}
				).then(
					function( result ) {
						monthContainer = result[0];
						
						console.log("monthContainer: %o",monthContainer);
						
						dayContainer = {
				    			title: pubDay
					    };

						// Create the day container if it doesn't exist
						return getOrCreateAndGetChildDocument(myAppContext,monthContainer.id,monthContainer.id + pubDay + "/",dayContainer);
					}
				).then(
					function( result ) {													
						dayContainer = result[0];
						
						console.log("dayContainer: %o",dayContainer);

						content = {
			    			title: contentTitle,
			    			label: contentTitle,
			    			body: contentBody,
			    			publishedDate: pubDate,
			    			types: ["http://example.org/ont/web#BlogPost"]
				    	};
												
						return getOrCreateAndGetChildDocument(myAppContext,dayContainer.id,dayContainer.id + contentSlug + "/",content);
					}
				).then(
					function( result ) {												
						content = result[0];
						console.log("Content: %o", content);
						
						document.getElementById(alertSuccess)
						
						var a = document.getElementById("alertSuccess"), aStyle = a.style;
						document.getElementById("alertSuccessDocId").nodeValue = content.id;
						
						var $alertLink = $("#alertSuccessDocId");
						$alertLink.text(content.id);
						$alertLink.attr("href", content.id)
						aStyle.display = "block";
						
					}
				).catch( 
						function( result ) {
							console.log("ERROR: %o", result);
						}
				);

			
		}

		var el = document.getElementById('submitButton');
		el.addEventListener("click", submitContent, false);
		
	})();
	
	</script>

</body>
</html>

 

In my previous post, I used a very simple Create Blog Post example to show how to save a document using the Carbon LDP JavaScript SDK. In this post, I build upon that example by showing how to save a tree of multiple documents instead of just one.

When you create a document with the Carbon JS SDK, it can be the parent of another. So, in LDP terms, you could say that we're creating a hierarchy of LDP containers. Instead of saving blog posts into the application root as shown in the previous post, the goal is to now save posts in dated containers. A structure similar to the following is the goal:

  • Blog
    • 2016
      • 09
        • 05
          • blog-post-document/
        • 04
        • 03

When we create a new blog post, we want to create the container hierarchy for blog, year, month, and day. The blog post content will become a child of the day container. Of course, any of these container documents might already exist, so we'll use thedocuments.exists() function to test for them before creating them. For this, I created a general function to reuse called getOrCreateAndGetChildDocument(). The function gets a document if it exists or creates and gets the document if it does not exist. There is also a helper function called getSlug(), which gets the last part (the most identifying part) of a given URI.

Here's the new script for the Create Blog Post page. Instead of creating a new document in the application root, it now creates it within a dated folder hierarchy. The HTML for the input form hasn't changed since the previos post, so I've left it out for the sake of brevity.

(function() {

	var APP_SLUG = 'test-app/';
	
	var carbon = new Carbon();
	carbon.setSetting( "domain", "localhost:8083" );
	carbon.setSetting( "http.ssl", false );


	// Returns the slug (document identifying portion) of the given URI.
	function getSlug(uri) {
		// Snip off the trailing slash, if the uri has one...
		if(uri.endsWith('/')) {
			uri = uri.substring(0,uri.length-1);
		}
		// Return all characters after the last remaining slash if one 
		// exists, otherwise return the given string
		if ( uri.indexOf('/') != -1 ) {
			var lastSlashNdx = uri.lastIndexOf('/');
			return uri.substring(lastSlashNdx + 1, uri.length);
		} else {
			return uri;
		}
	}
	
	
	function getOrCreateAndGetChildDocument(myAppContext,parentDocId,docId,doc) {
		console.log(">> getOrCreateAndGetDocument()");
		var slug = getSlug(docId);
		return myAppContext.documents.exists( docId ).then(
    		function( promiseResult ){
				if( promiseResult[0] ) {
					console.log("Document exists; fetching it...");
					return myAppContext.documents.get( docId );
				} else {
					console.log("Document does not exist; creating it...");
					return myAppContext.documents.createChildAndRetrieve( parentDocId, doc, slug );
				}
    		}
    	);
	}
	
	
	function submitContent() {
		
		// Get all the values from the UI Form...
		// (No form validation going on here yet)
	    var contentTitle = document.getElementById("contentTitle").value;
	    var contentBody = CKEDITOR.instances.editor1.getData();
	    
	    var pubDate = moment().format();
	    var pubYear = moment().format('YYYY');
	    var pubMonth = moment().format('MM');
	    var pubDay = moment().format('DD');
	    
	    var contentSlug = document.getElementById("contentSlug").value;
	    var userName = document.getElementById("userName").value;
	    var password = document.getElementById("password").value;

	    var myAppContext,
	    	content,
	    	blogContainer,
	    	yearContainer,
	    	monthContainer,
	    	dayContainer;
	    
	    return carbon.auth.authenticate( userName, password ).then(
		    	function( token ) {
		    		//console.log( "Authenticated Agent: %o",carbon.auth.authenticatedAgent ); // Yourself!
		    		return carbon.apps.getContext( APP_SLUG );
		    	}
		    ).then(
		    	function( appContext ) {
		    		myAppContext = appContext; // retrieving your app context
		    		
		    		// First, lets create the blog container
		    		
		    		// ID (URI) of the Carbon document you want to retrieve
		    		// Could be fully qualified...
					// var containerId = "http://localhost:8083/apps/" + APP_SLUG + "blog/";
		    		// Or just relative...
		    		var containerId = "blog/";
					
		    		blogContainer = {
		    			title: "Blog"
		    		};
		    		
		    		// Create the Blog container if it doesn't exist...
					return getOrCreateAndGetChildDocument(myAppContext,"/",containerId,blogContainer);
				}
			).then(
				function( result ) {
					blogContainer = result[0];
					console.log("blogContainer: %o",blogContainer);

					yearContainer = {
		    			title: pubYear
			    	};

					return getOrCreateAndGetChildDocument(myAppContext,blogContainer.id,blogContainer.id + pubYear + "/",yearContainer);
				}
			).then(
				function( result ) {
					yearContainer = result[0];
					console.log("yearContainer: %o",yearContainer);

					monthContainer = {
			    			title: pubMonth
				    };
					
					// Create the month container if it doesn't exist
					return getOrCreateAndGetChildDocument(myAppContext,yearContainer.id,yearContainer.id + pubMonth + "/",monthContainer);
				}
			).then(
				function( result ) {
					monthContainer = result[0];
					
					console.log("monthContainer: %o",monthContainer);
					
					dayContainer = {
			    			title: pubDay
				    };

					// Create the day container if it doesn't exist
					return getOrCreateAndGetChildDocument(myAppContext,monthContainer.id,monthContainer.id + pubDay + "/",dayContainer);
				}
			).then(
				function( result ) {													
					dayContainer = result[0];
					
					console.log("dayContainer: %o",dayContainer);

					content = {
		    			title: contentTitle,
		    			body: contentBody,
		    			publishedDate: pubDate
			    	};
											
					return getOrCreateAndGetChildDocument(myAppContext,dayContainer.id,dayContainer.id + contentSlug + "/",content);
				}
			).then(
				function( result ) {												
					content = result[0];
					console.log("Content: %o", content);
				}
			).catch( 
					function( result ) {
						console.log("ERROR: %o", result);
					}
			);

		
	}


	var el = document.getElementById('submitButton');
	el.addEventListener("click", submitContent, false);

	
})();

 

Here we can begin to see the usefulness of JavaScript Promises. Since we cannot create a child of a document or container unless we're sure it already exists, then we can chain.then() functions to be sure the previously called asynchronous operation always finishes before the next.

So, reading down through the code, we're doing this:

  • Authenticate with Carbon, then...
  • Get the application (application context), then...
  • Create the blog/ document (if it does not already exist), then...
  • Create the YYYY/ document (if it does nor already exist), then...
  • Create the MM/ document (if it does nor already exist), then...
  • Create the DD/ document (if it does nor already exist), then...
  • Create the slug/ blog post content (if it does nor already exist).

After submitting the form to create a new blog post, I can then navigate to the Carbon Workbench and find the following in the app...

 

If you're new to programming with Carbon LDP, you will likely want to learn the bare essentials first - like CRUD operations (Create, Read, Update, Delete). In this post, I show how to use the Carbon LDP JavaScript SDK to create a document based on inputs from a simple UI form. Until we get a chance to improve the getting started guide, I hope this provides some additional context.

Input Form Example

First, take a look at the UI form I created for this exercise. The goal is to add click handler to the submit button, and the save a new document to Carbon based on the form's values.

Input Form HTML

Nothing too fancy - just a simple form using Twitter Bootstrap.

<form>
	<div class="row">
		<div class="form-group col-xs-6">
			<label for="userName">Email (Login ID)</label> 
			<input type="text" class="form-control" id="userName" value="cody@base22.com">
		</div>
		<div class="form-group col-xs-6">
			<label for="password">Password</label>
			<!-- Change to type="password" in a real-world scenario... -->
			<input type="text" class="form-control" id="password" value="password">
		</div>
	</div>
	<div class="form-group">
		<label for="contentTitle">Title</label> 
		<input type="text" class="form-control" id="contentTitle" placeholder="Content Title">
	</div>
	<div class="form-group">
	    <label for="editor1">Body</label>
	    <textarea class="form-control" id="editor1" name="editor1" rows="10" cols="80">
	     Write your brilliant words here.
	    </textarea>
	</div>
	<div class="form-group">
		<label for="slug">Slug</label> 
		<input type="text" class="form-control" id="contentSlug" placeholder="content-uri-slug">
	</div>

    <script>
        // Replace the <textarea id="editor1"> with a CKEditor
        // instance, using default configuration.
        CKEDITOR.replace( 'editor1' );
    </script>
    <p></p>
    <button type="button" class="btn btn-primary" id="submitButton">Submit</button>
</form>

 

JavaScript

I've included the following scripts on the page...

<script src="/javascript/moment.min.js"></script>
<!-- IE required polyfill -->
<!-- Copied by gulp copy-resources from node_modules/core-js/client/shim.js -->
<script src="/javascript/shim.min.js"></script>
<script src="/javascript/Carbon.sfx.js"></script>
  • moment.js - for generating a publish date in the ISO format I want.
  • shim.min.js - from core-js, which is a required polyfill for IE.
  • Carbon.sfx.js - Carbon supports ES6 and TypeScript, but this is the lib you need for plain-old JavaScript.

And here's the entire working script I've placed below the form on the page to save the input to Carbon.

(function() {

	var APP_SLUG = 'test-app/';
	
	var carbon = new Carbon();
	carbon.setSetting( "domain", "localhost:8083" );
	carbon.setSetting( "http.ssl", false );
	
	function submitContent() {
		
		// Get all the values from the UI Form...
		// (No form validation going on here yet)
	    var contentTitle = document.getElementById("contentTitle").value;
	    var contentBody = CKEDITOR.instances.editor1.getData();
	    var pubDate = moment().format();
	    var contentSlug = document.getElementById("contentSlug").value;
	    var userName = document.getElementById("userName").value;
	    var password = document.getElementById("password").value;

	    var myAppContext;
	    var content;

	    carbon.auth.authenticate( userName, password ).then(
	    	function( token ) {
	    		//console.log( "Authenticated Agent: %o",carbon.auth.authenticatedAgent ); // Yourself!
	    		return carbon.apps.getContext( APP_SLUG );
	    	}
	    ).then(
	    	function( appContext ) {
	    		myAppContext = appContext; // retrieving your app context

	    		content = {
	    			title: contentTitle,
	    			body: contentBody,
	    			publishedDate: pubDate
	    		};
	    		
	    		return myAppContext.documents.createChild( "/", content, contentSlug );
	    	}
	    ).then(
	    	function( result ) {
	    		var persistedContent = result[ 0 ];
	    		var response = result[ 1 ];
	    		
	    		console.log( content === persistedContent ); // true
	    		console.log( "Created document: " + content.id ); // document's URI
	    	}
	    ).catch( console.error );
	    
	}

	var el = document.getElementById('submitButton');
	el.addEventListener("click", submitContent, false);
	
})();

 

This script does the following, in general:

  • Creates a new platform context (referencing my localhost dev instance of Carbon).
  • Defines a function to execute and attaches it to the submit button.
  • Within the function, we login to Carbon,
  • We get a handle on the particular Carbon application to save data to (test-app/),
  • We populate a simple JSON object with the values given by the form,
  • And then finally, we persist the JSON document in the root of the application usingreturn myAppContext.documents.createChild( "/", content, contentSlug );

Viewing Documents in the Workbench

After submitting the form several times to create test documents, I am then able to check for the documents using the Carbon Workbench as shown below...

GET a Document by URI

I can also use my web browser or a REST client to get at any of the individual documents by URI. Following is an example of one of the documents, which I requested to be returned in JSON-LD.

[ {
  "@graph" : [ {
    "@id" : "http://localhost:8083/apps/test-app/test-26/",
    "@type" : [ "http://www.w3.org/ns/ldp#BasicContainer", "http://www.w3.org/ns/ldp#Container", "http://www.w3.org/ns/ldp#RDFSource", "https://carbonldp.com/ns/v1/security#ProtectedDocument" ],
    "http://localhost:8083/apps/test-app/vocabulary/#body" : [ {
      "@value" : "<p>This is my textarea to be replaced with CKEditor.</p>\n"
    } ],
    "http://localhost:8083/apps/test-app/vocabulary/#publishedDate" : [ {
      "@value" : "2016-09-02T16:37:03-05:00"
    } ],
    "http://localhost:8083/apps/test-app/vocabulary/#title" : [ {
      "@value" : "Test 26"
    } ],
    "http://www.w3.org/ns/ldp#hasMemberRelation" : [ {
      "@id" : "http://www.w3.org/ns/ldp#member"
    } ],
    "https://carbonldp.com/ns/v1/platform#created" : [ {
      "@type" : "http://www.w3.org/2001/XMLSchema#dateTime",
      "@value" : "2016-09-02T21:37:03.925Z"
    } ],
    "https://carbonldp.com/ns/v1/platform#defaultInteractionModel" : [ {
      "@id" : "http://www.w3.org/ns/ldp#RDFSource"
    } ],
    "https://carbonldp.com/ns/v1/platform#modified" : [ {
      "@type" : "http://www.w3.org/2001/XMLSchema#dateTime",
      "@value" : "2016-09-02T21:37:03.925Z"
    } ],
    "https://carbonldp.com/ns/v1/security#accessControlList" : [ {
      "@id" : "http://localhost:8083/apps/test-app/test-26/~acl/"
    } ]
  } ],
  "@id" : "http://localhost:8083/apps/test-app/test-26/"
} ]

 

Notice that Carbon automatically generated a vocabulary for the attributes of the JSON object I gave it. For example, for the title, Carbon created the property URI, http://localhost:8083/apps/test-app/vocabulary/#title. Carbon provides a means to use existing vocabularies and to create specific vocabulary URIs of own, but as you can see, you don't have to.

 

RDF Syntax Examples

RDF can be expressed in a variety of different serialization formats. It can also be used inline with HTML. Following is an example of the most widely used of these formats so that you can compare them at a glance.


RDF/XML

An XML-based syntax for RDF graphs that was the first standard format for serializing RDF.

<?xml version="1.0" encoding="utf-8"?>
<rdf:RDF
  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
  xmlns:gr="http://purl.org/goodrelations/v1#"
>
  <gr:Location rdf:about="http://www.acme.com/#store">
    <gr:name>Hepp's Happy Burger Restaurant</gr:name>
    <gr:hasOpeningHoursSpecification>
      <gr:OpeningHoursSpecification>
        <gr:opens>08:00:00</gr:opens>
        <gr:closes>20:00:00</gr:closes>
        <gr:hasOpeningHoursDayOfWeek rdf:resource="http://purl.org/goodrelations/v1#Wednesday"/>
        <gr:hasOpeningHoursDayOfWeek rdf:resource="http://purl.org/goodrelations/v1#Monday"/>
        <gr:hasOpeningHoursDayOfWeek rdf:resource="http://purl.org/goodrelations/v1#Tuesday"/>
        <gr:hasOpeningHoursDayOfWeek rdf:resource="http://purl.org/goodrelations/v1#Thursday"/>
        <gr:hasOpeningHoursDayOfWeek rdf:resource="http://purl.org/goodrelations/v1#Friday"/>
      </gr:OpeningHoursSpecification>
    </gr:hasOpeningHoursSpecification>
  </gr:Location>
</rdf:RDF>

 

See also: RDF 1.1 XML Syntax

Turtle

A compact, human-friendly format.

@prefix gr: <http://purl.org/goodrelations/v1#> .

<http://www.acme.com/#store>
  a gr:Location ;
  gr:name "Hepp's Happy Burger Restaurant" ;
  gr:hasOpeningHoursSpecification [
    a gr:OpeningHoursSpecification ;
    gr:opens "08:00:00" ;
    gr:closes "20:00:00" ;
    gr:hasOpeningHoursDayOfWeek gr:Wednesday, gr:Monday, gr:Tuesday, gr:Thursday, gr:Friday
  ] .

 

See also: Turtle - Terse RDF Triple Language

N-Triples

A very simple, easy-to-parse, line-based format that is not as compact as Turtle.

<http://www.acme.com/#store> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://purl.org/goodrelations/v1#Location> .
<http://www.acme.com/#store> <http://purl.org/goodrelations/v1#hasOpeningHoursSpecification> _:b0 .
<http://www.acme.com/#store> <http://purl.org/goodrelations/v1#name> "Hepp's Happy Burger Restaurant" .
_:b0 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://purl.org/goodrelations/v1#OpeningHoursSpecification> .
_:b0 <http://purl.org/goodrelations/v1#opens> "08:00:00" .
_:b0 <http://purl.org/goodrelations/v1#closes> "20:00:00" .
_:b0 <http://purl.org/goodrelations/v1#hasOpeningHoursDayOfWeek> <http://purl.org/goodrelations/v1#Wednesday> .
_:b0 <http://purl.org/goodrelations/v1#hasOpeningHoursDayOfWeek> <http://purl.org/goodrelations/v1#Thursday> .
_:b0 <http://purl.org/goodrelations/v1#hasOpeningHoursDayOfWeek> <http://purl.org/goodrelations/v1#Friday> .
_:b0 <http://purl.org/goodrelations/v1#hasOpeningHoursDayOfWeek> <http://purl.org/goodrelations/v1#Tuesday> .
_:b0 <http://purl.org/goodrelations/v1#hasOpeningHoursDayOfWeek> <http://purl.org/goodrelations/v1#Monday> .

 

See also: RDF 1.1 N-Triples

N3 (or Notation3)

A non-standard serialization that is very similar to Turtle, but has some additional features, such as the ability to define inference rules.

@prefix gr: <http://purl.org/goodrelations/v1#> .

<http://www.acme.com/#store> a gr:Location;
    gr:hasOpeningHoursSpecification [ a gr:OpeningHoursSpecification;
            gr:opens "08:00:00";
            gr:closes "20:00:00";
            gr:hasOpeningHoursDayOfWeek gr:Friday,
                gr:Monday,
                gr:Thursday,
                gr:Tuesday,
                gr:Wednesday ];
    gr:name "Hepp's Happy Burger Restaurant" .

 

See also: Notation3 (N3): A readable RDF syntax

JSON-LD

a JSON-based serialization (for Linked Data).

{
  "@context": {
	"gr": "http://purl.org/goodrelations/v1#"
  },
  "@id": "http://www.acme.com/#store",
  "@type": "gr:Location",
  "gr:hasOpeningHoursSpecification": {
    "@type": "gr:OpeningHoursSpecification",
    "gr:closes": "20:00:00",
    "gr:hasOpeningHoursDayOfWeek": [
      {
        "@id": "gr:Thursday"
      },
      {
        "@id": "gr:Wednesday"
      },
      {
        "@id": "gr:Friday"
      },
      {
        "@id": "gr:Monday"
      },
      {
        "@id": "gr:Tuesday"
      }
    ],
    "gr:opens": "08:00:00"
  },
  "gr:name": "Hepp's Happy Burger Restaurant"
}

 

See also: JSON-LD 1.0

RDFa

Not really an RDF syntax, but rather - a compatible format. "RDFa is an extension to HTML5 that helps you markup things like People, Places, Events, Recipes and Reviews. Search Engines and Web Services use this markup to generate better search listings and give you better visibility on the Web, so that people can find your website more easily." - rdfa.info

<div xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
     xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
     xmlns="http://www.w3.org/1999/xhtml"
     xmlns:gr="http://purl.org/goodrelations/v1#">
   <div about="http://www.acme.com/#store" typeof="gr:Location">
      <div rel="gr:hasOpeningHoursSpecification">
         <div typeof="gr:OpeningHoursSpecification">
            <div property="gr:closes" content="20:00:00"/>
            <div rel="gr:hasOpeningHoursDayOfWeek"
                 resource="http://purl.org/goodrelations/v1#Thursday"/>
            <div rel="gr:hasOpeningHoursDayOfWeek"
                 resource="http://purl.org/goodrelations/v1#Tuesday"/>
            <div rel="gr:hasOpeningHoursDayOfWeek"
                 resource="http://purl.org/goodrelations/v1#Monday"/>
            <div rel="gr:hasOpeningHoursDayOfWeek"
                 resource="http://purl.org/goodrelations/v1#Friday"/>
            <div rel="gr:hasOpeningHoursDayOfWeek"
                 resource="http://purl.org/goodrelations/v1#Wednesday"/>
            <div property="gr:opens" content="08:00:00"/>
         </div>
      </div>
      <div property="gr:name" content="Hepp's Happy Burger Restaurant"/>
   </div>
</div>

 

See also: RDFa

Microdata

Also not really an RDF syntax, but a compatible format. This mechanism allows machine-readable data to be embedded in HTML documents in an easy-to-write manner, with an unambiguous parsing model.

<div itemscope itemtype="http://purl.org/goodrelations/v1#Location" itemid="http://www.acme.com/#store">
  <span itemprop="name">Hepp's Happy Burger Restaurant</span>
  <div itemprop="hasOpeningHoursSpecification" itemscope
       itemtype="http://purl.org/goodrelations/v1#OpeningHoursSpecification">
Opening hours: Mo-Fri,
     <link itemprop="hasOpeningHoursDayOfWeek"
           href="http://purl.org/goodrelations/v1#Monday" />
     <link itemprop="hasOpeningHoursDayOfWeek"
           href="http://purl.org/goodrelations/v1#Tuesday" />
     <link itemprop="hasOpeningHoursDayOfWeek"
           href="http://purl.org/goodrelations/v1#Wednesday" />
     <link itemprop="hasOpeningHoursDayOfWeek"
           href="http://purl.org/goodrelations/v1#Thursday" />
     <link itemprop="hasOpeningHoursDayOfWeek"
           href="http://purl.org/goodrelations/v1#Friday" />
     <meta itemprop="opens" content="08:00:00">8:00 a.m. -
     <meta itemprop="closes" content="20:00:00">8:00 p.m.
  </div>
</div>

 

See also: HTML Microdata

RDF Converter Services Online

There's a few decent RDF converter services that you can use online:

 

Your inbox says a lot about you. What does it say? That you waste precious time? That you lose sight of important messages? That you're unresponsive? Unfocused? Unprofessional? If you don't have a simple and consistent system for dealing with e-mail, you should be ashamed of yourself. Stop the madness, get your ass in gear and get a handle on it, man. Here's a simple system that (usually) works for me.


Inbox Zero

Aim to keep your inbox empty, or almost empty, at all times. This doesn't mean that you have to respond to every message, it just means that every message gets processed. The workflow for processing each message is simple. Practice it enough and it becomes second-nature, like muscle-memory.

  • DO - If the message is actionable and you can complete the action within two minutes or less, then do it immediately.
  • DELEGATE - Is someone else better equipped to handle it? If so, put it where it belongs - in their hands. Don't "pass the buck", but don't waste time with things where you can't add value.
  • DEFER - transfer the action to your TO DO list and then archive the message. This only works if you're managing a TO DO list effectively also. You are, aren't you?
  • DELETE - If you can't take action and there's nothing in the e-mail you might refer to later, delete it.
  • ARCHIVE - Anything you might refer to later and everything you've already processed. And don't create a complex folder system for organizing your archived mail. This wastes more time over the long run than what using your e-mail client's search tools will cost you. Categories can better than folders, they're more flexible because you can apply multiple categories to a single message. A limited set of folder or categories can help for a few things (e.g. personal receipts / professional receipts).

Three Times a Day - Max

Do you live in your inbox? Is that what you do for a living? Set three times per day, at most, to manage and respond to e-mail. Three times per day that you'll get back to "inbox zero". What helps me is to actually include a line in my signature that says "I check mail at 9:00 AM, 1:00 PM, and 5:00 PM CST". More than anything else, it's a reminder to me. When I see it, and the time isn't right, it reminds me to get the hell out of my inbox and get real work done.

Use E-mail Filtering Rules

Write e-mail filtering rules to pre-process messages. For example, messages for which you're only on CC might get a lesser priority that those for which you're in the TO line. Newsletters and unsolicited mail can go out of sight and out of mind until you have ample free time for processing them.

Unsubscribe, Unsubscribe, Unsubscribe

If you're not getting value from regular subscriptions, take the time to find the unsubscribe link and get off the list.

Teach People How to Treat You

Don't be a dick, but don't be afraid to remind people that it's not necessary to include you in direct replies, just because you were on the original CC list. If people are thoughtlessly wasting your time, you should not feel guilty about giving them a subtle reminder that you have better things to do.

Be Consistent

The key to any system is consistency. When you slip out off the tracks (and you will), just put yourself back on. The more you do, the more the habit sets in. Keep coming back; it works if you work it. If you don't own your e-mail, your e-mail owns you. Don't be an e-mail loser. Take charge. Get a system (like this one), personalize it, and then stick to it.


Last night, I did something I haven't done in a long time. I went to bed without my laptop or smart phone. Once in the bed, I did not so much as peek at either screen. I didn't even set an audiobook or podcast to playing as usual. I just laid there in the dark with nothing but silence and my very own thoughts. Not even a book.

As I laid there, for what seemed like forever, I was reminded of a conversation I often have with my son, which goes something like this.

"Caden," I say, "You're not getting enough sleep."

"I can't sleep," he says.

I look at the unshaven hair on his chin and neck. It itches me.

"You know you need at least seven or eight hours of sleep per night," I say. "How much sleep have you been getting?"

"I don't know. About four hours?"

"If you'd stop going to bed so late, you wouldn't be so exhausted and then, when you areawake, you would be more effective. Your mind will be clear. Your brain will work better. You won't be so stressed."

"Yeah," he says. "I tried that. It doesn't work. I just lie there and I can't fall asleep."

"So, just keep lying there until you do!"

"It doesn't work! I just lie there with my monkey mind, thinking about all sorts of things, and I never fall asleep."

And then he gets frustrated. I bark at him about shaving, or his unkempt hair, or his absences at school, unfinished business, or whatever. I'm always dispensing advice - like pestulant drips of water on his forehead. I might as well be thumping him there with my finger. I can see from the way he diverts his eyes that it feels like that - endless criticism, not guidance.

Anyway, so I'm lying there thinking about this. Thinking about that. Thinking about all sorts of things. Monkey mind, as my son said. And just like him, I can't sleep.

Still, I resist the swelling urge to grab my smart phone.

And then it occurs to me. That monkey mind, all that noise in my head, is me. I'm am with myself, in myself, of myself. Even if my thoughts are in the past, or the future, I am experiencing them fully.

I remembered that, as a boy, I once enjoyed this time before sleep. It was my chance to make movies in my mind. I would close my eyes, the lights in the theater would dim, and I would project my stories up there on the back of my eyelids. And I would enjoy them as much, if not more, than the real movies. I would work on them for nights in a row - changing up the scenes, introducing new characters and situations. And then, in the days, I would write or draw or tell stories and people would often ask "Where in hell do you come up with this stuff?"

So, I try it again.

Instead of just letting my mind jump around like a broken time machine, I direct my thoughts. I try to hold them on a single idea or question.

At first, it's very clear that I'm out of practice. Every little thing that comes to mind reminds me of something else - like an inbox full of spam. But, I concentrate. I heave my mind back out of the muck and return it to the idea. Back and forth it goes like this until alas, I'm thinking.

I'm thinking!

I'm not bored, I'm thinking.

Since when did thinking become boredom?

I like thinking!

I like this place of thought. I like this spot here in the dark, in the bed - this perfect place for quiet contemplation. I like the silence. I can hear myself. I'm in good company.

And then, maybe thirty minutes later, maybe an hour, maybe more, I fell asleep.

I woke early, feeling fully rested - energized, but remarkably peaceful. I made a cup of hot tea. The house was quiet. I watched a hummingbird drink from the feeder just outside the window.

I opened my laptop. I didn't jump straight into the email. I didn't open Skype. No Facebook. No Twitter. Not even the daily news. Just a blank page and a blinking cursor.

I wrote.

 

Writing is rewriting. If you want to improve your content, here's an editor's checklist you can use to polish your work.

 

This is a "living document", which I revise, reorganize, and refine as I continue to learn. It was last modified on August 3, 2016.

1. Trim the Fat

Nine times out of ten, and most especially with Web and business writing, as much as twenty percent of the text can be removed without affecting the efficacy of what's written. Trimming the fat makes it better.

For an example, see: Trim Fat From Your Web Content.

Whenever you can shorten a sentence, do. And one always can. The best sentence? The shortest.

- Gustave Flaubert

Things to look for:

  •  Redundancy, superfluity, and excess
  •  Saying the same thing that's already been said
  •  Extra words that may be deleted without changing the meaning
  •  Verbosity
  •  The mention of things not worth mentioning
  •  Repetition
  •  Tangents, digressions, and irrelevancies
  •  Authorial intrusion (be invisible)
  •  Excess modifiers
  •  Idle, nonworking words
  •  Long words where shorter ones will do

2. Rearrange

Strive for:

  •  Unity of subject and scope
  •  Unified tone
  •  Unified stye
  •  A consistent point of view and unified character
  •  Episodes that are unified
  •  Consistent verb tenses
  •  Paragraphs and sentences that are to the point and unified
  •  Content is organized according to the logic of time, space, and a degree of specificity
  •  Pronouns are not ambiguous
  •  Coherent word order and phrasing
  •  Parallel structure
  •  Effective transitions
  •  Coherent beginnings are established
  •  Consistent through the middle
  •  Effective and coherent ending
  •  Parts are put into the right proportions
  •  Important things anywhere, but in the middle
  •  Psychology of emphasis is understood
  •  Repetition of words, sounds, phrases, and ideas is effective
  •  The length of sentences, paragraphs, and chapters is varied
  •  Spaces, pauses, and special typography for emphasis
  •  Things that kill emphasis are avoided

2. Reword

  •  Develop and recognize your own special style
  •  Select the best word, best phrasing, and most effective diction
  •  Improve your diction with shorter, more active words
  •  Scrutinize your verbs and keep them active
  •  Limit modifiers
  •  Appeal to the senses
  •  Use concrete details
  •  Be sensitive to rhythm and sound
  •  Use the sounds of words and words that imitate sounds
  •  Use figurative language
  •  Watch out for misuse of figurative language
  •  Avoid distractors and detractors
  •  Watch for common mispellings and incorrect usages

Reference Resources