Compare commits
25 Commits
3e258930a8
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 344dc4719b | |||
| 1d7635a1ce | |||
| bd784619bc | |||
| 0bdf2b982b | |||
| e967ddccbc | |||
| 00e8e56113 | |||
| 3a40244d7f | |||
| 2f4c7eaeb6 | |||
| 09e5f40b31 | |||
| dbebca1e45 | |||
| dbeb634110 | |||
| 143ddfda92 | |||
| f9d6d775a7 | |||
| 6b7edc5b56 | |||
| 7cbd68f704 | |||
| c02512f688 | |||
| af69f463f5 | |||
| 7da665ffaf | |||
| 2a5b1edf19 | |||
| 3d197e5c9a | |||
| 9953f4ad27 | |||
| aa08908493 | |||
| 0a8631a0f2 | |||
| e3a1c94597 | |||
| e1ba854abd |
@@ -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 +0,0 @@
|
|||||||
hugo 0.124.1
|
|
||||||
@@ -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;
|
||||||
@@ -243,6 +251,15 @@ input:focus, textarea:focus {
|
|||||||
color: var(--fg-muted);
|
color: var(--fg-muted);
|
||||||
margin-top: 0;
|
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 {
|
.course-value-prop {
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
margin: 1.5rem 0;
|
margin: 1.5rem 0;
|
||||||
@@ -303,3 +320,39 @@ input:focus, textarea:focus {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 3rem;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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/"
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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/).
|
||||||
@@ -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."
|
||||||
|
---
|
||||||
@@ -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)
|
||||||
@@ -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)."
|
||||||
|
---
|
||||||
@@ -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)
|
||||||
@@ -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)."
|
||||||
|
---
|
||||||
@@ -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.
|
||||||
@@ -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"
|
||||||
|
---
|
||||||
@@ -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.
|
||||||
@@ -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,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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 }}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{{ define "main" }}
|
||||||
|
{{ partial "courses-body.html" . }}
|
||||||
|
{{ end }}
|
||||||
@@ -1,42 +1,3 @@
|
|||||||
{{ define "main" }}
|
{{ define "main" }}
|
||||||
<section class="course-hero">
|
{{ partial "courses-body.html" . }}
|
||||||
<h1>{{ .Title }}</h1>
|
|
||||||
{{ with .Params.hero_subhead }}<p class="course-subhead">{{ . }}</p>{{ 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>
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
+2
-8
@@ -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 }}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
{{ $id := .id }}
|
{{ $id := .id }}
|
||||||
{{ $page := .page }}
|
{{ $page := .page }}
|
||||||
<form id="enroll-form-{{ $id }}" class="enroll-form">
|
<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>
|
<label for="email-{{ $id }}">Email</label>
|
||||||
<input type="email" id="email-{{ $id }}" name="email" required autocomplete="email" placeholder="you@example.com">
|
<input type="email" id="email-{{ $id }}" name="email" required autocomplete="email" placeholder="you@example.com">
|
||||||
|
|
||||||
@@ -18,6 +24,7 @@
|
|||||||
var form = document.getElementById("enroll-form-{{ $id }}");
|
var form = document.getElementById("enroll-form-{{ $id }}");
|
||||||
var status = document.getElementById("enroll-status-{{ $id }}");
|
var status = document.getElementById("enroll-status-{{ $id }}");
|
||||||
var slug = {{ $page.Params.eec_slug }};
|
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) {
|
form.addEventListener("submit", function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (form.website.value) return; // honeypot
|
if (form.website.value) return; // honeypot
|
||||||
@@ -27,12 +34,16 @@
|
|||||||
fetch("{{ $page.Site.Params.eec_base_url }}/courses/" + slug + "/enroll", {
|
fetch("{{ $page.Site.Params.eec_base_url }}/courses/" + slug + "/enroll", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ email: form.email.value })
|
body: JSON.stringify({
|
||||||
|
email: form.email.value,
|
||||||
|
first_name: form.first_name.value,
|
||||||
|
last_name: form.last_name.value
|
||||||
|
})
|
||||||
}).then(function (res) {
|
}).then(function (res) {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
form.reset();
|
form.reset();
|
||||||
form.hidden = true;
|
form.hidden = true;
|
||||||
status.textContent = "Check your inbox — let us know you're ready and we'll get started.";
|
status.innerHTML = successMessage;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return res.text().then(function (msg) {
|
return res.text().then(function (msg) {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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 }}
|
||||||
@@ -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 |
Reference in New Issue
Block a user