Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Test

on:
pull_request:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install node dependencies
run: npm ci
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Run test suite
run: bundle exec rake test
44 changes: 44 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# dotcom

Personal Jekyll site (`www.nateberkopec.com`).

## Test Setup

The test suite mirrors the Speedshop structure:

- `test/ruby/` — Minitest unit tests for Jekyll plugins
- `test/integration/site_test.rb` — HTTP smoke tests against the built site
- `test/integration/link_integrity_test.rb` — full built-site link/resource/anchor validation

### Commands

```bash
bundle exec rake test:ruby
bundle exec rake test:integration
bundle exec rake test
```

Or with mise:

```bash
mise run test:ruby
mise run test:integration
mise run test
```

### Link Integrity Rules

`test/integration/link_integrity_test.rb` checks the built `_site` output and verifies:

- all internal and external links/resources (anchors, images, scripts, stylesheets, forms, media)
- fragment anchors (`#id`) resolve to existing elements
- redirects are allowed
- `mailto:` and `tel:` are syntax-validated only
- external checks retry 3 times, then hard-fail
- requests are parallelized with `Concurrent::ThreadPoolExecutor` (pool size: 25)

### Local Test Server

Integration tests build `_site` and start a local static server automatically on `127.0.0.1` using port `0` (ephemeral).
The assigned port is discovered at runtime and used by the test helpers.
Set `BASE_URL` to run against a specific target instead.
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ gem "csv"
gem "logger"
gem "base64"
gem "bigdecimal"
gem "concurrent-ruby", ">= 1.3"
gem "nokogiri", ">= 1.16"
gem "minitest", ">= 5.22"
gem "cgi", ">= 0.4"

group :jekyll_plugins do
gem "jekyll-postcss-v2", ">= 1.0.2"
Expand Down
17 changes: 17 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ GEM
public_suffix (>= 2.0.2, < 8.0)
base64 (0.3.0)
bigdecimal (4.0.1)
cgi (0.5.1)
colorator (1.1.0)
concurrent-ruby (1.3.6)
csv (3.3.5)
Expand Down Expand Up @@ -59,9 +60,21 @@ GEM
rb-inotify (~> 0.9, >= 0.9.10)
logger (1.7.0)
mercenary (0.4.0)
mini_portile2 (2.8.9)
minitest (6.0.1)
prism (~> 1.5)
nokogiri (1.19.0)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.19.0-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.0-arm-linux-musl)
racc (~> 1.4)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
prism (1.9.0)
public_suffix (7.0.2)
racc (1.8.1)
rake (13.3.1)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
Expand Down Expand Up @@ -110,11 +123,15 @@ PLATFORMS
DEPENDENCIES
base64
bigdecimal
cgi (>= 0.4)
concurrent-ruby (>= 1.3)
csv
foreman (>= 0.90.0)
jekyll (>= 4.4.0)
jekyll-postcss-v2 (>= 1.0.2)
logger
minitest (>= 5.22)
nokogiri (>= 1.16)

BUNDLED WITH
4.0.3
16 changes: 16 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
require "rake/testtask"

Rake::TestTask.new("test:ruby") do |t|
t.libs << "test/ruby"
t.test_files = FileList["test/ruby/**/*_test.rb"]
end

Rake::TestTask.new("test:integration") do |t|
t.libs << "test/integration"
t.test_files = FileList["test/integration/**/*_test.rb"]
end

desc "Run all tests"
task test: ["test:ruby", "test:integration"]

task default: :test
2 changes: 1 addition & 1 deletion _data/social.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
- link: //www.twitter.com/nateberkopec
- link: https://www.twitter.com/nateberkopec
name: Twitter
- link: //github.com/nateberkopec
Comment thread
nateberkopec marked this conversation as resolved.
name: Github
Expand Down
2 changes: 1 addition & 1 deletion _layouts/post.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
{% endif %}
<h1>{{ page.title | titleize }}</h1>
<div style="margin-top: -24px; margin-bottom: 28px;">
<div>by <b>Nate Berkopec</b> (<a target="_blank" href="http://twitter.com/nateberkopec">@nateberkopec</a>)</div>
<div>by <b>Nate Berkopec</b> (<a target="_blank" href="https://twitter.com/nateberkopec">@nateberkopec</a>)</div>
</div>

{% if page.summary %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ It would be immensely helpful to me if other programming authors (self-published
I think "product money" is the eventual goal for nearly all freelancers or solopreneurs. There's probably a lot of developers sitting on the sidelines too wishing they could tell their boss to take this scrum and shove it. And, now that I've made some product money, I can tell you it is pretty awesome. Waking up with more money than you went to bed with is a very, very good feeling. I've been trying to "start a business" since 2008 (you may know me from my appearance on ABC's reality show Shark Tank in 2009). It's always been a struggle for me to find the magical intersection in the theoretical Venn diagram of my interests/skills and what people will pay for. For some reason, I had always avoided information products, because all I knew about was programming, and specifically Ruby on Rails. I think I wanted to get *out* of programming, and make a product for more "normal people", like the infamous Bingo Card Creator.

{% marginfigure /assets/img/posts/37sigproduct.jpg "The copy bragged that it was 'featured on InternetRetailer.com!'" %}
For some reason, as software developer solopreneurs, a lot of us are caught up on SaaS products. Yes, subscription revenue is great. But SaaS is not easy, and the effort of creating one frequently means you're sinking tons of your own development time into something that doesn't end up making any money (or doesn't make enough to compare with your freelancing rate). Worse, as developers, we *love* coding. In fact, a lot of us love it so much that we can code for *years* without actually *shipping* anything! It took me a long time to get past this. In the end, [something from Amy Hoy actually pushed me over the edge](https://unicornfree.com/2013/why-you-should-do-a-tiny-product-first) - she pointed out the 37signal's first product wasn't a piece of software, it was an information product. It was a ~$79 45-page whitepaper about SEO. [You can still download it here.](https://37signals.com/report_search_0103.php) But, of course, it was dripping with the viewpoint, style, and contrarianism that we've come to expect from Basecamp-nee-37signals. All that was important, all that *made* 37signals what it is, could still be delivered in a simple information product rather than a SaaS.
For some reason, as software developer solopreneurs, a lot of us are caught up on SaaS products. Yes, subscription revenue is great. But SaaS is not easy, and the effort of creating one frequently means you're sinking tons of your own development time into something that doesn't end up making any money (or doesn't make enough to compare with your freelancing rate). Worse, as developers, we *love* coding. In fact, a lot of us love it so much that we can code for *years* without actually *shipping* anything! It took me a long time to get past this. In the end, [something from Amy Hoy actually pushed me over the edge](https://unicornfree.com/2013/why-you-should-do-a-tiny-product-first) - she pointed out the 37signal's first product wasn't a piece of software, it was an information product. It was a ~$79 45-page whitepaper about SEO. [You can still download it here.](https://archive.org/search?query=https://37signals.com/report_search_0103.php) But, of course, it was dripping with the viewpoint, style, and contrarianism that we've come to expect from Basecamp-nee-37signals. All that was important, all that *made* 37signals what it is, could still be delivered in a simple information product rather than a SaaS.

That was the last push I needed - I was firmly thinking about creating an information product for programmers. This was in May of 2015.

Expand Down Expand Up @@ -111,7 +111,7 @@ Most writing about "scaling" turns into an ego-measurement contest where "real s
Those are just *my* positions. If you're considering writing about programming yourself, here are some positions that others have taken or that you should consider:

* **Learning should be hard.** This is the entire point of the ["Learn X the Hard Way" series by Zed Shaw](https://learncodethehardway.org/). This is great positioning because it spits directly in the face of decades of "Learn X in 24 Hours" books that dominated bookshelves in the late 90's and throughout the 00's. Zed is writing for a different audience than those books - he wants people who are searching for a deep understanding, not the shortcut method.
* **Learning should be weird.** This is \_why's legacy. The Poignant Guide was an absolutely insane piece of programming how-to. It wasn't just cutesy, oh-so-friendly "MAKE PROGRAMMING LE FUN" material, like, say, [Rails Zombies](http://railsforzombies.org/). It was weird, it was genuinely unique, it was unlike anything that had come before or since.
* **Learning should be weird.** This is \_why's legacy. The Poignant Guide was an absolutely insane piece of programming how-to. It wasn't just cutesy, oh-so-friendly "MAKE PROGRAMMING LE FUN" material, like, say, [Rails Zombies](https://archive.org/search?query=http://railsforzombies.org/). It was weird, it was genuinely unique, it was unlike anything that had come before or since.
* **Look at the dominant groupthink, and head the opposite way.** {% marginfigure /assets/img/posts/fuckyou.jpg "Unpopular opinion: everyone who hated this does not understand why Rails succeeded."%} There seems to be a lot of opportunities here in JavaScript, for example, which is a community with extremely strong groupthink. Who's advocating for *simplicity* in JavaScript? As far as I can tell right now, no one. The bigger and badder the framework, the better. What about *stability* in JavaScript? Right now, it's a matter of learning the hot new framework of the month. Someone that wrote about using vanilla, no-framework JavaScript in a practical setting or wrote about simpler, smaller libraries would probably do really well in this climate, I think. Maybe this already exists, I'm not a big follower of the JavaScript blogosphere. Or with programming languages in general, the meme right now is in favor of strongly typed languages - well where's the classic OO, dynamic-typing advocate? Who's reviving Alan Kay or some of the genius of the original Smalltalk? Look at the well-travelled road, and head the opposite way. No subject area in programming can support only a single, correct position. There's always an alternative, and those alternatives need advocates.
* If positioning interests you, you may want to check out [The 22 Immutable Laws of Marketing](https://www.amazon.com/22-Immutable-Laws-Marketing-Violate/dp/0887306667). It's a classic tome.

Expand Down Expand Up @@ -165,7 +165,7 @@ OK, you're writing a great post every couple of weeks or so, now how do let ever
{% marginfigure /assets/img/posts/sleazyguy.jpg "The easiest way to not be this guy is to actually have a great product." %}
First, let's remember that a good product is the best marketing you can have. If after ~3 months of posting you're still not getting the numbers you want, it means that your writing just isn't hitting the right buttons. Either your subject area is too narrow, your positioning is uninteresting, or you haven't cranked your voice to 11 yet.

{% marginfigure /assets/img/posts/seth_godin_3.jpg "The king of permission marketing, [Seth Godin](http://sethgodin.typepad.com/seths_blog/2008/01/permission-mark.html)." %}
{% marginfigure /assets/img/posts/seth_godin_3.jpg "The king of permission marketing, [Seth Godin](https://seths.blog/2008/01/permission-mark/)." %}
The entire strategy I followed is based around *building permission*. We are giving away awesome things for free (our blog posts, which are like miniature products) in return for the *permission* to market to someone in the future. I give you an awesome, informative blog post, and you give me your email address and the permission to email you in the future (your newsletter). So, with that in mind, our most important distribution channel is gonna be our newsletter.

There's a lot of ways to embed a newsletter in your posts. I've waffled back and forth a lot on this, and ended up with a popup in the lower-right-hand corner that appears after you've scrolled halfway through a post, and an additional signup box at the end of the post. {% marginfigure /assets/img/posts/popup.png "My current newsletter side-popup. It never appears again if you click the X." %} A lot of people choose to go for the big, obtrusive popover for a newsletter signup. You know the kind - it completely blocks out the page content and says "sign up to get my 5 free tips about..." I don't think this works for a technical audience that spends most of their day on the Internet. Maybe it even does work to juice the numbers on newsletter signups, but remember: if they put in their email address just to get your stupid popup to go away, they didn't give you permission to market your product to you later. Newsletter signups are about *capturing* the permission generated by your great content, so you want them to be *useful*, not obtrusive. How can a reader possibly give you permission if they haven't even read your content yet?
Expand Down Expand Up @@ -205,11 +205,11 @@ Again, I recommend getting yourself in the mindset of a $100-tier (a $100-500 pr

In addition, I don't recommend starting with a subscription. These seem far harder to sell nowadays because the competition is so steep - every freelancer *thinks* they want a subscription revenue stream, so there's an endless sea of $10/month screencast services that you'll have to compete against. The secret is that with a high-enough priced product (i.e. not a $10 product), you can make just as much money and experience similar stability as you can with a subscription.

As for actually deciding what my $100-tier product was going to be about and how I was going to sell it, I followed [Brian Harris'](http://videofruit.com/blog/online-course-sell/) process almost to the letter. If you're seriously following this post as a guide, I suggest just heading to him directly but the gist of it is:
As for actually deciding what my $100-tier product was going to be about and how I was going to sell it, I followed [Brian Harris'](https://archive.org/search?query=http://videofruit.com/blog/online-course-sell/) process almost to the letter. If you're seriously following this post as a guide, I suggest just heading to him directly but the gist of it is:

{% marginfigure /assets/img/posts/originalspeed.jpg "Original splash image I used to test the CGRP" %}

* **Write a long-copy style marketing page** - [Here's the actual Google Doc I used when developing the Complete Guide to Rails Performance](https://docs.google.com/document/d/1wOxDoPyroW7hapOYF0g869IEemegmfZF_aezecDO4ng/edit?usp=sharing). Write about 2,000+ words about your product, describing it from all angles. For more about how to write long-copy marketing, [see here](http://videofruit.com/blog/online-course-sell/). You're basically trying to address every possible objection to why someone *wouldn't* buy your product. If you think long-copy marketing pages don't work, you are 100% wrong and clearly have not tried to sell a product like this before. There's a reason why I do it, there's a reason why every successful infoproduct seller does it. They work.
* **Write a long-copy style marketing page** - [Here's the actual Google Doc I used when developing the Complete Guide to Rails Performance](https://docs.google.com/document/d/1wOxDoPyroW7hapOYF0g869IEemegmfZF_aezecDO4ng/edit?usp=sharing). Write about 2,000+ words about your product, describing it from all angles. For more about how to write long-copy marketing, [see here](https://archive.org/search?query=http://videofruit.com/blog/online-course-sell/). You're basically trying to address every possible objection to why someone *wouldn't* buy your product. If you think long-copy marketing pages don't work, you are 100% wrong and clearly have not tried to sell a product like this before. There's a reason why I do it, there's a reason why every successful infoproduct seller does it. They work.
* **Take 50 newsletter subscribers as a cohort and ask them to pre-order** - Time for our first build-measure-learn loop! Take 50 newsletter subscribers at random, and send them your marketing page. Tell them you're trying out this product idea and ask them for some feedback. Videofruit has a specific list of questions to ask, which I also used. When they reply with an answer to your questions, reply back with a thank you and ask them to pre-order at the price point that you're testing. I used Gumroad to take preorders (more on them in a bit).
* **Revise the marketing page based on what you learned.** The survey responses you received will be gold. Go back and revise the marketing page with this feedback. If 5 or more (that is, 10%) of your 50 subscribers actually put in their credit card and hit "pre-order", test a *higher* price point this time. If 5 people ordered at $200, test $300.
* **Take another 50 newsletter subscribers, and try again.** Repeat the process with another set of 50 newsletter subscribers. Again, your goal is for 10% of them to put in their credit cards and hit "preorder".
Expand Down
Loading