My Micro ISV World is a Little Less Lonely

I received a nice email from a fellow named Paul Finn. He’s also slurped from the pipe that is Patrick McKenzie’s blog and HN comment feed. Paul has taken the plunge into the MicroIsv pool with an simple newsletter web app. He also blogs about his adventures at Little ISV.

I don’t know what technology Paul is using to actually send the emails. I’d be interested to see if it’s Amazon’s new email service. According to his blog, the application is written using Django. I think Paul has made an ambitious choice here to do a newsletter service. I’m struggling to imaging which keywords he’ll ever be able to rank for. Also, sending emails is typically a task that Micro ISV’s want to outsource rather than do themselves. I use Mail Chimp to send mine.

If I were Paul, and I wasn’t scared to death of my application becoming a SPAM machine and being blacklisted, I’d design and market it as a dead simple newsletter tool for the computer phobic. The homepage would be the create new newsletter form (with a few hundred words of SEO friendly copy beneath the fold). It would be possible, or nearly possible to create and send a newsletter without ever leaving the homepage. I wish you could embed fonts in emails because then the editor would use a nice font and the whole experience would be that of writing a beautiful letter.

I hope Paul succeeds with Letter Push. His website looks great. I wish Cupcake Wrapper Creator looked half that nice. He’s also a polished writer — check out his blog.

Selling Ad Space on CWC

During my research into selling downloadable PDFs to semi-computer-phobic women I picked up the notion that placing ads on my site would be not only sacrilege, but tantamount to throwing in the towel. I put so much time and effort into attracting users to my site, wouldn’t it be a loser move to send them away for mere pennies?

Placing ads on my site is like saying, “Howdy Jane Potential-customer, I see you’ve just come over from Google to find Kustom Kupcake Wrappers. Well, I have those and you could be making them in seconds for as little as $19.95, and even though this is the only site on the internet where you can design them right in your browser, wouldn’t you rather click on this link for industrial boat wrapping film?” Or worse, sometimes Google realizes the page is not geared toward boat dealers and serves up an ad for premium reusable kupcake wrappers or custom lollipops (they seem to be serving up more relavent ads as time goes on).

Are ads strictly a bad idea when you’re selling a product? Burger King doesn’t let McDonalds put ads in their menu. Perhaps, but it’s an experiment. Nearly everyone who comes to my site is looking for free wrappers. In fact, the page with the word “free” in the title is the second most visited one after the home page. There are many, many free kupcake wrapper templates on the internet. Women have fun on my site. The average engagement time is over 3.5 minutes. I’m competing for eyeball time with other sites and winning. So the questions is: how do I monetize those visitors?

I consistently compare my kupcake wrapper creator site to Patrick McKenzie’s Bingo Card Creator site, because his blog inspired me to get started. Now I don’t have the kind of sales volume he has over there. There are several reasons why. First, the market for kupcake wrappers isn’t as big as for learning to read. Second, let’s compare pain points — on one hand you have a teacher, who can go to bed once she comes up with a fun activity for Friday, and on the hand you have a grandma looking for something fun and creative for her granddaughter’s upcoming birthday party. The big difference is that, for the teacher, it’s her job, and for the grandma, it’s just a nice idea. There are other reasons, like teachers being part of a community who share tips and tricks, where the community of grandma cupcake wrapper decorators is, well, disjoint.

Patrick is laser focused on optimizing his funnel. Placing outgoing links on his site would be like periodically deleting random blocks of his source code repository in an effort to complete a project faster.

For the past year, I’ve never questioned the wisdom of an ad free site, but now it’s time to experiment. During the past two weeks, I’ve placed big flashy ads all over the site. In that time, I’ve made four sales. I typically average one sale per week. I’ve made $11 in ad revenue. I’ll let this play out for a while, but so far I’m feeling good.

P.S. Some ads pay nearly $2 for a single click!

Quick Cupcake Wrapper Creator Update

Sales are going better. I’m making about one sale per week, split pretty evenly between the $19.95 and the $29.95 versons. A quick SQL query reveals that total sales are $502.57. I had to issue one refund to a confused customer who somehow accidentally purchased my product. I flubbed up the PayPal refund due to poor record-keeping and gave it to the wrong customer. So then I refunded the right customer, losing two sales and my appetite for the rest of the day.

Daily visits are steadily increasing. I’m averaging right under 100 visitors per day, which is the threshold where, once crossed, I plan to start A/B testing.

