Compare commits

..

28 Commits

Author SHA1 Message Date
will 344dc4719b Add Plausible Analytics tracking
deploy / deploy (push) Successful in 11s
Wires the site up to the newly-deployed self-hosted Plausible instance
at plausible.reground.org, gated behind hugo.IsProduction so local dev
builds never send events.
2026-08-21 13:23:58 -04:00
will 1d7635a1ce Break up an em-dash in Colleen landing page's credibility bullet
deploy / deploy (push) Successful in 6s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 11:08:24 -04:00
will bd784619bc Add Colleen partnership gift-guide landing/thanks pages
Part of the BOPA list swap with Colleen McCann (Love & Magic Mentoring):
opt-in page bridged for sensitive/intuitive writers, delivery page linking
to the uploaded PDF.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 10:22:16 -04:00
will 0bdf2b982b Add blog section (4 draft posts) and social post drafts for /platform funnel
Continues the content-engine plan: /platform, the homepage link, and the
small-business-infrastructure course are already live. This adds the
remaining pieces — a content/blog/ section with 4 long-form posts (kept
draft:true, so nothing goes live until reviewed) and a committed set of
~12 ready-to-copy social posts in drafts/social-posts.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 12:33:28 -04:00
will e967ddccbc Add /platform devops funnel, point homepage Technical Help link at it
deploy / deploy (push) Successful in 8s
Reuses the existing courses/single template so this doubles as both the
value-validation case study and the small-business-infrastructure email
course opt-in. /devops stays live as the direct contact-form path, just
no longer linked from the homepage.
2026-08-13 11:10:33 -04:00
will 00e8e56113 Update reground-tools script name (downloads-uploader was folded in and renamed)
deploy / deploy (push) Successful in 7s
2026-08-03 14:08:35 -04:00
will 3a40244d7f Add dedicated /devops funnel, leave /contact as generic form
deploy / deploy (push) Successful in 7s
Homepage's Technical Help CTA now points to a qualifying page for
DevOps prospects instead of the bare contact form.
2026-07-30 20:44:55 -04:00
will 2f4c7eaeb6 Switch deploy to CI-build + scoped rsync instead of shared-runner bind-mount
deploy / deploy (push) Successful in 6s
Piloting a fleet-wide change: CI now builds the site in an isolated
container and pushes output via rsync to a forced-command SSH key
restricted (via rrsync) to this site's own docroot only, instead of
relying on the shared Actions runner's blanket /var/www/** bind-mount
capability. See the static-site-deploy Ansible role.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 12:19:24 -04:00
will 09e5f40b31 Add credibility and curriculum preview content to /gift page
deploy / deploy (push) Successful in 4s
Fills the space between the two signup forms so the repeated CTA
reads as intentional rather than an accidental duplicate.
2026-07-26 22:34:53 -04:00
will dbebca1e45 tighten up gift funnel copy
deploy / deploy (push) Successful in 4s
2026-07-24 14:33:13 -04:00
will dbeb634110 Collect first/last name on the course enroll form
deploy / deploy (push) Successful in 4s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 19:47:33 -04:00
will 143ddfda92 removed bad attempt at humor on homepage
deploy / deploy (push) Successful in 3s
2026-07-22 20:03:41 -04:00
will f9d6d775a7 removed tool versions, not needed locally
deploy / deploy (push) Successful in 4s
2026-07-22 19:49:53 -04:00
will 6b7edc5b56 Document the gift/lead-magnet partnership pattern
deploy / deploy (push) Successful in 4s
Maps the four repos involved (reground-site, reground-gift-guide,
downloads-uploader, eec-courses) into one SOP, since none of it is
derivable from reading any single repo on its own.
2026-07-22 19:36:12 -04:00
will 7cbd68f704 Turn /gift into a branch bundle so partner subpages can be added later
deploy / deploy (push) Successful in 3s
Converts content/gift/index.md to _index.md and extracts the shared
hero/form rendering into a partial (courses-body.html) used by both
single.html and a new list.html, so /gift renders identically but can
now hold real child pages like /gift/<partner>/.

The gift.reground.org vanity redirect already forwards any path
generically (redir https://reground.org/gift{uri}), so no
infrastructure change is needed to support future partnership URLs
like gift.reground.org/cheyenne once a matching page exists here.
2026-07-22 19:20:01 -04:00
will c02512f688 Close the two remaining /gift funnel gaps
deploy / deploy (push) Successful in 4s
Add a hero image (rendered from the redesigned PDF's own cover) so the
opt-in page shows the actual resource instead of pure text, and make
the post-submit message PDF-specific with a working /gift-thanks
fallback link instead of a generic dead-end "check your inbox" line.

Both are opt-in per-page params (hero_image, success_message) so the
real drip-course page's rendering is unchanged.
2026-07-22 19:01:27 -04:00
will af69f463f5 Point gift-thanks download link at the redesigned PDF
deploy / deploy (push) Successful in 4s
The regenerated PDF (dark theme, /talk CTA) was uploaded to a new
path since the upload script always namespaces by slug.
2026-07-22 17:25:35 -04:00
will 7da665ffaf Add /talk booking page with cal.com inline embed
deploy / deploy (push) Successful in 4s
The gift PDF's closing CTA promises a conversation, so it needs a
landing page to point to. Kept separate from /gift-thanks since the
PDF can be read/forwarded independent of any signup on this site.
2026-07-22 16:09:45 -04:00
will 2a5b1edf19 Add root-wood mark to the site header
deploy / deploy (push) Successful in 4s
Traced from a piece of petrified root wood the user owns, simplified into
a flat currentColor SVG so it matches the header text in both light and
dark mode, plus a bio line explaining the connection.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 13:12:07 -04:00
will 3d197e5c9a Rename freebie -> gift
deploy / deploy (push) Successful in 4s
Matches the gift.<domain> vanity redirect's own name instead of a
different word for the same page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:05:48 -04:00
will 9953f4ad27 Bring the homepage headshot back into the repo
deploy / deploy (push) Successful in 4s
The Linode bucket added unnecessary indirection for a single small
static image. Commit will.jpg under static/images/ and point the
homepage at it directly; the now-orphaned copy has been deleted from
the reground-downloads bucket. The freebie PDF stays on
downloads.reground.org, unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 09:17:02 -04:00
will aa08908493 Fix accessibility bug: fully comment out disabled main menu entry
deploy / deploy (push) Successful in 4s
[[menu.main]] was left active while its name/url/weight fields were
commented out, so Hugo built one real menu entry with no name or URL.
The header template rendered it as an empty <a href="">, which screen
readers announce using the resolved href (the current page's own URL)
since there's no text or aria-label -- e.g. reading "freebie" on the
freebie page.
2026-07-13 12:48:10 -04:00
will 0a8631a0f2 Simplify homepage to a data-free markdown body with headshot
deploy / deploy (push) Successful in 4s
Replaces the hero_heading/subheading/intro param-driven layout with a
plain markdown body (headshot, Creative Help, Technical Help, bio), so
layouts/index.html just renders .Content. Headshot is served from the
Linode bucket (same convention as the freebie PDF) rather than
committed to git. CSS floats the photo left with the first list
wrapping beside it, matching the site's font/color patterns.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 16:47:14 -04:00
will e3a1c94597 Add /freebie and /freebie-thanks pages for the PDF lead magnet
deploy / deploy (push) Successful in 3s
/freebie reuses the courses/single layout (hero + signup form, same
mechanism as find-your-right-readers) via type: "courses" in front
matter, which controls layout lookup independently of the page's actual
URL — so the signup form is free, without the URL reading as
/courses/... for a single-PDF page. /freebie-thanks is a plain page
whose download link points at the new downloads.reground.org bucket.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 18:44:23 -04:00
will e1ba854abd remove top menu entires for now
deploy / deploy (push) Successful in 4s
2026-07-09 11:07:13 -04:00
will 3e258930a8 Populated eec landing page with actual content
deploy / deploy (push) Successful in 4s
2026-07-09 09:44:14 -04:00
will 0fded0b845 Removed pitch from eec layout 2026-07-09 09:43:36 -04:00
will ca5c8862fd Restructure course landing page into data-driven sections
Splits the single hardcoded-feeling course template into hero,
credibility, curriculum, and repeat-CTA sections, each rendering only
when its front matter fields are set. The enroll form (and its
EEC-trigger JS) moves into a reusable partial so it can appear more
than once on a page without id collisions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 08:51:49 -04:00
34 changed files with 1210 additions and 90 deletions
+21 -3
View File
@@ -7,10 +7,28 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: hugomods/hugo:exts image: hugomods/hugo:exts
volumes:
- /var/www/reground.org:/var/www/reground.org
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
- run: hugo --minify -d /var/www/reground.org - name: Install rsync + ssh client
run: |
if command -v apk >/dev/null; then
apk add --no-cache rsync openssh-client
else
apt-get update && apt-get install -y rsync openssh-client
fi
- run: hugo --minify -d public
- name: Deploy via rsync
run: |
mkdir -p ~/.ssh
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
# The server-side key is forced-command, restricted via rrsync to
# this site's own docroot only (see the static-site-deploy
# Ansible role) — it can push new files but can never read/list
# anything back off the server, and can't reach any other site's
# directory regardless of what path is requested here.
rsync -az --delete -e "ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new" public/ root@reground.org:
env:
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
-1
View File
@@ -1 +0,0 @@
hugo 0.124.1
+123
View File
@@ -107,12 +107,20 @@ header.site .inner {
flex-wrap: wrap; flex-wrap: wrap;
} }
header.site .brand { header.site .brand {
display: flex;
align-items: center;
gap: 0.6rem;
font-family: var(--font-heading); font-family: var(--font-heading);
font-weight: 700; font-weight: 700;
font-size: 1.1rem; font-size: 1.1rem;
color: var(--bg); color: var(--bg);
text-decoration: none; text-decoration: none;
} }
header.site .brand svg {
height: 28px;
width: auto;
display: block;
}
header.site nav { header.site nav {
display: flex; display: flex;
gap: 1.5rem; gap: 1.5rem;
@@ -233,3 +241,118 @@ input:focus, textarea:focus {
height: 1px; height: 1px;
overflow: hidden; overflow: hidden;
} }
/* Course landing pages (layouts/courses/single.html) */
.course-hero {
text-align: center;
}
.course-subhead {
font-style: italic;
color: var(--fg-muted);
margin-top: 0;
}
.course-hero-image {
float: none;
display: block;
width: 100%;
max-width: 320px;
height: auto;
margin: 1.5rem auto;
border-radius: var(--radius);
}
.course-value-prop {
font-size: 1.05rem;
margin: 1.5rem 0;
}
.course-social-proof {
font-style: italic;
font-size: 0.9rem;
color: var(--fg-muted);
}
.enroll-form {
max-width: 24rem;
margin: 1.5rem auto;
text-align: left;
}
.enroll-status {
text-align: center;
}
.course-credibility,
.course-curriculum {
background: var(--bg-raised);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 2rem;
margin: 3rem 0;
}
.course-bullets {
list-style: none;
padding: 0;
margin: 1.5rem 0 0;
}
.course-bullets li {
position: relative;
padding-left: 1.75rem;
margin-bottom: 0.75rem;
}
.course-bullets li::before {
content: "✓";
position: absolute;
left: 0;
color: var(--cta);
font-weight: 700;
}
.course-curriculum-list {
list-style: none;
padding: 0;
margin: 1.5rem 0 0;
}
.course-curriculum-list li {
margin-bottom: 1rem;
}
.course-day-label {
font-family: var(--font-heading);
color: var(--cta);
font-weight: 700;
}
.course-repeat-cta {
text-align: center;
margin-top: 3rem;
}
/* Cal.com inline embed (layouts/partials/cal-embed.html) */
.cal-embed {
width: 100%;
height: 700px;
border-radius: var(--radius);
overflow: hidden;
margin: 2rem 0;
}
@media (max-width: 30rem) {
.cal-embed {
height: 600px;
}
}
/* Home page: hero photo floats left, first list wraps beside it, second
section clears below once the photo's height runs out. */
main img {
float: left;
width: 220px;
height: auto;
margin: 0 1.5rem 1rem 0;
border-radius: var(--radius);
}
main h2:nth-of-type(2) {
clear: left;
}
@media (max-width: 30rem) {
main img {
float: none;
width: 100%;
max-width: 220px;
margin: 0 auto 1.5rem;
display: block;
}
}
+21
View File
@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 978 207" role="img" aria-hidden="true">
<g transform="translate(0,207) scale(0.1,-0.1)" fill="currentColor" stroke="none">
<path d="M3470 1989 c-213 -19 -348 -59 -575 -170 -263 -127 -460 -178 -880
-225 -128 -14 -215 -16 -585 -11 -252 4 -513 13 -620 22 -343 29 -535 -1 -628
-96 -52 -53 -75 -129 -69 -224 4 -44 13 -102 22 -130 8 -27 20 -84 26 -125 13
-94 74 -283 105 -329 71 -104 223 -138 575 -126 136 5 213 3 226 -3 13 -7 170
-8 494 -2 496 9 590 17 825 64 65 13 157 29 204 35 210 28 305 45 445 82 273
72 489 106 599 94 33 -3 106 -18 161 -32 150 -37 191 -43 425 -53 118 -6 245
-17 283 -25 74 -16 181 -60 211 -87 36 -33 172 -97 301 -141 161 -56 215 -79
272 -117 24 -17 48 -30 54 -30 5 0 9 -4 9 -9 0 -20 237 -250 279 -272 65 -33
143 -36 233 -9 77 23 168 67 168 82 0 4 7 8 15 8 8 0 36 20 62 45 25 25 50 45
54 45 4 0 21 16 38 35 17 19 32 35 34 35 3 0 31 16 62 35 71 42 123 58 290 85
212 34 325 58 610 130 248 62 394 91 750 151 223 37 492 102 857 209 171 50
359 132 476 207 64 41 152 121 152 138 0 7 7 15 15 19 8 3 15 14 15 25 0 11 9
26 19 34 23 15 103 173 155 303 47 118 49 179 7 226 -24 28 -36 32 -90 36 -52
4 -79 -2 -189 -38 -70 -24 -206 -76 -302 -117 -331 -141 -468 -183 -780 -237
-150 -26 -391 -61 -495 -71 -116 -12 -401 -56 -495 -76 -123 -27 -335 -63
-530 -91 -214 -30 -490 -30 -673 1 -255 43 -347 72 -758 238 -171 70 -342 129
-749 258 -628 199 -808 232 -1110 204z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+15 -10
View File
@@ -1,13 +1,18 @@
--- ---
title: "ReGround" title: "ReGround"
hero_heading: "Your story deserves readers who feel it." description: "DevOps consulting, ghostwriting, and helping fiction authors find their right readers"
hero_subheading: "ReGround helps fiction authors find the readers who were always meant to find them."
intro: |
Writing a novel is a years-long act of faith. Finding the readers who will
actually connect with it shouldn't be left to chance. I work with authors
to understand both sides of that relationship — what it took to write the
book, and what a reader is searching for when they pick one up — and use
that to build a path between them.
cta_text: "Get in touch"
cta_link: "/contact/"
--- ---
![Will Estes](/images/will.jpg)
## Creative Help
- Go from "author" to [someone your fans seek out](/gift).
- Help your customers go from "found you" to "trust you." [See it in action](/courses/find-your-right-readers/)
## Technical Help
- Enterprise-grade reliability for small
businesses, without the overhead. [See how](/platform/).
Will Estes lives outside Atlanta with his children and one very opinionated cat. He plays classical guitar and listens to country music. His favorite season is soup season. The mark by the site name is traced from a piece of petrified root wood he keeps on his desk.
+4
View File
@@ -0,0 +1,4 @@
---
title: "Blog"
description: "Notes on running production infrastructure for small businesses, one honest mistake at a time."
---
@@ -0,0 +1,73 @@
---
title: "How I Run Infrastructure for Five Small Businesses From One Server"
date: 2026-08-14
draft: true
description: "A full walkthrough of the hub-and-spoke setup behind five real small businesses — what's shared, what's scoped per brand, and why that split is the whole point."
---
Right now, one hub server runs the entire technical backend for five real
small businesses. An author's site. A short-fiction writer's site. A
small-batch soap shop. A literary press running open submissions. A small
press with multiple authors. Different industries, different products,
different customers — same infrastructure underneath, run by one person.
This is the long version of the story I tell on [/platform](/platform/).
Here's what "one hub, five brands" actually looks like, mechanically.
## What's on the hub
One server runs:
- Mail for every domain — inbound and outbound, for every business, from one
mail platform instead of five.
- The CI/CD pipeline that builds and ships every site, on every push to its
own repo.
- The email courses and campaigns that go out under each business's own
name and voice, from a shared engine that just happens to run five times.
- A shared Postgres database, holding each business's data in its own scope.
- The checkout and fulfillment system behind every book and product sale.
- Continuous monitoring watching all of it, all the time, instead of five
separate someones checking in occasionally.
Each of those five businesses would otherwise need to find, pay for, and
separately manage their own version of every single one of those pieces —
their own email platform, their own checkout stack, their own hosting
provider, their own monitoring. Here, it's one system, config-scoped per
brand, watched by one person the whole time.
## What "config-scoped" actually means
This isn't five businesses crammed into one undifferentiated system hoping
nothing collides. Each business's site, mailing list, and checkout
configuration are separate, brand-specific settings layered on top of the
same underlying engine — not separate codebases someone has to keep in sync
by hand.
When a real fix is needed — a security patch, a new feature, a bug fix — it
gets written once, verified once, and it's live for every business
immediately. When something is client-specific — their branding, their
content, their pricing — it lives in that client's own configuration, and
nowhere else.
That split is the entire value proposition: shared reliability, individual
identity.
## The part that actually makes this safe
None of the above works if a routine, one-line change to one business's site
could somehow reach into another's, or if "shared server" quietly meant
"shared risk." The thing that makes consolidation safe isn't which machine
everything runs on — it's how tightly every path between the pieces is
scoped. That's a big enough topic on its own that it gets [its own
post](/blog/guardrails-for-running-production-alone/).
## Is this the right shape for your business?
If you're currently paying for and separately managing a stack of vendors —
a hosting provider, an email platform, a checkout processor, a project
tool — that have never heard of each other, this is what the alternative
looks like in practice: one coherent system, run by someone personally
accountable for how the pieces fit together.
If that's where you are, the [5-day email course on /platform](/platform/)
walks through why this approach holds up, one honest mistake at a time.
@@ -0,0 +1,83 @@
---
title: "The Guardrails That Let One Person Safely Run Production Email for Five Businesses"
date: 2026-08-14
draft: true
description: "What actually happens when you're not around and something breaks — the specific, boring mechanisms that make one-person-run infrastructure safe."
---
The honest question anyone should ask before trusting one person with their
technical backend is: what happens when you're not around and something
breaks? Not "do you have good intentions" — systems don't run on intentions.
Here are the specific, unglamorous mechanisms that make it safe, today, in
production, for five real businesses.
## A routine push can never trigger a real send
Every campaign content repo I run has the same rule built into the tooling
itself, not just into a process someone has to remember: pushing content
changes can only ever produce a *draft* campaign. There's no field, no flag,
no config value that lets a routine content push schedule or trigger an
actual send to real subscribers.
Sending for real requires a separate, deliberate command — normally
triggered by pushing a dedicated `send/<slug>` tag, not a content commit.
If the worst outcome of a typo'd push is "an unsent draft appeared,"
the failure mode is designed correctly.
## Storage that's supposed to be private can't quietly go public
Every object storage bucket I run defaults to private. There's an automated
check in CI that fails the build if a change would make one publicly
listable — unless there's a deliberate, documented comment explaining
exactly why, right next to the code making the change. Not a wiki policy
somewhere. A build that won't go green until you either fix it or justify it
in writing.
## One business's access can't reach another's
Each site I run has its own deploy key, and each key is scoped so narrowly
it can only touch that one site's own files — nothing else on the server, no
other business's credentials or data. If one site's repo were ever
compromised, the blast radius stops at that one site.
This is worth being specific about, because "shared server" and "shared
risk" get conflated constantly. The thing that actually determines whether
one business's problem can become another's isn't which physical machine
they're on — it's whether access is properly scoped. You can have that
property, or fail to have it, on a dedicated server or a shared one.
## Anything touching real customer data defaults to "show me," not "do it"
Every ops script that could affect real subscribers — bulk name backfills,
unsubscribes, list edits — defaults to a dry run. Actually writing a change
requires an explicit `--apply` flag, every time, no exceptions. The safe
path is the path of least resistance; the dangerous one takes an extra,
deliberate step.
This isn't hypothetical caution. When one client's mailing list switched to
double opt-in partway through, their earlier import batch — people who'd
already agreed to be on the list through a real prior relationship, just not
yet confirmed in the system — stayed exactly as `unconfirmed`, honestly
reflecting what was actually true about them, instead of getting swept into
a clean-looking but false "confirmed" count. Boring and correct beat clean
and wrong.
## You verify after every step, not just at the end
Moving a production deploy key off a root account isn't safe to do in one
commit if you're doing it carefully. It took three: move the key to a
dedicated account, verify the deploy still works. Fix the account's shell
so the key actually functions, verify again. Remove the old root-level
access, verify a final time. Three commits, three separate verifications —
because "I'm pretty sure that worked" isn't good enough on a production
system running mail for five businesses.
## None of this is exotic
Every mechanism above is the ordinary, unglamorous work of making the
dangerous action harder than the safe one — done once, for the whole
system, instead of relied on per person, per day. That's what "safe to hand
this to someone else" actually means in practice.
Curious how the whole system fits together? Start at
[/platform](/platform/).
@@ -0,0 +1,60 @@
---
title: "Why Small Businesses Don't Need a SaaS Stack"
date: 2026-08-14
draft: true
description: "The real cost of renting a different tool for every job — and what the alternative looks like once you stop assuming it's the only option."
---
Ask most small businesses how their technical backend is put together, and
you'll get some version of the same answer: a checkout processor here, a
storefront platform there, a separate email tool, a separate hosting
provider, maybe a separate project tracker — a different vendor for every
job, each with its own login, its own bill, its own outage schedule, and
none of them aware the others exist.
That's not a personal failing. It's the default path, because every one of
those tools is easy to sign up for individually and hard to evaluate as a
system. Nobody sits down and designs a five-vendor stack on purpose — it
just accumulates, one "we needed this by Friday" decision at a time.
## What that actually costs
The sticker price of each individual tool is rarely the real cost. The real
cost is what happens at the seams: the storefront platform doesn't know
about the email tool's suppression list, so a customer who unsubscribed
from one still gets marketed to by the other. The checkout processor's
receipts don't match the bookkeeping tool's categories, so someone
reconciles them by hand every month. Nobody owns the whole picture, because
no single vendor is responsible for more than their own piece of it.
And when something breaks at a seam — a webhook silently stops firing, a
sync job quietly drops records — there's often no single person whose job
it is to notice, because it's not clearly any one vendor's fault.
## The alternative isn't "bigger," it's "coherent"
The fix for SaaS sprawl usually gets pitched as an enterprise platform —
Salesforce-scale software that can theoretically do everything one tool at
a time was doing. But that trades a pile of small vendors for one giant one,
with all the same seam problems just moved inside a single product's
byzantine configuration, plus an enterprise price tag a small business never
needed.
The actual alternative is smaller than that: one coherent system —
hosting, email, checkout, project tracking — run as one thing by someone
personally accountable for how the pieces fit together, instead of managed
piecemeal across a stack of vendors who've never heard of each other. Not
bigger. Just not fragmented.
That's the whole premise behind the way I run infrastructure for five real
small businesses today — one hub, config-scoped per brand, one person
watching the whole system instead of five partial views of five different
pieces.
## If this sounds like where you are
If your current setup is "it mostly works, but I'm the one reconciling the
seams by hand," it's worth seeing what the non-fragmented version actually
looks like in practice — not in theory, but running, today, for real
businesses. The [5-day email course on /platform](/platform/) walks through
it, one honest mistake at a time.
+56
View File
@@ -0,0 +1,56 @@
---
title: "Template, Don't Fork: How a New Client Goes Live Without Inheriting Anyone Else's History"
date: 2026-08-14
draft: true
description: "Why onboarding a new business starts from a genuinely clean slate — and why that's cheap and low-risk instead of a corner cut."
---
When a new business signs on, the easy shortcut would be to copy an existing
client's site, swap out the branding, and call it done. It's faster to set
up. It's also a quiet liability: that new site inherits the old one's full
git history, whatever secrets or config might have leaked into old commits,
and a codebase shaped by decisions made for a completely different business.
Every new site I bring on instead starts from a template, generated fresh —
not forked, not cloned. Zero inherited commit history. Zero leftover config
from whoever came before. The skeleton — the site structure, the shared
tooling — is identical across every client, because it's the proven,
already-working shape. What diverges from day one is content, branding, and
the client's own secrets, nothing else.
## Why this matters more than it sounds like it should
Onboarding risk is usually invisible until it isn't. A forked repo carries
forward every decision — good or bad — that was ever made in the original
site's history, plus whatever accumulated in there that nobody remembers is
still there. A generated repo carries forward none of that. The new
business gets exactly the current, proven template, with a clean history
that's theirs alone from commit one.
It also means a shared tool can be pinned to a specific, tested version per
client, rolled out deliberately, one business at a time — instead of every
site silently inheriting whatever the "source" repo happened to be running
on the day it was forked.
## What actually diverges between clients
Looking across the five real businesses running on this setup, what's
different from one to the next is narrow and deliberate: their content,
their branding, their pricing, their own credentials for their own mailing
list. What's identical is the skeleton underneath — the site structure, the
shared engines for email and checkout, the deployment pipeline.
That's what makes bringing on a new business cheap and low-risk instead of a
bespoke project every time: there's no "figuring out this client's
infrastructure from scratch." There's a proven shape, and a short list of
things that actually need to be decided for them specifically.
## What this means if you're considering this for your own business
Onboarding you doesn't mean untangling your business from someone else's
history, and it doesn't mean building your infrastructure from a blank
page either. It means slotting your business into a shape that's already
running, safely, for others — with your own data and credentials scoped
to you, and nothing carried over that shouldn't be.
More on how the whole system fits together: [/platform](/platform/).
@@ -1,11 +1,39 @@
--- ---
title: "Find Your Right Readers" title: "The Find Your Right Readers Roadmap"
description: "A free email course for fiction authors on finding the readers who were always meant to find their story." description: "How to find readers who actually need your stories? avoid these 5 mistakes that lead to 0 sales on launch day, writing for an audience of 1 (just your mom), and posting every day for a year with nothing to show for it (even if you have zero platform and hate marketing)"
eec_slug: "find-your-right-readers" eec_slug: "find-your-right-readers"
pitch: |
Most advice about "finding readers" is really just advice about marketing — # Optional italic line under the title, for handling an objection up front.
ads, algorithms, funnels. This is different. Over a few short emails, at hero_subhead: "How to find readers who actually need your stories? avoid these 5 mistakes that lead to 0 sales on launch day, writing for an audience of 1 (just your mom), and posting every day for a year with nothing to show for it (even if you have zero platform and hate marketing)"
whatever pace you want to take it, I'll walk through how to think about who
your right readers actually are, and how to build a real path to them # Optional line under the signup button.
instead of shouting into the void. # social_proof: "Join a few hundred authors who've already started."
# Optional label on every "Send me day one" button across the page.
cta_text: "Get me started!"
# Optional "who's teaching this and why" section. Both fields are independently
# optional — set either or both.
credibility_heading: "Written by Will Estes, who's..."
credibility_bullets:
- "Noticed that most advice on selling your book is marketing advice about hustling."
- "Believes that most writers want to think about stories, not their next ad campaign."
# Optional day-by-day curriculum preview. `label` is optional per item.
curriculum_heading: "Here's what's inside"
curriculum_intro: "Five short emails, sent at whatever pace you want."
curriculum_items:
- label: "Day 1."
text: "Mistake #1: Writing for everyone who reads books. (Why a smaller target means more hits.)"
- label: "Day 2."
text: "Mistake #2: Perfecting your book before you start building your audience"
- label: "Day 3."
text: "Mistake #3: Selling the plot instead of the feeling."
- label: "Day 4."
text: "Mistake #4: Looking for readers in the wrong places. (Finding the people who need your stories means not talking to the ones who don't.)"
- label: "Day 5."
text: "Mistake #5: Quitting before the compounding kicks in."
# Optional heading above the repeated signup form at the bottom of the page.
repeat_cta_heading: "Ready to find your right readers?"
--- ---
+10
View File
@@ -0,0 +1,10 @@
---
title: "Enterprise-grade reliability, without the overhead"
layout: "devops"
description: "DevOps help for small businesses — a simple, secure setup that grows with you, without the enterprise price tag."
hero_subhead: "Tell me what's going on, and I'll tell you honestly what's worth fixing."
proof_bullets:
- "My approach is simple and secure by default, and grows easily as your traffic does — no unnecessary complexity, no vendor sprawl."
- "I run the funnels and mailing list behind it myself, so your site, forms, and list work as one system instead of a pile of disconnected tools."
fit_note: "Best fit for small businesses who want this handled as one thing, not managed piecemeal across a stack of vendors. Not a fit if you're specifically on WordPress or need someone to manage WordPress plugins."
---
+8
View File
@@ -0,0 +1,8 @@
---
title: "Here's your PDF"
description: "Download Three Steps to Find the Readers Who Need Your Stories."
---
Here's your copy of *Three Steps to Find the Readers Who Need Your Stories.*
[Download the PDF](https://downloads.reground.org/three-steps-find-your-right-readers/three-steps-find-your-right-readers.pdf)
+39
View File
@@ -0,0 +1,39 @@
---
title: "Three Steps to Find the Readers Who Need Your Stories"
description: "Three simple steps for fiction authors to find the readers who actually need their stories, instead of guessing and hoping."
type: "courses"
eec_slug: "three-steps-find-your-right-readers"
# Optional italic line under the title, for handling an objection up front.
hero_subhead: "Three concrete steps to find the readers who need your stories."
# Optional image shown in the hero, below the subhead.
hero_image: "/images/gift-cover.png"
# Optional label on every "Send me day one" button across the page.
cta_text: "Send me the PDF"
# Optional "who's teaching this and why" section. Both fields are independently
# optional — set either or both.
credibility_heading: "Written by Will Estes, who's..."
credibility_bullets:
- "Noticed that most advice on selling your book is marketing advice about hustling."
- "Believes that if there's one reader who needs your story, there's more than one. You just have to find them."
# Optional curriculum preview.
curriculum_heading: "Here's what's inside"
curriculum_intro: "Three short steps, not another growth-hacking checklist."
curriculum_items:
- label: "Step 1."
text: "Find the review that proves someone already needed your story."
- label: "Step 2."
text: "Thank that reader in a way every future reader can see."
- label: "Step 3."
text: "Share what they connected with so the next reader who needs it can find it."
# Optional heading above the repeated signup form at the bottom of the page.
repeat_cta_heading: "Ready to find the readers who need your stories?"
# Optional override of the enroll form's post-submit message (rendered as markdown).
success_message: "Your pdf will be in your inbox in a few minutes. Can't find it? [Grab it here](/gift-thanks)."
---
+8
View File
@@ -0,0 +1,8 @@
---
title: "Here's your PDF"
description: "Download The Quiet Way to Reach the Readers Who Actually Need Your Story."
---
Here's your copy of *The Quiet Way to Reach the Readers Who Actually Need Your Story.*
[Download the PDF](https://downloads.reground.org/colleen/colleen.pdf)
+39
View File
@@ -0,0 +1,39 @@
---
title: "The Quiet Way to Reach the Readers Who Actually Need Your Story"
description: "For sensitive, intuitive writers who've been told to hustle louder: three concrete steps to find the readers who already need your story, without turning up the noise."
type: "courses"
eec_slug: "colleen"
# Optional italic line under the title, for handling an objection up front.
hero_subhead: "You don't need a bigger microphone. You need the one reader who's already looking for you."
# Optional image shown in the hero, below the subhead.
hero_image: "/images/gift-colleen.png"
# Optional label on every "Send me day one" button across the page.
cta_text: "Send me the PDF"
# Optional "who's teaching this and why" section. Both fields are independently
# optional — set either or both.
credibility_heading: "Written by Will Estes, who's..."
credibility_bullets:
- "Noticed that most advice on selling your book is marketing advice about hustling louder."
- "Believes that if there's one reader who needs your story, there's more than one. You just have to find them, not shout until they find you."
# Optional curriculum preview.
curriculum_heading: "Here's what's inside"
curriculum_intro: "Three short steps, not another growth-hacking checklist."
curriculum_items:
- label: "Step 1."
text: "Find the review that proves someone already needed your story."
- label: "Step 2."
text: "Thank that reader in a way every future reader can see."
- label: "Step 3."
text: "Share what they connected with so the next reader who needs it can find it."
# Optional heading above the repeated signup form at the bottom of the page.
repeat_cta_heading: "Ready to find the readers who need your story?"
# Optional override of the enroll form's post-submit message (rendered as markdown).
success_message: "Your pdf will be in your inbox in a few minutes. Can't find it? [Grab it here](/gift/colleen-thanks)."
---
+52
View File
@@ -0,0 +1,52 @@
---
title: "Small Business Infrastructure Without the SaaS Tax"
description: "Enterprise-grade reliability for small businesses, without the overhead — see how it actually works, then get the 5-day email course explaining why."
type: "courses"
eec_slug: "small-business-infrastructure"
hero_subhead: "I run the hosting, email, and checkout for five small businesses off one server, by myself. Here's what that actually looks like — and a free 5-day course on why it's built this way."
cta_text: "Get me started"
credibility_heading: "Written by Will Estes, who's..."
credibility_bullets:
- "Running the entire technical backend — hosting, email, checkout, project tracking — for five real small businesses right now, from one server."
- "Built the guardrails himself: safe-by-default sends, scoped access per business, dry-run-by-default tools — so nothing dangerous happens by accident, on a good day or a bad one."
curriculum_heading: "Here's what's inside"
curriculum_intro: "Five short emails, sent at whatever pace you want."
curriculum_items:
- label: "Day 1."
text: "Mistake #1: Assuming \"enterprise-grade\" means \"enterprise-sized.\""
- label: "Day 2."
text: "Mistake #2: Renting a different tool for every job because that's what everyone uses."
- label: "Day 3."
text: "Mistake #3: Trusting a person to be careful instead of a system that makes the mistake impossible."
- label: "Day 4."
text: "Mistake #4: Assuming one shared server running several businesses is riskier than five separate ones."
- label: "Day 5."
text: "Mistake #5: Waiting until something breaks to decide who's actually responsible for keeping it running."
repeat_cta_heading: "Ready to see how this could work for you?"
---
## What five businesses running on one server actually looks like
Right now, one hub server runs the entire backend for five real small businesses: an author's site, a short-fiction writer's site, a small-batch soap shop, a literary press running open submissions, and a small press with multiple authors. Same hosting, same email system, same checkout system, same project tracking — configured per business, but built and run once.
That one hub runs mail for every domain, the CI/CD that ships every site, the email courses and campaigns that go out under each business's own name, a shared Postgres database, the checkout and fulfillment system behind every book and product sale, and the monitoring that watches all of it, continuously. Each business would otherwise be paying for and separately managing its own version of every one of those — its own email platform, its own checkout stack, its own hosting, its own project tool. Here, it's one system, config-scoped per brand, watched by one person the whole time.
### The guardrails that make this safe to hand to someone else
Running five businesses' infrastructure from one place only works if a routine change can never accidentally become a real problem. A few of the specific ways that's enforced, today, in production:
- **A routine content update can never trigger a real send.** Publishing changes always produces a draft. Sending something for real requires a separate, deliberate action — nothing routine can trigger it by accident.
- **One business's access can't reach another's.** Each site's deploy access is scoped narrowly enough that even if one were compromised, it has no path to any other business's files or credentials.
- **Anything that touches real customer data defaults to "show me what this would do," not "do it."** Scripts that could affect real subscribers or real records require an explicit, separate flag to actually act — never the default.
- **Storage that's supposed to stay private can't quietly become public.** An automated check fails any change that would expose it, unless there's a deliberate, documented reason attached.
None of this is exotic. It's the ordinary, unglamorous work of making the dangerous action harder than the safe one — done once, for the whole system, instead of relied on per person, per day.
### Is this a fit for your business?
This is built for small businesses that want their technical backend handled as one coherent thing, by someone who's personally accountable for how the pieces fit together — not managed piecemeal across a stack of vendors who've never heard of each other. If that's where you are, the 5-day course above walks through why this approach holds up, one honest mistake at a time.
+7
View File
@@ -0,0 +1,7 @@
---
title: "Let's Talk"
description: "Book a free strategy session with Will Estes to talk about getting your stories in front of the readers who need them."
type: "talk"
hero_subhead: "Let's find out how to get your stories in front of the readers who need them. Pick a time below."
cal_link: "willestes/booking"
---
+103
View File
@@ -0,0 +1,103 @@
# Adding a gift/lead-magnet partnership
The `/gift` funnel (opt-in page → PDF → nurture emails) can be customized per
reader-acquisition partner, each getting its own URL, PDF, and email content. A
partnership touches four repos. This doc is the map between them; each repo's own
README covers the details of its own step.
## The URL
`gift.reground.org/<slug>` already works for any `<slug>` with no infrastructure
changes. `reground-infrastructure`'s Caddy config redirects generically:
```
gift.reground.org {
redir https://reground.org/gift{uri} permanent
}
```
So `gift.reground.org/cheyenne``https://reground.org/gift/cheyenne` automatically —
you only ever need to add the Hugo page below.
## 1. reground-site (this repo) — the pages
`content/gift/` is a branch bundle (`_index.md`), so it can hold real child pages. Add
two leaf bundles per partner:
```
content/gift/<slug>/index.md # the opt-in page
content/gift/<slug>-thanks/index.md # the delivery/thanks page
```
Both use `type: "courses"` (rendered by `layouts/courses/single.html`, sharing the
`courses-body.html` partial with the generic `/gift` page). Front matter fields worth
setting per partner, copied from `content/gift/_index.md`'s existing example:
- `title`, `hero_subhead`, `cta_text`, `repeat_cta_heading` — the bridging copy
- `hero_image` — path under `/images/...`; see "cover image" below
- `eec_slug` — must match the slug used in `eec-courses` (step 4)
- `success_message` — markdown, should link to this partner's own `<slug>-thanks` page
The `<slug>-thanks` page just needs a title, a short thanks note, and a download link
pointing at wherever the PDF got uploaded (step 3).
### Cover image
Not automated. Render page 1 of the partner's built PDF and crop it to the cover block:
```sh
cd /home/will/opt/src/reground-gift-guide
pdftoppm -png -f 1 -l 1 -r 150 partners/<slug>.pdf cover
convert cover-1.png -crop 1275x885+0+0 +repage -resize 640x /home/will/opt/src/reground-site/static/images/gift-<slug>.png
rm cover-1.png
```
(The `885` crop height assumes the cover design hasn't changed — check the rendered PNG
first if it looks off.)
## 2. reground-gift-guide — the PDF
See that repo's own README. Summary: `cp template.md partners/<slug>.md`, hand-edit the
title and bridging paragraphs, `make partners/<slug>.pdf`.
## 3. reground-tools — hosting the PDF
See that repo's own README ("Downloads" section). Summary:
```sh
./download-upload.sh --site reground --slug <slug> partners/<slug>.pdf
```
Publishes to `https://downloads.reground.org/<slug>/<slug>.pdf` — that's the URL the
`<slug>-thanks` page (step 1) should link to.
## 4. eec-courses — delivery + nurture emails
Not engine work — pure content, same shape as the existing generic offer. Copy both
directories:
```
courses/three-steps-find-your-right-readers/ → courses/<eec_slug>/
courses/welcome-three-steps-find-your-right-readers/ → courses/welcome-<eec_slug>/
```
Each is a `course.yml` (metadata: `slug`, `name`, `from_email`, `ceiling_hours`, `steps`)
plus a `day-0.md` (YAML front matter: `subject`, `list_name`, `redirect_url`, `enroll:`
chaining to the welcome sequence's slug — followed by the email body in markdown).
Update:
- `course.yml`'s `slug` and `name` in both directories (must match `eec_slug` used in
step 1, and the welcome sequence's slug referenced by `enroll:`)
- `day-0.md`'s `subject`, body copy, and `redirect_url` (→ the `<slug>-thanks` page,
step 1) in the first course
- The welcome sequence's `day-0.md` copy, to reference the partner instead of the
generic offer
No changes needed in `will/eec` (the engine processes any slug with a valid
`course.yml` generically).
## Verification
1. `hugo --minify -D` in reground-site, confirm no build errors.
2. Local dev server: load `/gift/<slug>/`, confirm hero copy/image render and the form
points at the right `eec_slug`.
3. Confirm the uploaded PDF URL returns `200`.
4. Once eec-courses changes are deployed, do one real signup end-to-end and confirm the
delivery email, its PDF link, and its redirect all point at this partner's content.
+219
View File
@@ -0,0 +1,219 @@
# Social post drafts — /platform launch series
Ready-to-copy LinkedIn/X posts. Suggested cadence: ~2-3x/week, alternating
technical-guardrail posts (credibility with technical referral partners) and
human/outcome posts (credibility with small-business owners). Posting order
below follows that alternation. Each links to https://reground.org/platform/.
All claims are sourced from real commits/incidents in this workspace —
sourced-from note under each post for your own reference; drop those notes
before posting.
---
## 1. [Guardrail] The CI check that blocks a public bucket
I run object storage for five small businesses. Every one of those buckets
defaults to private — and there's a CI check that fails the build if a change
would make one public, unless there's a comment explaining exactly why.
Not a policy. Not a reminder in a wiki somewhere. A build that won't go green
until you either fix it or justify it in writing, right next to the code that
does it.
That's the difference between "we're careful about this" and "this system
will not let you be careless about this by accident."
More on how I run infra for five real businesses off one server →
reground.org/platform
*(sourced: reground-infrastructure .gitea/workflows/tf-lint.yml + scripts/check-public-buckets.sh, ACL gate on every linode_object_storage_bucket)*
---
## 2. [Human] Meet the five businesses
Right now, one hub server runs the hosting, email, and checkout for:
- An author's site
- A short-fiction writer's site
- A small-batch soap shop
- A literary press running open submissions
- A small press with multiple authors
Same infrastructure underneath. Completely different businesses on top. None
of them think about servers, deploy keys, or database backups — because
that's the whole point.
Curious what "one hub, five brands" actually looks like under the hood?
reground.org/platform
*(sourced: reground-site content/platform.md client description; kept anonymized to match how the live page describes them)*
---
## 3. [Guardrail] A `git push` can never send an email
Every content repo I run campaigns out of has the same rule baked into the
tooling: pushing to the repo can only ever produce a *draft* campaign. There
is no field, no flag, no config value that lets a routine content push
schedule or trigger a real send.
Sending for real is a separate, deliberate command. Always. On purpose.
If the worst that can happen from a typo'd commit is "an unsent draft
appeared," you've designed the failure mode correctly.
reground.org/platform
*(sourced: eec-campaigns internal/campaign/send.go — Send() only transitions draft/paused → running; sync never touches status)*
---
## 4. [Human] The soap maker doesn't know what a deploy key is
And that's exactly how it should be.
One of the five businesses I run infrastructure for makes small-batch
cold-processed soap. She doesn't need to know what a deploy key is, what
Postgres is, or how her checkout flow is hosted. She needs her site up, her
orders processed, and her email list intact.
The technical decisions underneath are mine to be accountable for, not hers
to worry about. That's the actual service.
reground.org/platform
---
## 5. [Guardrail] Scoped keys, not shared trust
Each of the five sites I run has its own deploy key, and each key can only
touch that one site's files — nothing else on the server. It's enforced by
the key itself, not by a policy someone has to remember to follow.
So if one site's repo were ever compromised, the blast radius is that one
site. Not the other four. Not the shared database. Not the mail server.
"Scoped" isn't a nice-to-have when you're running multiple businesses from
one place — it's the entire reason it's safe to do at all.
reground.org/platform
*(sourced: reground-infrastructure per-site forced-command rrsync deploy keys)*
---
## 6. [Human] Why a literary press's open-submissions inbox matters to me
One of the businesses I run infra for is a literary press that runs open
submissions — meaning strangers' work lands in their system every week,
sight unseen, from writers hoping someone reads it.
That's a real trust relationship on both ends: the press trusting their
infrastructure won't lose or leak someone's submission, and the writer
trusting the press with something they wrote. I don't get to see the
manuscripts. I just make sure the plumbing holds.
reground.org/platform
---
## 7. [Guardrail] Verify after every step, not just at the end
Moving a production deploy key off the root account isn't a one-commit
change if you're doing it carefully. I did it in three: move the key to a
dedicated account, verify the deploy still works. Fix the account's shell
from a locked-down nologin to bash so the key actually functions, verify
again. Remove the old root-level key, verify a final time.
Three commits, three separate verifications, because "I'm pretty sure that
worked" isn't good enough on a production mail server for five businesses.
reground.org/platform
*(sourced: eec commits 9fa7895 / 4497e4e / b8759d0)*
---
## 8. [Human] A real incident: what "unconfirmed" protected
One of the five businesses I run switched their list to double opt-in
partway through. Their earlier import batch — people who'd already agreed to
be on the list through a real prior relationship, just not yet
Listmonk-confirmed — could have been swept up and auto-confirmed to make the
numbers look clean.
Instead, those subscribers stayed exactly as `unconfirmed`, honestly
reflecting what was actually true about them, while the tooling was updated
to explain why. Boring is correct. Correct beats clean.
reground.org/platform
*(sourced: reground-tools commit f03c36a, 2026-07-22, Two Squirrels Press double opt-in)*
---
## 9. [Guardrail] Nothing that touches real subscriber data runs by default
Every ops script I use that could affect real subscribers — bulk name
backfills, unsubscribes, list edits — defaults to "show me what this would
do." Actually doing it requires a separate `--apply` flag, every time, no
exceptions.
The safe path is the path of least resistance. The dangerous path takes an
extra, deliberate step. That's not a coding style — it's a decision about
what a mistake is allowed to cost.
reground.org/platform
*(sourced: reground-tools backfill-names.sh / unsubscribe-subscriber.sh dry-run-by-default, --apply required)*
---
## 10. [Human] "Generate," not "fork"
Every new client site I bring on starts from a clean slate — zero inherited
git history, zero leftover config from whoever came before them. Not because
forking is technically hard, but because a new client's site shouldn't carry
another business's commit history, secrets, or half-finished experiments
into their own repo on day one.
Onboarding a new business should feel like a fresh start, because it is one.
reground.org/platform
*(sourced: campaigns-template / eec-courses-template — Gitea "Generate Repo" pattern)*
---
## 11. [Guardrail] One engine, five brands, one fix
When RFC 8058 one-click unsubscribe needed to ship, it went out to every
brand I run at the same time — because it lives in one shared engine, not
five separate forks that would each need the same fix applied by hand, five
times, with five chances to get it slightly wrong.
That's the actual payoff of "shared engine, per-brand config": a real fix is
one change, verified once, live everywhere.
reground.org/platform
*(sourced: eec + bookshop shared RFC 8058 one-click unsubscribe rollout)*
---
## 12. [Human] What used to be four vendors is now one afternoon
Before this setup, a small business selling books needed a checkout
processor, a separate storefront platform, a separate ebook delivery
service, and a separate email platform — four vendors, four logins, four
bills, none of which talk to each other on their own.
Now it's one shared service, deployed once, configured per brand, running
identically for every business that uses it. Same reliability, a fraction of
the moving parts.
reground.org/platform
*(sourced: bookshop — replaces Stripe + Shopify + BookFunnel + ConvertKit-style stacking)*
+10 -9
View File
@@ -10,19 +10,20 @@ disableKinds = ["taxonomy", "term"]
[params] [params]
env = "production" env = "production"
description = "ReGround helps fiction authors find the readers who were always meant to find them." description = "DevOps consulting, ghostwriting, and helping fiction authors find their right readers"
author = "Will Estes" author = "Will Estes"
eec_base_url = "https://eec.reground.org" eec_base_url = "https://eec.reground.org"
plausible_domain = "reground.org"
[[menu.main]] # [[menu.main]]
name = "Courses" # name = "Courses"
url = "/courses/" # url = "/courses/"
weight = 10 # weight = 10
[[menu.main]] # [[menu.main]]
name = "Contact" # name = "Contact"
url = "/contact/" # url = "/contact/"
weight = 20 # weight = 20
[[menu.footer]] [[menu.footer]]
name = "Contact" name = "Contact"
+6 -1
View File
@@ -15,13 +15,18 @@
{{ else }} {{ else }}
<link rel="stylesheet" href="{{ $css.RelPermalink }}"> <link rel="stylesheet" href="{{ $css.RelPermalink }}">
{{ end }} {{ end }}
{{ partial "head-analytics.html" . }}
</head> </head>
<body> <body>
<a class="skip-link" href="#main-content">Skip to content</a> <a class="skip-link" href="#main-content">Skip to content</a>
<header class="site"> <header class="site">
<div class="inner"> <div class="inner">
<a class="brand" href="/">{{ .Site.Title }}</a> {{ $rootMark := resources.Get "images/reground-root.svg" }}
<a class="brand" href="/">
{{ $rootMark.Content | safeHTML }}
<span>{{ .Site.Title }}</span>
</a>
<nav aria-label="Primary"> <nav aria-label="Primary">
{{ range .Site.Menus.main }} {{ range .Site.Menus.main }}
<a href="{{ .URL }}">{{ .Name }}</a> <a href="{{ .URL }}">{{ .Name }}</a>
+79
View File
@@ -0,0 +1,79 @@
{{ define "main" }}
<h1>{{ .Title }}</h1>
{{ with .Params.hero_subhead }}<p style="opacity:0.85;">{{ . }}</p>{{ end }}
{{ with .Params.proof_bullets }}
<ul class="course-bullets">
{{ range . }}<li>{{ . | markdownify }}</li>{{ end }}
</ul>
{{ end }}
{{ with .Params.fit_note }}<p style="opacity:0.7;font-size:0.9em;">{{ . }}</p>{{ end }}
<form id="devops-form" style="margin-top:2rem;">
<label for="name">Name</label>
<input type="text" id="name" name="name" required autocomplete="name">
<label for="email">Email</label>
<input type="email" id="email" name="email" required autocomplete="email">
<label for="company">Company / site</label>
<input type="text" id="company" name="company" autocomplete="organization">
<label for="message">What's going on right now?</label>
<textarea id="message" name="message" rows="6" required placeholder="Site keeps going down, hosting's gotten expensive, email's landing in spam, thinking about migrating off somewhere..."></textarea>
<div class="field-honeypot" aria-hidden="true">
<label for="website">Leave this field empty</label>
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
</div>
<button type="submit" class="btn">Tell me what's going on</button>
</form>
<p id="devops-status" role="status" style="margin-top:1rem;"></p>
<p style="margin-top:2rem;">Prefer to just get on a call? <a href="/talk/">Book a time →</a></p>
<script>
(function () {
var form = document.getElementById("devops-form");
var status = document.getElementById("devops-status");
form.addEventListener("submit", function (e) {
e.preventDefault();
var messageParts = [];
if (form.company.value) {
messageParts.push("Company/site: " + form.company.value);
messageParts.push("");
}
messageParts.push(form.message.value);
var payload = {
name: form.name.value,
email: form.email.value,
message: messageParts.join("\n"),
website: form.website.value
};
var submitBtn = form.querySelector("button[type=submit]");
submitBtn.disabled = true;
status.textContent = "Sending…";
fetch("{{ .Site.Params.eec_base_url }}/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
}).then(function (res) {
submitBtn.disabled = false;
if (res.ok) {
form.reset();
status.textContent = "Thanks — I'll get back to you soon.";
return;
}
return res.text().then(function (msg) {
status.textContent = msg || "Something went wrong sending that. Try again, or email hello@reground.org directly.";
});
}).catch(function () {
submitBtn.disabled = false;
status.textContent = "Something went wrong sending that. Try again, or email hello@reground.org directly.";
});
});
})();
</script>
{{ end }}
+3
View File
@@ -0,0 +1,3 @@
{{ define "main" }}
{{ partial "courses-body.html" . }}
{{ end }}
+1 -50
View File
@@ -1,52 +1,3 @@
{{ define "main" }} {{ define "main" }}
<h1 style="font-size:2rem; margin-bottom:0.5rem;">{{ .Title }}</h1> {{ partial "courses-body.html" . }}
{{ with .Params.pitch }}<div style="font-size:1.05rem; opacity:0.9;">{{ . | markdownify }}</div>{{ end }}
{{ .Content }}
<form id="enroll-form" style="margin-top:2rem; max-width:24rem;">
<label for="email">Email</label>
<input type="email" id="email" name="email" required autocomplete="email" placeholder="you@example.com">
<div class="field-honeypot" aria-hidden="true">
<label for="website">Leave this field empty</label>
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
</div>
<button type="submit" class="btn">Send me day one</button>
</form>
<p id="enroll-status" role="status" style="margin-top:1rem;"></p>
<script>
(function () {
var form = document.getElementById("enroll-form");
var status = document.getElementById("enroll-status");
var slug = {{ .Params.eec_slug }};
form.addEventListener("submit", function (e) {
e.preventDefault();
if (form.website.value) return; // honeypot
var submitBtn = form.querySelector("button[type=submit]");
submitBtn.disabled = true;
status.textContent = "Signing you up…";
fetch("{{ .Site.Params.eec_base_url }}/courses/" + slug + "/enroll", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: form.email.value })
}).then(function (res) {
if (res.ok) {
form.reset();
form.hidden = true;
status.textContent = "Check your inbox — let us know you're ready and we'll get started.";
return;
}
return res.text().then(function (msg) {
submitBtn.disabled = false;
status.textContent = msg || "Something went wrong. Try again in a moment.";
});
}).catch(function () {
submitBtn.disabled = false;
status.textContent = "Something went wrong. Try again in a moment.";
});
});
})();
</script>
{{ end }} {{ end }}
+2 -8
View File
@@ -1,10 +1,4 @@
{{ define "main" }} {{ define "main" }}
<h1 style="font-size:2.25rem; margin-bottom:0.5rem;">{{ .Params.hero_heading }}</h1> <h1>{{ .Title }}</h1>
<p style="font-size:1.15rem; opacity:0.85; margin-top:0;">{{ .Params.hero_subheading }}</p> {{ .Content }}
<div style="margin-top:2rem;">{{ .Params.intro | markdownify }}</div>
{{ with .Params.cta_text }}
<p style="margin-top:2rem;">
<a class="btn" href="{{ $.Params.cta_link }}">{{ . }}</a>
</p>
{{ end }}
{{ end }} {{ end }}
+19
View File
@@ -0,0 +1,19 @@
<div id="cal-inline" class="cal-embed"></div>
<script type="text/javascript">
(function (C, A, L) { let p = function (a, ar) { a.q.push(ar); }; let d = C.document; C.Cal = C.Cal || function () { let cal = C.Cal; let ar = arguments; if (!cal.loaded) { cal.ns = {}; cal.q = cal.q || []; d.head.appendChild(d.createElement("script")).src = A; cal.loaded = true; } if (ar[0] === L) { const api = function () { p(api, arguments); }; const namespace = ar[1]; api.q = api.q || []; if (typeof namespace === "string") { cal.ns[namespace] = cal.ns[namespace] || api; p(cal.ns[namespace], ar); p(cal, ["initNamespace", namespace]); } else p(cal, ar); return; } p(cal, ar); }; })(window, "https://app.cal.com/embed/embed.js", "init");
Cal("init", "talk", { origin: "https://cal.com" });
Cal.ns.talk("inline", {
elementOrSelector: "#cal-inline",
calLink: "{{ . }}",
config: { layout: "month_view" },
});
Cal.ns.talk("ui", {
theme: "dark",
cssVarsPerTheme: {
light: { "cal-brand": "#007f73" },
dark: { "cal-brand": "#2dd4c4" },
},
hideEventTypeDetails: false,
layout: "month_view",
});
</script>
+59
View File
@@ -0,0 +1,59 @@
{{ $id := .id }}
{{ $page := .page }}
<form id="enroll-form-{{ $id }}" class="enroll-form">
<label for="first-name-{{ $id }}">First name</label>
<input type="text" id="first-name-{{ $id }}" name="first_name" required autocomplete="given-name">
<label for="last-name-{{ $id }}">Last name (optional)</label>
<input type="text" id="last-name-{{ $id }}" name="last_name" autocomplete="family-name">
<label for="email-{{ $id }}">Email</label>
<input type="email" id="email-{{ $id }}" name="email" required autocomplete="email" placeholder="you@example.com">
<div class="field-honeypot" aria-hidden="true">
<label for="website-{{ $id }}">Leave this field empty</label>
<input type="text" id="website-{{ $id }}" name="website" tabindex="-1" autocomplete="off">
</div>
<button type="submit" class="btn">{{ $page.Params.cta_text | default "Send me day one" }}</button>
</form>
<p id="enroll-status-{{ $id }}" role="status" class="enroll-status"></p>
<script>
(function () {
var form = document.getElementById("enroll-form-{{ $id }}");
var status = document.getElementById("enroll-status-{{ $id }}");
var slug = {{ $page.Params.eec_slug }};
var successMessage = {{ ($page.Params.success_message | default "Check your inbox — let us know you're ready and we'll get started.") | markdownify }};
form.addEventListener("submit", function (e) {
e.preventDefault();
if (form.website.value) return; // honeypot
var submitBtn = form.querySelector("button[type=submit]");
submitBtn.disabled = true;
status.textContent = "Signing you up…";
fetch("{{ $page.Site.Params.eec_base_url }}/courses/" + slug + "/enroll", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: form.email.value,
first_name: form.first_name.value,
last_name: form.last_name.value
})
}).then(function (res) {
if (res.ok) {
form.reset();
form.hidden = true;
status.innerHTML = successMessage;
return;
}
return res.text().then(function (msg) {
submitBtn.disabled = false;
status.textContent = msg || "Something went wrong. Try again in a moment.";
});
}).catch(function () {
submitBtn.disabled = false;
status.textContent = "Something went wrong. Try again in a moment.";
});
});
})();
</script>
+41
View File
@@ -0,0 +1,41 @@
<section class="course-hero">
<h1>{{ .Title }}</h1>
{{ with .Params.hero_subhead }}<p class="course-subhead">{{ . }}</p>{{ end }}
{{ with .Params.hero_image }}<img class="course-hero-image" src="{{ . }}" alt="">{{ end }}
{{ partial "course-enroll-form.html" (dict "id" "top" "page" .) }}
{{ with .Params.social_proof }}<p class="course-social-proof">{{ . }}</p>{{ end }}
</section>
{{ if or .Params.credibility_heading .Params.credibility_bullets }}
<section class="course-credibility">
{{ with .Params.credibility_heading }}<h2>{{ . }}</h2>{{ end }}
{{ with .Params.credibility_bullets }}
<ul class="course-bullets">
{{ range . }}<li>{{ . | markdownify }}</li>{{ end }}
</ul>
{{ end }}
</section>
{{ end }}
{{ if or .Params.curriculum_heading .Params.curriculum_items }}
<section class="course-curriculum">
{{ with .Params.curriculum_heading }}<h2>{{ . }}</h2>{{ end }}
{{ with .Params.curriculum_intro }}<p>{{ . }}</p>{{ end }}
{{ with .Params.curriculum_items }}
<ol class="course-curriculum-list">
{{ range . }}
<li>{{ with .label }}<span class="course-day-label">{{ . }}</span> {{ end }}{{ .text | markdownify }}</li>
{{ end }}
</ol>
{{ end }}
</section>
{{ end }}
{{ .Content }}
<section class="course-repeat-cta">
{{ with .Params.repeat_cta_heading }}<h2>{{ . }}</h2>{{ end }}
{{ partial "course-enroll-form.html" (dict "id" "bottom" "page" .) }}
</section>
+5
View File
@@ -0,0 +1,5 @@
{{ if hugo.IsProduction }}
{{ with .Site.Params.plausible_domain }}
<script defer data-domain="{{ . }}" src="https://plausible.reground.org/js/script.js"></script>
{{ end }}
{{ end }}
+8
View File
@@ -0,0 +1,8 @@
{{ define "main" }}
<section class="course-hero">
<h1>{{ .Title }}</h1>
{{ with .Params.hero_subhead }}<p class="course-subhead">{{ . }}</p>{{ end }}
</section>
{{ with .Params.cal_link }}{{ partial "cal-embed.html" . }}{{ end }}
{{ end }}
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB