<![CDATA[ashleyd.ws]]>https://ashleyd.ws/Ghost 0.7Fri, 18 May 2018 08:22:20 GMT60<![CDATA[Running multiple iOS simulators with React Native]]>Whist developing an app for React Native I wanted to be able to compare the iPhone version vs the iPad version; basically a slightly different layout between the two.

xcode doesn't allow multiple simulators to run and attach to it's debugger but you can run multiple simulators together:

cd /Applications/
]]>
https://ashleyd.ws/running-multiple-ios-simulators-with-react-native/c8116753-0681-4131-8dc1-16b4a94de707Wed, 30 Mar 2016 20:05:08 GMTWhist developing an app for React Native I wanted to be able to compare the iPhone version vs the iPad version; basically a slightly different layout between the two.

xcode doesn't allow multiple simulators to run and attach to it's debugger but you can run multiple simulators together:

cd /Applications/Xcode.app/Contents/Developer/Applications  
open -n Simulator.app  
open -n Simulator.app  

Once open, the simulator hardware can be changed to the desired platform. The problem with this is however getting the apps onto the simulators, and running them both. It's also a pain to have to change each simulator hardware every time it opens.

I modified the react-native run-ios command to allow multiple simulators to be spawned, built against, and run with the RN app. The command is:

`react-native run-ios --simulator "iPhone 6, iPad Retina"

Where the --simulator flag is a CSV of simulators available (checked against xcrun simctl list devices).

Here is is in action:

When https://github.com/facebook/react-native/pull/6179 get's merged, hot loading works too!

]]>
<![CDATA[Custom MarshalJSON in GOLang]]>Getting JSON from structs or any interface{} in GO is pretty trivial, but custom marshalling can be troublesome.

The following struct is based on that in the GO docs but with the json field hints to demonstrate the output modifications produced.

type Message struct {  
    Name string `json:"user"`
    Body string
]]>
https://ashleyd.ws/custom-json-marshalling-in-golang/306ff96f-7732-4256-a245-3ef4ea8b8b15Thu, 24 Mar 2016 10:58:08 GMTGetting JSON from structs or any interface{} in GO is pretty trivial, but custom marshalling can be troublesome.

The following struct is based on that in the GO docs but with the json field hints to demonstrate the output modifications produced.

type Message struct {  
    Name string `json:"user"`
    Body string `json:"message"`
    Time int64  `json:"timestamp"`
}

m := Message{"Alice", "Hello", 1294706395881547000}

b, err := json.Marshal(m)

b == {  
"user":"Alice",
"message":"Hello",
"timestamp":1294706395881547000
}

The above shows how to rename fields during output, but modifying an output requires the a custom MarshalJSON function on the struct. If this function exists, json.encode will call it recurvisely on the struct and any interfaces within the struct. This is also helpful if you cannot modify the struct with the json field hints.

This simple example performs the same modifications as the field hints above and adds the additional ID and Origin fields.

func (m *Message) MarshalJSON() ([]byte, error) {  
    return json.Marshal(&struct {
        ID          string `json:"id"`
        Origin      string `json:"origin"`
        Name        string `json:"name"`
        Message     string `json:"message"`
        Timestamp   int64  `json:"timestamp"`
    }{
        ID:        "Alices-Message-ID"
        Origin:    "Wonderland"
        Name:      m.Name,
        Message:   m.Body,
        Timestamp: m.Time,
    })
}

The problem with this solution is the requirement to specify each of the fields that exist on the Message struct. If we use both the json field hints and the above approach we eliminate this issue.

func (m *Message) MarshalJSON() ([]byte, error) {  
    return json.Marshal(&struct {
        ID          string `json:"id"`
        Origin      string `json:"origin"`
        *Message
    }{
        ID:        "Alices-Message-ID"
        Origin:    "Wonderland"
        Message:   m
    })
}

However, the above function will loop; the outer struct marshalling will call the inner Message struct marshal function and there begins the loop. To circumvent this issue we must instead "copy" the original object; this object will have the same properties of the original but no functions - thus removing the infinite loop.

func (m *Message) MarshalJSON() ([]byte, error) {  
    type Copy Message
    return json.Marshal(&struct {
        ID          string `json:"id"`
        Origin      string `json:"origin"`
        *Copy
    }{
        ID:        "Alices-Message-ID"
        Origin:    "Wonderland"
        Copy:      (*Copy)(m),
    })
}
]]>
<![CDATA[Lightbox Github page reaches 500k page views in 2015]]>During 2015, the Github documentation page for my lightbox plugin reached ~527k page views from ~200k users.

That's excluding ~30k fake referrals from those damn-annoying bots promoting their insurance or SEO services.

Most visitors come via searches but a great deal also land from the Bootstrap expo.

It's interesting how

]]>
https://ashleyd.ws/hitting-the-500k-page-views-for-github-lightbox-page/a8f254d4-5517-4fc0-bfaa-3f449be9b738Thu, 07 Jan 2016 14:46:02 GMTDuring 2015, the Github documentation page for my lightbox plugin reached ~527k page views from ~200k users.

That's excluding ~30k fake referrals from those damn-annoying bots promoting their insurance or SEO services.

Most visitors come via searches but a great deal also land from the Bootstrap expo.

It's interesting how a small side-project that was a by-product of a separate side-project (ekko was going to be a music service, which I got side tracked on and eventually binned) has become one of the main plugins for bootstrap.

Let's see if 2016 can beat the 500k mark!

]]>
<![CDATA[Converting localisation of numerical values in React Native applications]]>TLDR: Not translations with L18N, but handling numerical localisation https://github.com/fixdio/react-native-locale.

The application we are currently writing at fixd.io allows users to enter several numerical values. This poses a problem with localisation due to the point based system used in device locale. As an example, the

]]>
https://ashleyd.ws/localisation-numerical-values-react-native/2e20c582-7cb2-4416-9d2b-41ef79ff5fbfWed, 16 Dec 2015 13:59:50 GMTTLDR: Not translations with L18N, but handling numerical localisation https://github.com/fixdio/react-native-locale.

The application we are currently writing at fixd.io allows users to enter several numerical values. This poses a problem with localisation due to the point based system used in device locale. As an example, the below shows the Danish locale vs the UK locale; the point system in Denmark uses comma notation, in the UK full-stops/periods/points.

The API for the app assumes, and requires, that numerical values passed to it are in decimal format with a point (full-stop/period/.); anything else will fail validation. Although we wanted to build a flexible API that can be used for various endpoints, we didn't feel that we should be handling the conversion of different point notations to the one which we require.

Rather, we enforce the conversion on the client. For our mobile app, this meant converting the notation used to the standard point notation.

There are several existing projects for React Native L18N, but none for localisation such as this. So, it was down to me to write the conversion. iOS has this localisation conversion system built in; I "just" needed to expose it to our React Native JavaScript.

The React Native localisation project is available as open source on the Fixd GitHub: https://github.com/fixdio/react-native-locale.

It allows one to to format numerical values based on the point system to a users locale format in number of different styles, including decimal, currency, percentage etc. The core functionality required for conversion of locale notation to point notation is the function numberFromDecimalString.

This function takes a numerical string in the locale notation and returns the point system value via the Promise resolve function. The function is to be used when a user has interacted to use the value with the API. In the Fixd application, on the Save functionality, we show a loading interface, run the function, wait for the Promise to return, interact with the API, and then display a state change based on success or failure.

]]>
<![CDATA[Realtime Twitter game: ASOS Two-Up]]>ASOS Australia wanted to run an Anzac Day promotional Twitter game similar to that of the Australian game "Two-Up" (https://en.wikipedia.org/wiki/Two-up). The logic behind the game itself is straightforward - flip two coins and compare the result to the users guess - the key challenges surround

]]>
https://ashleyd.ws/realtime-asos-twitter-game/3f8c330c-b43a-4b95-96fe-d66b32b3a631Tue, 17 Nov 2015 16:41:59 GMTASOS Australia wanted to run an Anzac Day promotional Twitter game similar to that of the Australian game "Two-Up" (https://en.wikipedia.org/wiki/Two-up). The logic behind the game itself is straightforward - flip two coins and compare the result to the users guess - the key challenges surround running the game on Twitter in realtime.

The aim of the game was to produce a high engagement and sharing rate on the ASOS AU Twitter account for a period of time building up to, and contributing to the promotions held on the day.

The image shows some basic statistical results. As is shown, the game targets were well exceeded. Over the 5 hour period, the game processed over ten thousand Tweets. From the start I had recognised that ASOS has a huge social media following, and the game may be more popular than imagined; because of this I had built the system so that any number of processors could be started to handle increased traffic.

The automatic reply system meant that the ASOS twitter account had to reply to around 10,000 Tweets in the time period. Twitter has API rate limiting that would normally prevent this, however, the ASOS Twitter account is whitelisted for unlimited API usage.

Game statistics

The code is bespoke to the ASOS game requirements, and honestly rather simple once divided into separate systems. However, hopefully this post can inspire others how to create such a system.

The game system broken down into stages:

  • Listen for Tweets directed to the ASOS AU Twitter account that contain the hashtag used for the game (#TwoUp).
    • Store the Tweet.
  • Process Tweets as quickly as possible in FIFO method:
    • Filter out Tweets with offensive words and retweets.
    • Rate limit users to N entries every 5 minutes.
    • Extract the guess from the Tweet; a string containing a single valid guess was accepted (a user could not have #Heads and #Tails in the same Tweet).
    • If the user guessed correctly; winners were entered into a second round where they had the potential to win a prize.
    • Based on the second round win/loss, winning Tweets were given a prize; the game rules specified that a human was required had to be involved in this process so a realtime dashboard was developed for ASOS to monitor the game and reply to Tweets with prize information.
    • Losing tweets receive an automatic reply from a number of pre-written replies with images.

Simply put; receiving, storing, and processing Tweets. The receiving and processing components were written in Python; for no other reason than I felt it was the best adapted language to the requirements and time period I had to produce the system. The storage system was Redis, and the dashboard used to monitor the game was written in PHP and standard frontend technologies.

Screenshot of the ASOS Australia Twitter account

A couple of example replies are shown in the app screenshot; one person being rate-limited, another losing their try.

For the first stage - receiving Tweets - the Twitter API allows a subscription to be created that allows an API to listen for hashtags for a particular account. Having already worked on a similar process for GOTags, I was familiar with the problems that arise when using a subscription based API; connectivity issues with both their API and mine are potential problems, as too is the possibility that a large volume of data will be received. The receiving endpoint needs to be able to receive a Tweet and offload it into the storage system as quickly as possible.

In-memory databases are favourable when requiring as near real-time systems as possible, as such I used Redis. Data was stored in a list data structure; this allowed me to use POP and PUSH methods on the list; creating a FIFO queue of Tweets waiting to be processed that any of the processor component could access.

A processor process polls the Redis database for Tweets to process. The Tweet is then processed through the game logic, and either passed onto the second stage of the game or immediately replied to with a losing message. These processors had two layers of fault tolerance; on API/database failure the processor would restart itself, if the process completely stopped, Supervisor would restart it.

I started the game with two processors running, but soon realised the volume of Tweets was causing a delay in processing (at a maximum, 30 minutes into the game, the delay was around 45 seconds). Because of this, I threw more server kittens at the game, and started 5 more processors; this reduced the processing to between 1 and 2 seconds. In hind-sight, I should have started large and scaled down if the server was struggling to cope.

The majority of the time spent whilst processing replies was uploading the Tweet images to the Twitter API. It's unfortunate that media images are bespoke to each Tweet, as a faster solution would have been to reuse the same images already hosted on Twitter. However, apart from a few exceptions, the images were mostly under 300kb so the delay was minimal.

The individual components of the system performed well, and no failures were experienced. At the end of the five hour period, the subscription was removed and the processors gracefully stopped.

The game was a huge success and there are rumours flying around that a similar game will be played but on a global basis. The system to manage the amount of Tweets that a global game will attract will be substantial.

]]>
<![CDATA[Latest Projects]]>GOtags - Do more with your Instagram photos

Make your Instagram shoppable! Gotags make your posts linkable and send content straight to your followers' inbox or even add products straight to their
basket! gotags.co.uk

Bootstrap 3 Lightbox based on the modal component

A gallery lightbox built in coffeescript

]]>
https://ashleyd.ws/latest-projects/06938c2d-9c02-42a5-9180-cefb809376edFri, 20 Mar 2015 01:54:38 GMTGOtags - Do more with your Instagram photos

Make your Instagram shoppable! Gotags make your posts linkable and send content straight to your followers' inbox or even add products straight to their
basket! gotags.co.uk

Bootstrap 3 Lightbox based on the modal component

A gallery lightbox built in coffeescript github.com/ashleydw/lightbox

Guess That Theme

I reckon I can guess the theme that website uses guessthattheme.herokuapp.com

GitHub connected

A small ruby app to test neo4j with github data github.com/ashleydw/githubconnected

Just because Go is awesome and you know it github.com/ashleydw/simple-link

]]>
<![CDATA[Facebook's Selenium PHP Event Firing Web Driver]]>Back in late 2013, I wrote the Event Firing webdriver for Facebook's implementation of the Selenium webdriver in PHP.

The reasoning behind the event firing webdriver is that actions can be called at special events (no shit huh). The classes implement the official Java methods fully, and as such, the

]]>
https://ashleyd.ws/facebooks-selenium-php-event-firing-web-driver/74c8c978-2d1b-4d02-88e6-19666bc9c09aThu, 19 Mar 2015 16:00:00 GMTBack in late 2013, I wrote the Event Firing webdriver for Facebook's implementation of the Selenium webdriver in PHP.

The reasoning behind the event firing webdriver is that actions can be called at special events (no shit huh). The classes implement the official Java methods fully, and as such, the cross implementation specifics can be followed.

As an example, below is how to capture a screenshot when an error occurs in the webdriver.

]]>
<![CDATA[Blocking streak.com's email tracking with Chrome and AdBlock Plus]]>Streak.com has recently opened email tracking up to the masses, and whilst this is nothing new (newsletters and companies often do this) it got me wondering if I actually want people knowing when I've opened their email. This is like when Facebook added the dreaded "Seen at" status when

]]>
https://ashleyd.ws/blocking-streak-coms-email-tracking-with-chrome-and-adblock-plus/6f32b907-3143-4298-8003-4511dcf7e4b4Wed, 18 Mar 2015 00:58:23 GMTStreak.com has recently opened email tracking up to the masses, and whilst this is nothing new (newsletters and companies often do this) it got me wondering if I actually want people knowing when I've opened their email. This is like when Facebook added the dreaded "Seen at" status when you read a message - meaning you can no longer open a message and pretend like you've been too busy to check Facebook within the past hour.

The first thing that came to mind was to create an extension to block their tracking image, however AdBlock Plus already does blocking efficiently. So, how do we block streak.com's email tracking? Simple, add a customer filter with the domain mailfoogae.appspot.com.

Do this under "Add your own filters" in the AdBlock Plus options: https://adblockplus.org/en/images/gettingstartedfiltersaddcr3.png?a=show. This domain is streak.com's appspot domain - you can even visit their website through it (which won't work correctly if you add the filter).

]]>
<![CDATA[nginx.conf for Laravel]]>Laravel comes with a .htaccess file by default but this is not useful for nginx. In place of it I present the below conf file built from a variety of sources, and provides the essentials of what you need to get up and running. This is the simplified version that

]]>
https://ashleyd.ws/nginx-conf-for-laravel/eb938f51-dd95-4fb5-b0bf-625d3aff1ef4Wed, 18 Mar 2015 00:58:13 GMTLaravel comes with a .htaccess file by default but this is not useful for nginx. In place of it I present the below conf file built from a variety of sources, and provides the essentials of what you need to get up and running. This is the simplified version that our GOtags service is running - which is on Ubuntu 14.X, a LEMP stack with a few other services thrown in, all hosted from Linode.

]]>
<![CDATA[Populating Magento cart via the API]]>For our recent new service gotags.co.uk, we wanted to integrate directly with Magento to allow users to directly populate their shopping carts from Instagram. Magento, being the beast of a code base it is (seriously, it's stupidly complex) fortunately has the ability for custom SOAP API's to be

]]>
https://ashleyd.ws/populating-magento-cart-via-the-api/577efdb2-7b53-4170-9218-dfe71f19f896Wed, 18 Mar 2015 00:58:02 GMTFor our recent new service gotags.co.uk, we wanted to integrate directly with Magento to allow users to directly populate their shopping carts from Instagram. Magento, being the beast of a code base it is (seriously, it's stupidly complex) fortunately has the ability for custom SOAP API's to be built on top of the already existing SOAP API. Our custom endpoint is quite simple; receive an email, a product SKU, some text, and then attempt to populate the relevant customers cart, returning JSON depending on what happened.

So, how's this done? The code is freely available from the module download but here's the in-and-outs of it, skipping all the boring setup stuff.

The most complex part is attempting to find options based on a string given. This string could be blank, completely random text, YOLO, or actual options like "large blue". These human readable options need to be translated into actual option objects, before the product can be added to the cart:

Once you have the product, the next difficulty is in adding the product to the customer cart. Magento carts become stale and are archived off, and there's no simple way of getting the customer cart, stale or not, instead you must do the leg work yourself. To do this, you need to lookup an active quote, and then get the cart from the quote. The below shows how to find the active cart, or create one if not found. _getStoreId here simply returns the default store id (this is also stupidly complex but not at all interesting).

Download the module from the link above to view the full source code, or if you have a Magento store, you can integrate directly with GOtags and let us do the leg work.

]]>
<![CDATA[Scraping data with phantomjs]]>For the sake of their server, I won't post the link, but recently I needed to scrap a load of data from a fairly detailed page. This is pretty trivial using Ruby and the Nokogiri gem - nothing is particularly hard about scraping data when a gem does all the

]]>
https://ashleyd.ws/scraping-data-with-phantomjs/56a738ab-3bbc-44c0-bd5e-b7118d9fde53Wed, 18 Mar 2015 00:57:52 GMTFor the sake of their server, I won't post the link, but recently I needed to scrap a load of data from a fairly detailed page. This is pretty trivial using Ruby and the Nokogiri gem - nothing is particularly hard about scraping data when a gem does all the hard work for you. However, this particular site in question loads all the data through ajax requests which return JSON. Perfect! Right?

Well not really. I jumped straight for the ajax URL which on first appearance gave me all the JSON-data-goodness I could ever need. However, after some experimenting I started getting 404 errors on pages I knew existed. It turns out that the particular website has some rate limiting, and/or session protection from people like me who are after their data (and yes, it is purely for educational purposes) - if the URL was accessed directly, without going to it's parent loading page, it would throw a 404. The bastards; how dare they make getting their lovely formatted JSON harder than loading a URL.

Attempts with referer faking, cookie setting, getting the urls in particular orders etc failed. So I turned to phantomjs to get the job done. Since phantomjs is effectively a browser, the website in question knew nothing of my plans to harvest all their data.

The long and short of it is in the gist, for your full browsing pleasure. It is by no means a good script - I wanted to implement waitFor throughout (in place of the setTimeout) but I didn't have time [read: couldn't be bothered] to sort out the variable namespacing that phantomjs forces inside page.evaluate.

Happy scraping.

]]>
<![CDATA[The web is dead. Long live the web!]]>I am by no means a Master Splinter of web, my mediocre web experience can be at most gauged at 7 years experience. And most of that has been learning the thing. But lets get one thing straight; if you want to earn money, quit the web design and development

]]>
https://ashleyd.ws/the-web-is-dead-long-live-the-web/8a9c7d27-3d5b-40ea-b9b0-fbf2be2679b2Wed, 18 Mar 2015 00:57:39 GMTI am by no means a Master Splinter of web, my mediocre web experience can be at most gauged at 7 years experience. And most of that has been learning the thing. But lets get one thing straight; if you want to earn money, quit the web design and development freelancing game. There is simply no money to be made.

Well, there is money - but you'll be working too many hours for too little reward. By all means, if you can get a job in an agency, take it. But forget about starting up as a freelancer or starting your own agency; you will end up like the majority of other web hopefuls; you'll either burn yourself out working 60 hour weeks for little monetary compensation, or you'll die trying.

As the rest of the worlds business have done far too often over the past 5 to 10 years, we can blame all this on the recession. However, that's not the problem; the problem is saturation.

Everybody knows somebody "who can make websites". Or has Photoshop. Or once made a shop using WordPress. And good on them, they're learning to code and hoping to make some money on the way. I'm glad things like codecademy.com have been made; learning HTML and CSS inevitably leads onto JavaScript, and JavaScript, no matter what you think of it, is a fully fledged programming language - take node.js for example; who's to say that someone learning on Code Academy right now, after some hard work and time, won't be the next guy coding node modules?

But this saturation is the problem. So much competition has driven down the costs so much, that I can get a full HTML template from Theme Forest for $10, and then pay someone on Fiverr $5 to customise it. When was the last time you wanted to make a full web template for $15? Plug this into WordPress in their wonderfully refined setup, and (let's admit it, even though we hate to), amazing piece of software, and you have yourself a website. $50 dollars, max, will get you a good looking website, with everything you need.

But it's just HTML, I hear you cry! Wrong. There are many sites that do the same. You're a designer? You'll hate 99designs. Off the top of my head, the only other businesses I can think of right now that submit so much tender work for the chance to get given the opportunity to build their implementation is Architecture - but they get paid to tender. And they have great office parties.

But client's want face-to-face contact, I hear you cry! Sure, most of them do to some extent. Remember to include that time in your quote. Maybe they'll share your great ideas with the guy on Fiverr, that'd be a sweet Romeo and Juliet triangle-web-style relationship. Maybe you can even fix it for them once they're finished.

But there are good clients, I hear you cry! Sure there are. You just need to get them, and I wish you all the luck getting them. Just remember, the good clients, the ones with money, usually already have a relationship with a large agency. Or, if they're big enough, they have their own people to do it.

Maybe you're lucky, and you get some clients. They've probably heard of this WordPress thing and maybe even know about pre-built themes. So when you quote them, a (very) modest £2000 for a website, they'll spit their coffee out and demand to know why it's going to cost so much. They'll crunch the numbers: they'll take your estimate of a 3 week delivery time (60 hour weeks, remember?), whittle the three weeks down to two weeks MAX, and finally come out with the absolutely absurd(!) figure that you're earning around £16 an hour.

£16. Pretty ok for a freelancer starting out? Well.. take the tax off, take your expenses off, add the time that you spend communicating with the client onto the design & development estimate, bug fixing, changing that fucking button to flash in a different fucking colour, server maintenance, the occasional security audit... and what will you get? I make it a whole bag of pretty much nothing.

The client will haggle you down, or you'll drop them. If you do, you'll notice two months later something will appear. I guarantee, if you look at the source, it'll be a WordPress site, on a theme, with CSS and JavaScript scattered all over it. But guess what?! It's responsive!! I know what you'll say: "fuck 'em" .

Believe me. I've made the same mistakes. Trying to explain to a fairly computer-literate person that I have to charge them for upgrading the CMS of their website because there's a security flaw or required update in XYZ is not even worth the time in the majority of cases. They won't agree; "if ain't broken, don't fix it!". But you're a developer, you have to, or you'll have that niggling worry that a kid in some other country will abuse the flaw, and post a picture of a cat instead of the logo. So you fix it. You're £16 is probably fast approaching £0 an hour.

I explained once to a client why I charged what I did. The client in question was arguing over that it would "only take an hour or two, can't you chuck it in for free?". No.. Fuck you no. It may actually only take me 10 minutes to do, but I've spent the good part of the past 10 years learning how to do it in 10 minutes. I politely pointed to the contract section that states all work is a minimum of 2 hours, no matter what it is; I wanted that £32 if it killed me. In the end, I couldn't be bothered; the time wasn't worth it, I packed his public files up and gave him his notice.

The long and short of this is, unless you can get good clients constantly coming in, or have a good relationship (and I mean money making relationship) with retainer clients, the time vs income is simply too thinly spread. You are not your own boss, you are the clients employee. You do not get paid holidays, or sick days, or weekends without mail. You don't even get water cooler gossip about the weird guy in accounting; that weird guy in accounting is you.

I admit, there will be some who make a lot of money from freelancing. Lucky them. I am jealous, there I admit. But I've had my share of good times freelancing. I once sat in a beach bar in south India looking out at the sea, sipping my mango juice, and watching an elephant eat bananas - all whilst writing Unit Tests. Freelancing is great, for the times it's good. The rest of the time, it's probably not worth it.

So what's the point of this? You've learnt your PHP, HTML, and CSS, what now? Long live the web!

In an abbreviation. SaaS: Software as a Service (and the inherently similar PaaS). Here's the money to be made. Now all you need is an idea. And you know what? You could even take a good idea that's been done before, and make it great. Don't believe me? Ask my mate Mark Zuckerberg, he'll tell you where it's at.

My next thing? "Facebook, but more social". There's a quote for you, put it on the fridge. I hope the person who once proposed that to me is a millionaire by now.

]]>
<![CDATA[The dreaded first post]]>Writing the first post for a blog sucks. Sucks big time.

Therefore, no more. No more first post.

]]>
https://ashleyd.ws/welcome-to-ghost/a16291b1-8cb3-4a2e-8f9c-487f3bcfd622Wed, 18 Mar 2015 00:00:26 GMTWriting the first post for a blog sucks. Sucks big time.

Therefore, no more. No more first post.

]]>