Add nav/footer, contact form, privacy/terms drafts, first course landing page
deploy / deploy (push) Successful in 4s

- baseof.html: proper header/nav/footer, CSS custom properties for
  light/dark, button and form component styles — no external theme
  dependency, avoiding the submodule/version churn from the earlier
  PaperMod attempt.
- Contact page: real form (honeypot spam protection) posting to the eec
  service's new /contact endpoint, replacing the plain mailto CTA.
- Privacy Policy and Terms drafts — grounded in what's actually true here
  (self-hosted mail/course infra, no third-party trackers), flagged as
  drafts pending review.
- courses/ section with a generic single template driven by front matter
  (eec_slug, pitch) and the first course, Find Your Right Readers —
  landing page only; the actual course content isn't authored in
  eec-courses yet, so enrolling will 404 until that's written.
- Fixed hugo.toml baseURL (was www.reground.org, but Caddy's canonical
  host is the bare domain — www redirects to it, not the other way).
This commit is contained in:
2026-07-08 16:41:47 -04:00
parent cd6ba1415c
commit 4445da18bb
11 changed files with 425 additions and 14 deletions
+1 -1
View File
@@ -9,5 +9,5 @@ intro: |
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: "mailto:hello@reground.org"
cta_link: "/contact/"
---
+5
View File
@@ -0,0 +1,5 @@
---
title: "Contact"
layout: "contact"
description: "Get in touch with ReGround."
---
+6
View File
@@ -0,0 +1,6 @@
---
title: "Courses"
description: "Free email courses from ReGround."
---
Short, free email courses — sign up and they land in your inbox at whatever pace suits you.
@@ -0,0 +1,11 @@
---
title: "Find Your Right Readers"
description: "A free email course for fiction authors on finding the readers who were always meant to find their story."
eec_slug: "find-your-right-readers"
pitch: |
Most advice about "finding readers" is really just advice about marketing —
ads, algorithms, funnels. This is different. Over a few short emails, at
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
instead of shouting into the void.
---
+64
View File
@@ -0,0 +1,64 @@
---
title: "Privacy Policy"
description: "How ReGround collects, uses, and protects your information."
---
_Last updated: July 2026_
**This is a draft.** It hasn't been reviewed by a lawyer. Review before treating it as final.
This policy explains what information ReGround ("I," "me") collects through
reground.org and its related email courses, and what happens to it.
## What I collect
- **Contact form**: the name, email address, and message you submit.
- **Email courses**: the email address you sign up with, and which step of
a course you've reached (so the course can be delivered at your pace).
- **Newsletter list**: if you opt in at the end of a course (or by another
explicit signup), your email address and name, if given.
I don't use third-party analytics, ad trackers, or tracking pixels on this
site. Email delivery, the course system, and the mailing list all run on
infrastructure I operate myself — nothing is handed to a third-party
marketing platform.
## Why I collect it
- To respond to messages sent through the contact form.
- To deliver the email course(s) you sign up for.
- To send periodic updates to people who've explicitly opted in — never
as a side effect of using the contact form or taking a course.
## What I don't do
- I don't sell or rent your information to anyone.
- I don't share it with third parties except the infrastructure providers
necessary to run this site and send email (e.g. the hosting provider and
its network) — none of whom get to use it for their own purposes.
## Your control over it
- **Unsubscribe**: every newsletter email includes an unsubscribe link.
Clicking it removes you from that list immediately.
- **Email courses**: courses are self-paced with no ongoing subscription —
once you've received the emails, there's nothing further to opt out of
unless you also joined the newsletter list.
- **Deletion**: email me at [hello@reground.org](mailto:hello@reground.org)
and I'll delete your information from these systems.
## Security
Your information is stored on infrastructure I control 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 person accountable for it.
## Changes
If this policy changes in a way that matters, I'll update the date at the
top of this page.
## Questions
[hello@reground.org](mailto:hello@reground.org)
+50
View File
@@ -0,0 +1,50 @@
---
title: "Terms of Use"
description: "Terms for using reground.org and its email courses."
---
_Last updated: July 2026_
**This is a draft.** It hasn't been reviewed by a lawyer. Review before treating it as final.
These terms cover using reground.org, its free email courses, and
submitting the contact form. They don't cover the terms of any paid
coaching, ghostwriting, or consulting engagement — those are agreed
separately, in writing, before any paid work begins.
## Using this site
This site and its free email courses are provided as-is, for informational
and educational purposes. Nothing on it is professional advice specific to
your situation — it's general guidance, and you're responsible for how you
apply it.
## Email courses
Course content is mine, written for general educational use by whoever
signs up. You're welcome to apply what you learn to your own work; you're
not entitled to redistribute, resell, or republish the course content
itself without asking first.
## No warranty
This site and its content are provided without warranties of any kind,
express or implied. I make a good-faith effort to keep things accurate and
working, but I don't guarantee any particular outcome from taking a free
course or reading anything published here.
## Liability
To the extent the law allows, I'm not liable for damages arising from your
use of this site or its content. If you're looking for guarantees around a
specific outcome, that belongs in a paid engagement's written agreement,
not these terms.
## Changes
I may update these terms as the site changes. Material changes will update
the date at the top of this page.
## Questions
[hello@reground.org](mailto:hello@reground.org)
+26 -4
View File
@@ -1,4 +1,4 @@
baseURL = "https://www.reground.org/"
baseURL = "https://reground.org/"
locale = "en-US"
title = "ReGround"
@@ -12,7 +12,29 @@ disableKinds = ["taxonomy", "term"]
env = "production"
description = "ReGround helps fiction authors find the readers who were always meant to find them."
author = "Will Estes"
eec_base_url = "https://eec.reground.org"
# Menu is intentionally minimal for now — add Blog/About/Contact
# entries here as those sections come online.
[menu]
[[menu.main]]
name = "Courses"
url = "/courses/"
weight = 10
[[menu.main]]
name = "Contact"
url = "/contact/"
weight = 20
[[menu.footer]]
name = "Contact"
url = "/contact/"
weight = 10
[[menu.footer]]
name = "Privacy"
url = "/privacy/"
weight = 20
[[menu.footer]]
name = "Terms"
url = "/terms/"
weight = 30
+160 -8
View File
@@ -6,29 +6,181 @@
<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} · {{ .Site.Title }}{{ end }}</title>
<meta name="description" content="{{ .Site.Params.description }}">
<style>
:root { color-scheme: light dark; }
:root {
color-scheme: light dark;
--bg: #fafafa;
--bg-raised: #ffffff;
--fg: #1a1a1a;
--fg-muted: #5a5a5a;
--border: #e2e2e2;
--accent: #1a5fb4;
--accent-fg: #ffffff;
--radius: 0.5rem;
--max-width: 42rem;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #14161a;
--bg-raised: #1c1f24;
--fg: #e8e8e8;
--fg-muted: #9a9fa6;
--border: #2c3038;
--accent: #8ab4f8;
--accent-fg: #0f1115;
}
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
background: #fafafa;
color: #1a1a1a;
background: var(--bg);
color: var(--fg);
display: flex;
flex-direction: column;
min-height: 100vh;
}
@media (prefers-color-scheme: dark) {
body { background: #14161a; color: #e8e8e8; }
a { color: #8ab4f8; }
a { color: var(--accent); }
h1, h2, h3 { line-height: 1.25; }
header.site {
border-bottom: 1px solid var(--border);
}
header.site .inner {
max-width: var(--max-width);
margin: 0 auto;
padding: 1.25rem 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
header.site .brand {
font-weight: 700;
font-size: 1.1rem;
color: var(--fg);
text-decoration: none;
}
header.site nav {
display: flex;
gap: 1.5rem;
}
header.site nav a {
color: var(--fg-muted);
text-decoration: none;
font-size: 0.95rem;
}
header.site nav a:hover { color: var(--fg); }
main {
max-width: 640px;
flex: 1;
max-width: var(--max-width);
margin: 0 auto;
padding: 4rem 1.5rem;
width: 100%;
}
footer.site {
border-top: 1px solid var(--border);
margin-top: 2rem;
}
footer.site .inner {
max-width: var(--max-width);
margin: 0 auto;
padding: 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
font-size: 0.85rem;
color: var(--fg-muted);
}
footer.site nav {
display: flex;
gap: 1.25rem;
}
footer.site nav a {
color: var(--fg-muted);
text-decoration: none;
}
footer.site nav a:hover { color: var(--fg); }
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
border-radius: var(--radius);
background: var(--accent);
color: var(--accent-fg);
text-decoration: none;
font-weight: 600;
border: none;
font-size: 1rem;
cursor: pointer;
}
.btn:hover { opacity: 0.9; }
label {
display: block;
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.35rem;
}
input, textarea {
width: 100%;
padding: 0.65rem 0.75rem;
border-radius: var(--radius);
border: 1px solid var(--border);
background: var(--bg-raised);
color: var(--fg);
font-family: inherit;
font-size: 1rem;
margin-bottom: 1.25rem;
}
input:focus, textarea:focus {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
.field-honeypot {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
a { color: #1a5fb4; }
</style>
</head>
<body>
<header class="site">
<div class="inner">
<a class="brand" href="/">{{ .Site.Title }}</a>
<nav>
{{ range .Site.Menus.main }}
<a href="{{ .URL }}">{{ .Name }}</a>
{{ end }}
</nav>
</div>
</header>
<main>
{{ block "main" . }}{{ end }}
</main>
<footer class="site">
<div class="inner">
<span>&copy; {{ now.Year }} {{ .Site.Title }}</span>
<nav>
{{ range .Site.Menus.footer }}
<a href="{{ .URL }}">{{ .Name }}</a>
{{ end }}
</nav>
</div>
</footer>
</body>
</html>
+55
View File
@@ -0,0 +1,55 @@
{{ define "main" }}
<h1>Get in touch</h1>
<p style="opacity:0.85;">Tell me a bit about what you're working on — I read every message myself.</p>
<form id="contact-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="message">Message</label>
<textarea id="message" name="message" rows="6" required></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">Send message</button>
<p id="contact-status" role="status" style="margin-top:1rem;"></p>
</form>
<script>
(function () {
var form = document.getElementById("contact-form");
var status = document.getElementById("contact-status");
form.addEventListener("submit", function (e) {
e.preventDefault();
var payload = {
name: form.name.value,
email: form.email.value,
message: form.message.value,
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) {
if (!res.ok) throw new Error("request failed");
form.reset();
form.hidden = true;
status.textContent = "Thanks — I'll get back to you soon.";
}).catch(function () {
submitBtn.disabled = false;
status.textContent = "Something went wrong sending that. Try again, or email hello@reground.org directly.";
});
});
})();
</script>
{{ end }}
+46
View File
@@ -0,0 +1,46 @@
{{ define "main" }}
<h1 style="font-size:2rem; margin-bottom:0.5rem;">{{ .Title }}</h1>
{{ 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>
<p id="enroll-status" role="status" style="margin-top:1rem;"></p>
</form>
<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) throw new Error("request failed");
form.reset();
form.hidden = true;
status.textContent = "Check your inbox — day one is on its way.";
}).catch(function () {
submitBtn.disabled = false;
status.textContent = "Something went wrong. Try again in a moment.";
});
});
})();
</script>
{{ end }}
+1 -1
View File
@@ -4,7 +4,7 @@
<div style="margin-top:2rem;">{{ .Params.intro | markdownify }}</div>
{{ with .Params.cta_text }}
<p style="margin-top:2rem;">
<a href="{{ $.Params.cta_link }}" style="display:inline-block; padding:0.75rem 1.5rem; border-radius:0.4rem; background:#1a5fb4; color:#fff; text-decoration:none;">{{ . }}</a>
<a class="btn" href="{{ $.Params.cta_link }}">{{ . }}</a>
</p>
{{ end }}
{{ end }}