There are 440 total users, 32 of which are paying customers.

I’ve spent about $50 of my own and $200 free Google-Bucks on AdWords. I track conversions, have AdWords integrated with Analytics, and to the best of my knowledge, not one sale can be attributed to an ad campaign. AdWords are turned off indefinitely. Lest you think I didn’t take AdWords seriously, let me tell you, I used 16 different ad groups, each with unique keywords, and each with at least two ads. I targeted Search and Content networks for the US, UK and Oz.

The site is on auto-pilot now — no more long evenings implementing new features or enhancing the copy. I do have a short list of improvement to make. They include: Taking pictures of a dozen cupcake wrapper designs, this time with white backgrounds, finishing cupcake toppers, and adding baseball and Christmas designs.

I had hoped to be generating a lot of traffic right now for Halloween Cupcake Wrappers, but the second page of Google just isn’t cutting it. Oh well, there’s always next year :)

 

OSX Text to Speech and FFMPEG

I’m trying to memorize bible verses. So far I’ve just done a little hacking. OSX has a command line program, “say”, that will do just that, say whatever you tell it to. It has several voices — check out the manual.

BibleGateway has an ESV Bible audio API where you can download bible passages. So I threw together a shell script and a Python script to make MP3 files for the passages I want to memorize.

The first step was to make stock audio: The ordinal numbers I’d need (first, second and third) and all the cardinal numbers (1-150), the books of the bible (Genesis - Revelation) and some pauses. The say command recognizes by the string ”[[slnc 300]]” where e.g. 300 is the length of the pause. I put all these string in a text file and ran the following bash script to produce the stock MP3s.

 

Bash script to make MP3s from OSX’s say command

#!/bin/bash
trap "echo signal received; exit" 0 INT HUP QUIT TERM PIPE SEGV

while read line; do
	lower=$(echo $line | awk '{print tolower($0)}');
	lowertrimmed=$(echo $lower | sed 's/ //g');
	say -o $lowertrimmed.m4af $lowertrimmed;
	ffmpeg -i $lowertrimmed.m4af -ac 1 -ar 22050 -ab 32k $lowertrimmed.mp3 < /dev/null
	rm $lowertrimmed.m4af
done < books.txt

Stock audio complete, the next step was to write a Python script to read in a file with the passages I want to memorize, download them from BibleGateway, and append a header and footer with the passage name. For example, Proverbs 25: 8-10 would sound like this:

Proverbs twenty-five, eight through ten... Do not hastily bring into court for what will you do in the end, when your neighbor puts you to shame? Argue your case with your neighbor himself, and do not reveal another’s secret, lest he who hears you bring shame upon you, and your ill repute have no end... Proverbs twenty-five, eight through ten

 

Python script to make audio bible passages using FFMPEG

#!/usr/bin/env python

import urllib
import sys
import codecs
import re
import getopt
import os.path
import shutil
import os

KEY = 'IP'

OUTDIR = "/Users/joshuapearce/Dropbox/Code/MakeEsvAudio/output"
PASSAGEDIR = "/Users/joshuapearce/Dropbox/Code/MakeEsvAudio/passages"
STOCKDIR = "/Users/joshuapearce/Dropbox/Code/MakeEsvAudio/stock"
TEMPDIR = "/Users/joshuapearce/Dropbox/Code/MakeEsvAudio/temp"  

PAUSE100 = STOCKDIR + '/' + 'pause100.mp3'
PAUSE200 = STOCKDIR + '/' + 'pause200.mp3'
PAUSE300 = STOCKDIR + '/' + 'pause300.mp3'
PAUSE400 = STOCKDIR + '/' + 'pause400.mp3'
PAUSE800 = STOCKDIR + '/' + 'pause800.mp3'
FIRST = STOCKDIR + '/' + 'first.mp3'
SECOND = STOCKDIR + '/' + 'second.mp3'
THIRD = STOCKDIR + '/' + 'third.mp3'
THROUGH = STOCKDIR + '/' + 'through.mp3'

class ESVSession:
    def __init__(self, key):
        textoptions = ['include-short-copyright=0',
                   'output-format=plain-text',
                   'include-passage-horizontal-lines=0',
                   'include-heading-horizontal-lines=0',
                   'include-passage-references=false',
                   'include-first-verse-numbers=false',
                   'include-headings=false',
                   'include-subheadings=false',
                   'include-selahs=false',
                   'include-footnotes=false']

        mp3options = ['output-format=mp3']

        self.textoptions = '&'.join(textoptions)
        self.textBaseUrl = 'http://www.esvapi.org/v2/rest/passageQuery?key=%s' % (key)

        self.mp3options = '&'.join(mp3options)
        self.mp3BaseUrl = 'http://www.esvapi.org/v2/rest/passageQuery?key=%s' % (key)

    def getPassage(self, passage):
        passage = passage.split()
        passage = '+'.join(passage)
        url = self.textBaseUrl + '&passage=%s&%s' % (passage, self.textoptions)
        page = urllib.urlopen(url)
        return page.read()

    def getMp3(self, passage, filename):
        passage = passage.split()
        passage = '+'.join(passage)
        url = self.mp3BaseUrl + '&passage=%s&%s' % (passage, self.mp3options)

        urllib.urlretrieve (url, filename)

def usage():
    print "USAGE: program.py -f /path/to/file"

class Usage(Exception):
    def __init__(self, msg):
        self.msg = msg

# This doesn't work, but I'd like it to. I have no idea what this guy's talking about:
# http://www.artima.com/weblogs/viewpost.jsp?thread=4829
class Error(Exception):
    def __init__(self, msg):
        self.msg = msg

def main(argv=None):
    if argv is None:
        argv = sys.argv
    try:
        try:
            opts, args = getopt.getopt(argv[1:], "f:h", ["file=", "help"])
        except getopt.error, msg:
             raise Usage(msg)

        file_path = ''

        for opt, arg in opts:
            if opt in ("-h", "--help"):
                usage()
            if opt == "-f":
                file_path = arg
        if (file_path == ''):
            usage()
            return 

        f = open(file_path, 'r')
        passages = f.read().splitlines()
        f.close()

        bible = ESVSession(KEY)        

        line_num = 0
        for p in passages:
            line_num = line_num + 1
            if not p.startswith("#"):
                match = re.search("^([0-3]{0,1})([a-zA-Z ]+)([0-9]{1,3}) ?: ?([0-9]{1,2}) ?([\-0-9]{0,3})$", p)
                #try:
                book_num = match.group(1)
                book_num_str = book_num
                book = match.group(2).strip()
                chapter = match.group(3)
                verse_start = match.group(4)
                verse_end_str = match.group(5)
                verse_end = verse_end_str.replace('-', '')
                if book_num != '':
                    book_num_str = book_num + ' '

                passage = '%s%s %s:%s%s' % (book_num_str, book, chapter, verse_start, verse_end_str)
                bgw_filename = '%s%s_%s_%s%s.mp3' % (book_num_str.replace(' ', '_'), book.replace(' ', '_'), chapter, verse_start, verse_end_str)
                bgw_filepath = PASSAGEDIR + '/' + bgw_filename
                cat_filename = bgw_filename.replace('.mp3', '_cat.mp3')
                cat_filepath = TEMPDIR + '/' + cat_filename
                final_filepath = OUTDIR + '/' + bgw_filename

                if not os.path.isfile(bgw_filepath):
                    bible.getMp3(passage, bgw_filepath)

                mp3_files = [PAUSE300]                    

                if book_num == '1':
                    mp3_files.append(FIRST)
                elif book_num == '2':
                    mp3_files.append(SECOND)
                elif book_num == '3':
                    mp3_files.append(THIRD)

                mp3_files.append(STOCKDIR + '/' + book.replace(' ', '').lower() + '.mp3')
                mp3_files.append(STOCKDIR + '/' + chapter + '.mp3')
                mp3_files.append(STOCKDIR + '/' + verse_start + '.mp3')

                if verse_end != '':
                    mp3_files.append(THROUGH)
                    mp3_files.append(STOCKDIR + '/' + verse_end + '.mp3')

                mp3_files.append(PAUSE200)
                mp3_files.append(bgw_filepath)
                mp3_files.append(PAUSE200)

                mp3_files.extend(mp3_files[:-2])
		mp3_files.append(PAUSE800)
                cat_file = open(cat_filepath, 'wb')

                for mp3_file in mp3_files:
                    shutil.copyfileobj(open(mp3_file, 'rb'), cat_file)

                cat_file.close()

                popen_args = ['ffmpeg', '-i', cat_filepath]
                popen_args += ['-metadata', 'title="' + passage + '"']
                popen_args += ['-metadata', 'author="God"']
                popen_args += ['-metadata', 'artist="God"']
                popen_args += ['-metadata', 'album="Verses to Memorize"']
                popen_args += ['-metadata', 'comment="Created by Josh Pearce, www.beechtreetech.com"']
                popen_args += ['-y', final_filepath]
                #print " ".join(popen_args)
                proc = os.popen(" ".join(popen_args))                 

                #except Exception as err:
                    #print "trouble reading line: " + str(line_num)
                    #return 1

    except Usage as err:
        print >>sys.stderr, err.msg
        print >>sys.stderr, "for help use --help"
        return 2

if __name__ == "__main__":
    sys.exit(main())    

 

Resources for say, FFMPEG and the Bible Gateway API

Blog to Podcast

say manual from Apple

Bible Gateway ESV MP3 API

Bill Parcells of Micro ISVs

It just struck me that Patrick McKenzie is the Bill Parcells of Micro ISVs. Patrick wrote a simple program to create bingo cards for teachers He blogged nearly every day for the first year about the marketing process he used to make it a $30K/year side project. The blog is invaluable for folks attempting build a small software business. Patrick latest venture is salon appointment reminders.

Patrick has inspired many software developers, with day jobs, to spend a few hours a week building a niche product. His blog is a formula for success. If your idea has legs, and you follow his advice, you should see some success. One day there needs to be a business owners tree, with Patrick at the top, just like the Bill Parcells coaching tree. Right now, I’m more like Tony Sparano than Bill Belichick, but let me let me tell just one of the many ways I’ve benefited from Partick’s advice:

Dealing with Customers

I’ve read several of Patrick’s blog posts where he addresses customer service. He wisdom has shaped my thinking in this area. According to Patrick: The customer is never at fault. People are accustom to surly customer service so when you are nice, it makes an impression. In software, a bug is often a real frustrating experience for the user, and if they don’t have technical skills, it can make them feel stupid, an emotion which can easily be turned into rage at your software.

A few days ago, someone purchased the pro subscription to Cupcake Wrapper Creator. $24.95, yay! I noticed they were making a lot of wrappers, but they also reset their password three times in two days. At first, I didn’t think much of it, customer’s forget, but later that day, I decided to see if there was a problem with my site. Sure enough, there was. In the password reset email I sent out, I told users to use the “change password” link at the top of the page, but the link wasn’t showing up.

I remember Patrick recounting a time that his print process was offline for a while. He ran some queries, and discovered exactly which users were affected and then sent them a personal email. So I did this. I queried for all the users who had made password reset requests and sent them each a personalized template email. In it I apologized profusely for the trouble I had caused them. Only one customer responded, a teacher, but she gave me a two page gushing email telling me how she’s nearly addicted to making cupcakes wrappers for students, fellow teachers, and grandchildren. She went on to tell me, in detail, which features I need to add to make the product more appealing to the classroom market. If I had never sent the apology email, I likely would never have gotten this response. The praise she laid upon us was so far and above any of the other feedback we’ve received that I of course asked her permission to use it on the site. Ironically, just that day, I had make a simple landing page for classroom cupcake wrappers, but it didn’t have much content. Her testimony added 300 words to the page!!!

Now I need more content for: Halloween Cupcake Wrappers :)

Cupcake Wrapper Creator at Eight Months

Eight months ago we launched a two page website to see if anyone was interested in paying money to design printable cupcake wrappers online. I read somewhere that 700 was the minimum word count for a page to look respectable to Google. So on the landing page we had just over 700 words describing the fun things that Cupcake Wrapper Creator could do and why you should buy it. At the bottom was a “Sign Up” button for the yet non-existent site. Clicking it took you to the “Coming Soon” page which said the service is not ready yet, but if you give us your email address, we’ll be happy to notify you when it is. We wanted to know that enough people were interested before we wasted three months of our lives building a product nobody wants.

Three months later, 36 people had said they would like to be notified when Cupcake Wrapper Creator was ready. While minuscule in absolute terms, they represented 12% of the 291 total visitors who came to the website.  Having over 10% of strangers on the internet giving us their email address for a product that didn’t even exist yet, struck me as a good indication that the design-your-own-cupcake-wrappers-online niche was not being well served. Actually it wasn’t being served at all. We had done the research to know that beforehand. We took the sign-ups as evidence that, if we did build the service, people would pay us for it.

Numbers

On February 27, 2011 Cupcake Wrapper Creator went live. From the next five months we’ve had the following numbers:

  • 26,358 Pageviews
  • 4,837 Unique Visitors
  • 235 Trial Users
  • 20 Paying Customers
  • $283 in Receipts
  • Over 4000 Designs Created
  • Over 7,000 PDFs Downloaded

4.9% of people who visit the website decide to sign up for a trial membership. 8.5% of people who sign up for the trial go on to buy. And overall, 0.41% of visitors decide to purchase. If these figures extrapolate linearly, they mean that if we could increase our traffic from 1,100/month to 5,000/month, then we should get 20 purchases per month. We have two prices: $12.95 and $24.95. Most people choose the less $13 plan, but if we figure the average customer pays $15, then I would be making $300/month. This is actually sort of sad, considering the work we’ve put into the product so far, but we always thought of the cupcake site as a test bed where we would learn how to market and sell digital goods.

Easy Parts

Building the website part was easy since I build enterprise and industrial web applications in my day job. But unlike my day job, I used the latest technologies for an ASP.NET MVC application; Such as the Razor view engine, and two nice open source projects by Troy Goode, the membership starter kit that papers over some glaring holes in the membership capabilities that come out of the box with ASP.NET, and ABsoluteMaybe, an A/B testing plugin.

I just want to take a quick monument to say, I wanted to build this site on Ruby on Rails. I’ve never build a site with RoR, but I’m convinced it’s a more modern framework that ASP.NET MVC and has community contributions for nearly every reusable website feature I could ever need. But I know ASP.NET, and though the server costs are higher, I’m productive in it and the goal of every web business should be to make server costs a rounding error. My time was just too valuable to learn a new framework.

My app is basically a PDF generator. Users customize their cupcake wrappers by choosing colors and fonts and entering text in an HTML form. But, I don’t rely on ASP.NET to process the print queue and generate the PDFs. For that, I’ve tapped into the rich Java ecosystem. At the day job, I’ve wrote a good deal of original functioning code, but for this, I’m just stitching pre-existing APIs together. I highly recommend the Jav-o-sphere for it’s abundance of APIs. Now when the twice weekly Java update pops up, I’m like, “Heck yeah! Update ahoy! ” But, I’m terrible at Java; If I have three or four Google windows open and Eclipse, I might be able to write Hello World swing app. Good thing I haven’t had to write a single line of Java for this application.

I access the rich world of Java by writing JavaScript code and running it in Rhino. Yep, no Scala or Clojure coolness for me. I’ll stick with good old fashion JavaScript in all it’s slow glory on Rhino.

Hard Parts

Making designs for the wrappers is hard. It’s slower than I anticipated and requires much hand editing and tweaking to get them to look just right. All of my templates start out as a Python script with some fun 6th grade geometry. This is a gating task in my process. I’d hoped to hire freelancers to make the templates, but with a budget of $30, that’s not an option.

Harder Parts

Unsurprisingly, marketing the website consumes most of the time I spend on this project. Tasks include: promoting the website on various cupcake forums, writing blog owners, writing copy, hiring freelancers to write articles, flushing money down the AdWords commode, and constant tweaking of the website’s content as my knowledge of how Google sees my site increases. I’ve found it helpful to give my site some basic CMS functionality, so my wife, me, or maybe one day a freelancer can create new content, like the classroom cupcake wrappers page.

Synchronizing Server and Javascript Enumerations

I came up with a quick extension method that you can use to convert a C# enumeration definition to a dictionary. After you get the dictionary, you can then serialize it to JSON, and include it in your JavaScript code using a dynamically generated JS file, or for the hyper organized, incorporate it into your build process so the file is served statically.

Here’s the gist of it:

public enum PetEnum
{
    Dogs = 0;
    Cats = 1;
    Fleas = 2;
}

public static class Dictionary EnumDict(this PetEnum enumConst)
{
    string[] names = Enum.GetNames(enumConst.GetType());
    Array vals = Enum.GetValues(enumConst.GetType());

    var result = new Dictionary<string, int>();

    for (int i=0; i<names.Length; i++)
    {
        result.Add(names[i], (int)vals.GetValue(i));
    }

    return result;
}

// using Newtonsoft.Json.Converters or you favorite JSON serialization method
string petEnumDefJson = JsonConvert.SerializeToString(MyEnum.Fleas.EnumDict());

Now you’ve got a string enumDefJson and you need to stick it into your page somehow as a script. You could make it a model property.

<script language="javascript">
var petEnumDefJson = <%=Model.PetEnumFedJson%>;
</script>

Ruby On Rails, Thin and Nginx on Windows

Are you like me? Do you work in an industry or business which requires you to write web applications that runs on Windows? It’s not all that bad, right? C# 3.0 and 4.0 are pretty nice languages, and ASP.NET MVC is light years ahead of reguar ASP.NET. At my day job, I’m replacing traditional desktop applications with web applications. These are highly functional engineering applications, but they will have very few users at any one time, so they don’t need to run on a full blown web server. One big problem I run into is that ASP.NET applications are tied to IIS.

There are hardly any good ways to ship an ASP.NET web application as a stand alone product, which is a shame, because there are some impressive web applications which can run stand-alone, with an embedded web server and there should be more. It’s not like deploying ASP.NET applications is a total nightmare, there is something called “Web Matrix” and the commercial Cassini web server, plus the Windows Platform Installer, and even http.sys. Web Matrix, from Microsoft includes the “IIS Lite” web server, a stand-alone version of IIS, but it’s bundled with Sql Server Express, Visual Studio Express and Expression Studio — hardly a stand alone option for shipping a web application. The commercial Cassini server, might, I suppose, be an option, but I can’t tell, without purchasing it, whether it’s robust and up to date. It’s not sold by Microsoft so it might be out of sync with their latest technologies. The Windows Platform Installer has taken much of the pain out of packaging and deploying Windows web applications (except it doesn’t run aspnet_regiis.exe for some reason), so that a product like Umbraco can be installed with just a few clicks. Finally, you have http.sys, an HTTP server built into modern versins of Windows. It will allow Console or Windows Forms application to host WCF services, but will not, as far as I can tell, host an entire ASP.NET website.

This blog post is aimed at software developers, not really managers, but it may help you if you’re a manager trying to understand what some of your best developers are thinking. What are they thinking, you ask? Well, they really are stoked about ASP.NET MVC, at least they were for a while, until they realized it’s just a cheap knock-off of Ruby On Rails. Yes, it’s an MVC framework, that’s good, and even better, most of the code that MS ships is pretty solid, but as far as features, it’s consistently a year or two behind RoR. Good web developers want to work with the best technology, so being stuck in asenior web developer position writing ASP.NET, drives them mad after a while. They can’t leave and take an entry-level position learning Rails. So what can they do?

Enter RoR on Windows. It’s actually solves two problems:

  1. Your company needs to deploy a web application what is easy to install and run on any desktop (think sales guy on an airplane).
  2. Your good web developers want to work with awesome technologies.

I didn’t invent it, obviously, but I have will show you the steps I used to get it running.

First, a few caveats:

  1. All I’ve done so far is pull up the default rails application page, not run a full application.
  2. I want this installation to be able to sit anywhere in the file system, but right now it needs to stay in the same path you create it at, e.g. C:\StandAlone
  3. I’ll update this post as a progress further, hopefully developing a full, stand-alone RoR web app on Windows, which can be deployed by just copying files.

Procedure to Get RoR, Thin and Nginx Running on Windows

  1. Download the latest RubyInstaller for Windows.
  2. Install to C:\[Stand_Alone_Dir]\Ruby, where [Stand_Alone_Dir] is what ever you want to call it. I’ll call it C:\StandAlone for now on.
  3. During the install, do not add Ruby to the system path of register ruby file types, we want to make sure the web application can be installed by just copying a the StandAlone directory.
  4. After the RubyInstaller is finished, check out the properties of the shortcut in the start menu called, “Command Prompt with Ruby.” Notice that CMD.EXE opens setrbvars.bat with the /K flag which tells the command windows to run the batch file and remain open. setrbvars.bat uses the %~dp0 variable which stores the directory in which the batch file resides; in this case, it’s the Ruby bin. This little bit of DOS magic will be useful for other scripts.
  5. Download the DevKit from RubyInstaller.
  6. Extract it to C:\StandAlone\DevKit.
  7. Open a Ruby command prompt and CD to C:\StandAlone\DevKit. Run ruby dk.rb to generate the config.yaml file.
  8. Run ruby dk.rb install.
  9. gem install thin –platform=ruby
  10. gem install rails –platform=ruby
  11. Download the Sqlite shell (sqlite.exe) plus the Sqlite Dll for Windows and place them both in C:\StandAlone\Ruby\bin.
  12. gem install sqlite (or gem install sqlite3-ruby for for rails2) (notice here, we don’t use –platform=ruby. I don’t know why, but using the –platform=ruby flag here caused a lot of errors. These gem installations seems to be very smart about knowing if you’re running on a Windows platform, but the DekKit documentations is adamant about using the –platform=ruby flag to avoid downloading binaries for other platforms. )
  13. MK DIR C:\StandAlone\www
  14. CD C:\StandAlone\www
  15. rails new mystandalonewebapp.com
  16. Test out with, thin start
  17. Download Nginx binary for Windows and put in C:\StandAlone\nginx
  18. Create two folders: C:\Standalone\nginx\sites_available and C:\StandAlone\nginx\sites_enabled
  19. Create a file, mystandaloneapp.com.txt in sites_enabled.
  20. Copy C:\Standalone\nginx\conf\nginx.conf to nginx.conf.orig.
  21. Edit nginx.conf to look like:
    worker_processes  1;
    error_log  C:/StandAlone/nginx/logs/error.log;
    events {
        worker_connections  1024;
    }
    http {
        include       mime.types;
        default_type  application/octet-stream;
        log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                          '$status $body_bytes_sent "$http_referer" '
                          '"$http_user_agent" "$http_x_forwarded_for"';
    
        sendfile        on;
        #keepalive_timeout  0;
        keepalive_timeout  65;
        #gzip  on;
        include C:/StandAlone/nginx/sites_enabled/*.txt;
    }
    
  22. Create a file, C:\StandAlone\nginx\sites_enabled\mystandaloneapp.com.txt to look like:
    upstream mystandaloneapp {
    	server 127.0.0.1:3000;
    }
    
    server {
    	listen       8080;
    	server_name  localhost;
    	#charset koi8-r;
    
    	access_log C:/StandAlone/www/mystandaloneapp.com/log/access.log;
        error_log  C:/StandAlone/www/mystandaloneapp.com/log/error.log;
        root       C:/StandAlone/www/mystandaloneapp.com;
        index      index.html;
    
    	location / {
            proxy_set_header  X-Real-IP  $remote_addr;
            proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header  Host $http_host;
            proxy_redirect    off;
            try_files C:/StandAlone/www/maintenance.html $uri $uri/index.html $uri.html @ruby;
        }
    
        location @ruby {
            proxy_pass http://mystandaloneapp;
        }
    }
    
  23. That’s it. now you should be able to run, thin start from within the rails app, and then double click nginx.exe from within the nginx directory, and see the default rails website running at http://localhost:8080.

ESV Audio Bible Chapter Lookup

The Audio version of the ESV bible, by Crossway Bibles sounds great, but unfortunately, it’s not broken down by book and chapter. So, I took the opportunity to learn jQuery mobile and Rhino JS, to make a small mobile website which tells you the audio file and bookmark for each book and chapter of the bible.

It’s not perfect, so please email me if you find it’s a chapter or two off, and I’ll make the correction.

ESV Audio Bible Chapter Lookup

My First Micro ISV

It’s taken me years to realize that a tiny, part-time software business has to be based on a simple idea, which can be implemented fast. Beyond simple and fast, it has to be self-contained. All of the other ideas I had before this one required too much customer hand-holding, e.g. services and support for OpenEMR, an open source electronic medical records system. That idea was totally delusional! EMR systems are probably a close second to ERP when it comes to the amount of custom configuration and training which they require.

Inspiration

I wish I could take credit for this new idea, but that goes to my wife. Inspired by Patrick McKenzie of sight-word bingo fame, I knew I wanted to make a simple web-based service which would appeal to women, not your stiletto-wearing city gals, no, more the Midwestern homemaker type of woman. Now, my wife looks great in a pair of high-heels, but she’s still qualified to answer this question. I said: “Baby, what simple software product can I sell to women?” And BAM, she came up with this great idea for printable cupcake wrappers.

The idea is, you go to the website, sign up, (which will cost something like $20-$30 for a year or two of access) and you’re presented with a selection of template designs for printable cupcake wrappers. There will be all kinds of designs for: birthday parties, anniversaries, Halloween, Independence Day and more. So you pick a design, select what colors you want, and then perform further customizations like putting the birthday celebrant’s name on the wrappers, or making a wrapper with each guest’s name on it. Finally, you download a PDF with your wrapper designs, print them on heavy paper, and cut them out with scissors.

Technical Details

I plan on writing more about the technology I use for this project, but for now, here is a laundry list of them: