Compare commits

...

8 Commits

Author SHA1 Message Date
will d214420046 Write authors page introducing Cate Conners and Michael Ferrara
deploy / deploy (push) Successful in 6s
Contrasts the two pen names' voices and links out to each author's
own site, giving the press a real "who's behind this" page.
2026-08-11 10:52:00 -04:00
will 3b20b91cb3 Publish a stable, non-fingerprinted stylesheet copy for bookshop
deploy / deploy (push) Successful in 6s
Mirrors thistleandmoth-site's baseof.html: the fingerprinted stylesheet
link changes hash on every deploy, so it can't be hardcoded as
BRAND_SITE_STYLESHEET_URL for bookshop's pages to link. Publishes the same
source at a stable /css/main.min.css URL alongside it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 17:59:53 -04:00
will 6018a1f32d Switch deploy to CI-build + scoped rsync instead of shared-runner bind-mount
deploy / deploy (push) Successful in 5s
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 in reground-infrastructure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 12:30:23 -04:00
will 9dd53fdb96 Collect first/last name on the mailing-list subscribe form
deploy / deploy (push) Successful in 4s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 19:47:32 -04:00
will 911dacb1b2 Increase header logo size from 44px to 64px
deploy / deploy (push) Successful in 4s
Feedback that the banner logo was too small; the display size is
set entirely by CSS (source image and Hugo's resize pipeline both
have plenty of resolution headroom above 64px).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 17:45:23 -04:00
will 4d0b976c18 Add a submissions on/off toggle with a mailing-list signup fallback
deploy / deploy (push) Successful in 4s
accepting_submissions: false in the submissions page frontmatter now
swaps the manuscript form for a closed notice and an email signup that
posts to the eec service's new /subscribe endpoint, so visitors can
still join the mailing list while submissions are paused.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 20:16:15 -04:00
will db7a128c74 Wire up real Stripe sandbox test key and example book price ID
deploy / deploy (push) Successful in 11s
From the Two Squirrels Press Stripe sandbox account created and walked
through Managed Payments setup for. Both values are test-mode / safe
to commit; swap to live-mode equivalents at go-live.
2026-07-15 16:31:06 -04:00
will 41475f4a5a Add Stripe embedded checkout to book pages
Replaces the gumroad_url stub with an on-site embedded checkout (buy
button -> POST to the bookshop backend -> mounted Stripe iframe),
following the existing contact/submissions form's fetch/status-message
pattern. Adds thank-you and resend-link pages, an example book template
(draft), and updates the privacy policy and terms to honestly disclose
Stripe as a third-party payment processor.
2026-07-14 18:35:50 -04:00
14 changed files with 274 additions and 19 deletions
+21 -3
View File
@@ -7,10 +7,28 @@ jobs:
runs-on: ubuntu-latest
container:
image: hugomods/hugo:exts
volumes:
- /var/www/twosquirrelspress.com:/var/www/twosquirrelspress.com
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- run: hugo --minify -d /var/www/twosquirrelspress.com
- 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@twosquirrelspress.com:
env:
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
+1 -1
View File
@@ -94,7 +94,7 @@ header.site .brand {
text-decoration: none;
}
header.site .brand img {
height: 44px;
height: 64px;
width: auto;
display: block;
}
+7 -1
View File
@@ -3,4 +3,10 @@ title: "Authors"
description: "Authors published by Two Squirrels Press."
---
Coming soon.
Two Squirrels Press is run by two people who write very different books.
**Cate Conners** writes Fantasy Michigan — old-growth forests, lakes that keep their own counsel, the kind of magic that's been waiting under the water a long time before anyone noticed. [Read her work →](https://cateconners.com)
**Michael Ferrara** writes short fiction about the ordinary things that turn out to be enormous — AIs, wisecracking seagulls, songs that need to be heard. [Read his work →](https://michaelferrara.org)
Different genres, different voices, same instinct: character first, and trust the reader to meet you halfway.
+15
View File
@@ -0,0 +1,15 @@
---
title: "Example Book Title"
author: "Author Name"
cover_image: "https://downloads.twosquirrelspress.com/covers/example-book.jpg"
description: "One-line jacket copy for listing pages."
stripe_price_id: "price_1TtYspQEDYBEXYYF8YINz8y3"
price_display: "$6.99"
formats: ["EPUB", "PDF"]
draft: true
---
Full jacket-copy / long description goes here. Duplicate this file for
each real book, fill in the real Stripe Price ID (create the Product in
the Stripe Dashboard first, with `tsp_book=true` and `file_key=...` set
in the Product's metadata), and remove `draft: true` to publish it.
+8
View File
@@ -0,0 +1,8 @@
---
title: "Resend my download link"
description: "Lost your download link? Get a new one sent to your email."
layout: "book-resend"
---
Enter the email address you used when you bought a book, and if we find
a matching order we'll send a fresh download link to that address.
+10
View File
@@ -0,0 +1,10 @@
---
title: "Thanks for your order"
description: "Your ebook purchase is complete."
---
Thanks for your purchase! Check your email in a moment for a link to
download your book.
If it doesn't show up within a few minutes, check your spam folder, or
use the [resend form](/books/resend/) to get a fresh link sent to you.
+25 -10
View File
@@ -14,34 +14,49 @@ use of twosquirrelspress.com, and what happens to it.
- Your name, email address, manuscript file, author bio, a description of
your submission, the title or call it's for, and a headshot if you
provide one, when you submit the submissions form.
- Your email address and order details, when you buy a book. Your payment
details (card number and so on) go directly to Stripe, our payment
processor — they never touch Two Squirrels Press's own servers. Two
Squirrels Press only ever receives your email address, which book you
bought, and confirmation that the payment succeeded.
That's it — this site doesn't run a newsletter, doesn't sell anything
directly, and doesn't use third-party analytics, ad trackers, or tracking
pixels.
This site doesn't run a newsletter, and doesn't use third-party analytics,
ad trackers, or tracking pixels. Checkout with Stripe is the one
third-party service in the loop, and it's there because payment processing
and tax compliance genuinely can't be handled responsibly by a small press
running its own servers.
## Why Two Squirrels Press collects it
- To respond to messages sent through the contact form.
- To evaluate submissions sent through the submissions form.
- To fulfill book purchases (send your download link) and handle any
refund requests.
## What Two Squirrels Press doesn't do
- Two Squirrels Press doesn't sell or rent your information to anyone.
- Two Squirrels Press doesn't share it with third parties except the
infrastructure providers necessary to run this site and deliver that
email — none of whom get to use it for their own purposes.
infrastructure providers necessary to run this site, deliver that email,
and process a payment — currently: Stripe, for payment processing — none
of whom get to use it for their own purposes.
## Your control over it
- **Deletion**: reach out through [the contact form](/contact/) and Two
Squirrels Press will delete your information from these systems.
Squirrels Press will delete your information from these systems. (Stripe
keeps its own transaction records as required by law and its own
policies — that retention isn't something Two Squirrels Press controls.)
## Security
Your information is stored on infrastructure Two Squirrels Press controls
directly, not handed off to third-party SaaS platforms. That doesn't make
it invulnerable — no system is — but it does mean there's exactly one place
your data lives, and one party accountable for it.
Most of your information is stored on infrastructure Two Squirrels Press
controls directly. The one exception is payment processing: Stripe, acting
as merchant of record, handles your card details and tax compliance
directly, and Two Squirrels Press never sees or stores your card number.
That doesn't make any of this invulnerable — no system is — but it does
mean there's exactly one clearly accountable party for each piece of your
data.
## Changes
+1
View File
@@ -1,6 +1,7 @@
---
title: "Submissions"
description: "How to submit work to Two Squirrels Press."
accepting_submissions: false
---
We're always glad to look at character-driven speculative fiction and poetry. Markdown is our preferred format, but we'll happily take a `.docx`, `.odt`, or most other common document formats.
+14 -2
View File
@@ -5,14 +5,26 @@ description: "Terms for using twosquirrelspress.com."
_Last updated: July 2026_
These terms cover using twosquirrelspress.com and submitting the contact
form.
These terms cover using twosquirrelspress.com, submitting the contact or
submissions form, and buying a book.
## Using this site
This site is provided as-is, for informational purposes about Two Squirrels
Press, its books, and its authors.
## Buying a book
Payments are processed by Stripe, acting as merchant of record — Stripe
handles your card details, sales tax, and VAT directly. Two Squirrels
Press never sees or stores your card number.
Ebooks you buy are for your own personal use — please don't redistribute
or resell the files. Because these are digital downloads, all sales are
final except where required by law; if something's gone wrong with your
purchase (wrong file, failed download, etc.) [get in touch](/contact/)
and we'll sort it out.
## No warranty
This site and its content are provided without warranties of any kind,
+5
View File
@@ -13,6 +13,11 @@ disableKinds = ["taxonomy", "term"]
author = "Will Estes"
description = "Two Squirrels Press — character-driven speculative fiction and poetry."
eec_base_url = "https://eec.twosquirrelspress.com"
bookshop_base_url = "https://shop.twosquirrelspress.com"
# Publishable key is safe to commit (it's meant to be public). This is
# the Two Squirrels Press sandbox/test-mode key — swap to the live-mode
# publishable key as part of go-live (see the plan's verification steps).
stripe_publishable_key = "pk_test_51TtYRgQEDYBEXYYF870hcDq58pl5MgT9evlYdgVE19VVXzNGA1DnVUX0lDTFpKJGShUHDT6NlIhiqz7u6YHPvX6x00rKIk2T3v"
[[menu.main]]
name = "Books"
+12 -2
View File
@@ -12,8 +12,18 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Merriweather:wght@400;700&family=Lato:wght@400;600;700&family=Dancing+Script&display=swap">
{{ $css := resources.Get "css/main.css" }}
{{ if hugo.IsProduction }}
{{ $css = $css | minify | fingerprint }}
<link rel="stylesheet" href="{{ $css.RelPermalink }}" integrity="{{ $css.Data.Integrity }}">
{{ $cssFingerprinted := $css | minify | fingerprint }}
<link rel="stylesheet" href="{{ $cssFingerprinted.RelPermalink }}" integrity="{{ $cssFingerprinted.Data.Integrity }}">
{{/* Also publish the same source at a second, stable, non-fingerprinted
URL — the fingerprinted one above changes hash on every deploy, so
it's unusable as a link target for an external same-origin
consumer (bookshop's pages, via BRAND_SITE_STYLESHEET_URL) that
can't track that hash. Referencing .RelPermalink is what makes
Hugo actually publish this resource; nothing else in this
template links it. Stray non-whitespace text directly in <head>
gets moved into <body> by the HTML parser, so this has to sit
inside a comment. */}}
<!-- stable stylesheet copy: {{ ($css | minify).RelPermalink }} -->
{{ else }}
<link rel="stylesheet" href="{{ $css.RelPermalink }}">
{{ end }}
+43
View File
@@ -0,0 +1,43 @@
{{ define "main" }}
<h1>{{ .Title }}</h1>
{{ .Content }}
<form id="resend-form" style="margin-top:2rem;">
<label for="email">Email</label>
<input type="email" id="email" name="email" required autocomplete="email">
<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">Resend my link</button>
</form>
<p id="resend-status" role="status" style="margin-top:1rem;"></p>
<script>
(function () {
var form = document.getElementById("resend-form");
var status = document.getElementById("resend-status");
form.addEventListener("submit", function (e) {
e.preventDefault();
var payload = { email: form.email.value, website: form.website.value };
var submitBtn = form.querySelector("button[type=submit]");
submitBtn.disabled = true;
status.textContent = "Sending…";
fetch("{{ .Site.Params.bookshop_base_url }}/books/resend", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
}).then(function () {
form.reset();
status.textContent = "If that email has a purchase on file, a new link is on its way.";
}).catch(function () {
status.textContent = "Something went wrong. Try again, or email hello@twosquirrelspress.com directly.";
}).finally(function () {
submitBtn.disabled = false;
});
});
})();
</script>
{{ end }}
+52
View File
@@ -0,0 +1,52 @@
{{ define "main" }}
<h1>{{ .Title }}</h1>
{{ with .Params.cover_image }}
<img src="{{ . }}" alt="Cover of {{ $.Title }}" style="max-width:300px;">
{{ end }}
{{ with .Params.author }}
<p><span class="author-name">{{ . }}</span></p>
{{ end }}
{{ .Content }}
{{ with .Params.stripe_price_id }}
<div id="buy-btn-wrap">
{{ with $.Params.price_display }}<p><strong>{{ . }}</strong></p>{{ end }}
<button type="button" id="buy-btn" class="btn">Buy the ebook</button>
</div>
<div id="checkout-container"></div>
<p id="buy-status" role="status" style="margin-top:1rem;"></p>
<script src="https://js.stripe.com/v3/"></script>
<script>
(function () {
var stripe = Stripe("{{ $.Site.Params.stripe_publishable_key }}");
var priceId = "{{ . }}";
var buyBtn = document.getElementById("buy-btn");
var buyWrap = document.getElementById("buy-btn-wrap");
var status = document.getElementById("buy-status");
buyBtn.addEventListener("click", function () {
buyBtn.disabled = true;
status.textContent = "Starting checkout…";
fetch("{{ $.Site.Params.bookshop_base_url }}/books/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ price_id: priceId })
}).then(function (res) {
if (!res.ok) { throw new Error("checkout session request failed"); }
return res.json();
}).then(function (data) {
status.textContent = "";
buyWrap.hidden = true;
return stripe.initEmbeddedCheckout({ clientSecret: data.clientSecret });
}).then(function (checkout) {
checkout.mount("#checkout-container");
}).catch(function () {
buyBtn.disabled = false;
buyWrap.hidden = false;
status.textContent = "Something went wrong starting checkout. Try again, or email hello@twosquirrelspress.com.";
});
});
})();
</script>
{{ end }}
{{ end }}
+60
View File
@@ -2,6 +2,7 @@
<h1>{{ .Title }}</h1>
{{ .Content }}
{{ if ne .Params.accepting_submissions false }}
<form id="submissions-form" style="margin-top:2rem;">
<label for="name">Name</label>
<input type="text" id="name" name="name" required autocomplete="name">
@@ -65,4 +66,63 @@
});
})();
</script>
{{ else }}
<p style="margin-top:2rem;">We're not currently accepting submissions, but you can join our mailing list to hear when that changes.</p>
<form id="subscribe-form" style="margin-top:1.5rem;">
<label for="subscribe-first-name">First name</label>
<input type="text" id="subscribe-first-name" name="first_name" required autocomplete="given-name">
<label for="subscribe-last-name">Last name (optional)</label>
<input type="text" id="subscribe-last-name" name="last_name" autocomplete="family-name">
<label for="email">Email</label>
<input type="email" id="email" name="email" required autocomplete="email">
<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">Join the mailing list</button>
</form>
<p id="subscribe-status" role="status" style="margin-top:1rem;"></p>
<script>
(function () {
var form = document.getElementById("subscribe-form");
var status = document.getElementById("subscribe-status");
form.addEventListener("submit", function (e) {
e.preventDefault();
var payload = {
email: form.email.value,
first_name: form.first_name.value,
last_name: form.last_name.value,
website: form.website.value
};
var submitBtn = form.querySelector("button[type=submit]");
submitBtn.disabled = true;
status.textContent = "Signing you up…";
fetch("{{ .Site.Params.eec_base_url }}/subscribe", {
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 — we'll let you know.";
return;
}
return res.text().then(function (msg) {
status.textContent = msg || "Something went wrong signing you up. Try again, or email hello@twosquirrelspress.com directly.";
});
}).catch(function () {
submitBtn.disabled = false;
status.textContent = "Something went wrong signing you up. Try again, or email hello@twosquirrelspress.com directly.";
});
});
})();
</script>
{{ end }}
{{ end }}