commit f380af16a1fe81da5d72b9b796decf84dad7c69d Author: Will Estes Date: Wed Jul 15 06:39:49 2026 -0400 Scaffold michaelferrara-site: Hugo skeleton with reader-magnet freebie flow diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..734dd56 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,16 @@ +name: deploy +on: + push: + branches: [master] +jobs: + deploy: + runs-on: ubuntu-latest + container: + image: hugomods/hugo:exts + volumes: + - /var/www/michaelferrara.org:/var/www/michaelferrara.org + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - run: hugo --minify -d /var/www/michaelferrara.org diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c3fa73 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +public/ +resources/ +.hugo_build.lock diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..fb31987 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +hugo 0.124.1 diff --git a/assets/css/main.css b/assets/css/main.css new file mode 100644 index 0000000..07bf6e7 --- /dev/null +++ b/assets/css/main.css @@ -0,0 +1,308 @@ +:root { + color-scheme: light dark; + + /* TODO: placeholder palette copied from reground.org's site — restyle to + match Michael's own branding before launch. */ + --bg: #faf8f4; + --bg-raised: #f5f1e8; + --fg: #333333; + --fg-muted: #6b6560; + --border: #e5ddd0; + + --slate: #4a6fa5; + --rose: #d9a9b0; + --teal: #009688; + --taupe: #b19a75; + + --heading: var(--slate); + --accent: var(--slate); + --accent-fg: #ffffff; + --cta: #007f73; + --cta-fg: #ffffff; + + --radius: 0.5rem; + --max-width: 42rem; + + --font-heading: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; + --font-body: "Open Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; + --font-accent: "Source Code Pro", "Courier New", monospace; +} +@media (prefers-color-scheme: dark) { + :root { + --bg: #1c1917; + --bg-raised: #262220; + --fg: #f0ebe3; + --fg-muted: #a89e92; + --border: #3a352f; + + --slate: #8fa8d1; + --rose: #e6c3ca; + --teal: #2dd4c4; + --taupe: #cbb190; + + --heading: var(--slate); + --accent: var(--slate); + --accent-fg: #14161a; + --cta: var(--teal); + --cta-fg: #0f1115; + } +} + +* { box-sizing: border-box; } + +body { + margin: 0; + font-family: var(--font-body); + line-height: 1.6; + background: var(--bg); + color: var(--fg); + display: flex; + flex-direction: column; + min-height: 100vh; +} + +a { color: var(--accent); } + +.skip-link { + position: absolute; + left: -9999px; + top: 0; + z-index: 100; + background: var(--cta); + color: var(--cta-fg); + padding: 0.75rem 1.25rem; + border-radius: 0 0 var(--radius) 0; +} +.skip-link:focus { + left: 0; +} + +h1, h2, h3 { + font-family: var(--font-heading); + color: var(--heading); + line-height: 1.25; +} + +code, kbd, .accent-text { + font-family: var(--font-accent); +} + +header.site { + background: var(--slate); +} +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-family: var(--font-heading); + font-weight: 700; + font-size: 1.1rem; + color: var(--bg); + text-decoration: none; +} +header.site nav { + display: flex; + gap: 1.5rem; +} +header.site nav a { + color: var(--bg); + opacity: 0.85; + text-decoration: none; + font-size: 0.95rem; +} +header.site nav a:hover { + opacity: 1; + text-decoration: underline; + text-decoration-color: var(--rose); + text-decoration-thickness: 2px; + text-underline-offset: 3px; +} +header.site a:focus-visible { + outline-color: var(--bg); +} + +main { + flex: 1; + max-width: var(--max-width); + margin: 0 auto; + padding: 4rem 1.5rem; + width: 100%; +} + +footer.site { + background: var(--bg-raised); + border-top: 1px solid var(--taupe); + 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(--slate); + text-decoration: none; +} +footer.site nav a:hover { + text-decoration: underline; + text-decoration-color: var(--rose); + text-decoration-thickness: 2px; + text-underline-offset: 3px; +} + +.btn { + display: inline-block; + padding: 0.75rem 1.5rem; + border-radius: var(--radius); + background: var(--cta); + color: var(--cta-fg); + text-decoration: none; + font-weight: 600; + border: none; + font-size: 1rem; + cursor: pointer; +} +.btn:hover { opacity: 0.9; } + +a:focus-visible, button:focus-visible { + outline: 2px solid var(--cta); + outline-offset: 2px; +} + +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(--cta); + outline-offset: 1px; +} + +.field-honeypot { + position: absolute; + left: -9999px; + width: 1px; + height: 1px; + overflow: hidden; +} + +/* Course/freebie landing pages (layouts/courses/single.html) */ +.course-hero { + text-align: center; +} +.course-subhead { + font-style: italic; + color: var(--fg-muted); + margin-top: 0; +} +.course-social-proof { + font-style: italic; + font-size: 0.9rem; + color: var(--fg-muted); +} +.enroll-form { + max-width: 24rem; + margin: 1.5rem auto; + text-align: left; +} +.enroll-status { + text-align: center; +} +.enroll-download { + text-align: center; + margin-top: 1rem; +} + +.course-credibility, +.course-curriculum { + background: var(--bg-raised); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 2rem; + margin: 3rem 0; +} +.course-bullets { + list-style: none; + padding: 0; + margin: 1.5rem 0 0; +} +.course-bullets li { + position: relative; + padding-left: 1.75rem; + margin-bottom: 0.75rem; +} +.course-bullets li::before { + content: "✓"; + position: absolute; + left: 0; + color: var(--cta); + font-weight: 700; +} +.course-curriculum-list { + list-style: none; + padding: 0; + margin: 1.5rem 0 0; +} +.course-curriculum-list li { + margin-bottom: 1rem; +} +.course-day-label { + font-family: var(--font-heading); + color: var(--cta); + font-weight: 700; +} +.course-repeat-cta { + text-align: center; + margin-top: 3rem; +} + +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; + } +} diff --git a/content/_index.md b/content/_index.md new file mode 100644 index 0000000..c9c57d1 --- /dev/null +++ b/content/_index.md @@ -0,0 +1,8 @@ +--- +title: "Michael Ferrara" +description: "TODO: one-line author bio/description" +--- + +TODO: Michael's homepage copy goes here. + +Get a free short story: [download the reader magnet](/freebie). diff --git a/content/contact.md b/content/contact.md new file mode 100644 index 0000000..ccd4d50 --- /dev/null +++ b/content/contact.md @@ -0,0 +1,5 @@ +--- +title: "Contact" +layout: "contact" +description: "Get in touch with Michael Ferrara." +--- diff --git a/content/freebie-thanks/index.md b/content/freebie-thanks/index.md new file mode 100644 index 0000000..4410aea --- /dev/null +++ b/content/freebie-thanks/index.md @@ -0,0 +1,8 @@ +--- +title: "Thanks — here's your story" +description: "Download your free short story." +--- + +Thanks for signing up — here's your copy. + +[Download the story](https://downloads.michaelferrara.org/TODO-story-filename.epub) diff --git a/content/freebie/index.md b/content/freebie/index.md new file mode 100644 index 0000000..fa3d85d --- /dev/null +++ b/content/freebie/index.md @@ -0,0 +1,23 @@ +--- +title: "TODO: Story Title" +description: "TODO: one-line description of the free short story." +type: "courses" +eec_slug: "reader-magnet" + +# Optional italic line under the title, for handling an objection up front. +hero_subhead: "TODO: subhead for the landing page" + +# Label on the signup form's submit button. +cta_text: "Send me the story" + +# Direct link to the epub in the public downloads bucket — shown as an +# on-page button immediately after a successful signup, independent of the +# welcome email (see layouts/partials/course-enroll-form.html). +download_url: "https://downloads.michaelferrara.org/TODO-story-filename.epub" +download_text: "Download now" + +# Optional heading above the repeated signup form at the bottom of the page. +repeat_cta_heading: "Want the story?" +--- + +TODO: Michael's landing-page copy goes here. diff --git a/content/privacy.md b/content/privacy.md new file mode 100644 index 0000000..6b251b5 --- /dev/null +++ b/content/privacy.md @@ -0,0 +1,62 @@ +--- +title: "Privacy Policy" +description: "How michaelferrara.org collects, uses, and protects your information." +--- + +_Last updated: July 2026_ + +This policy explains what information this site collects through use of +michaelferrara.org, and what happens to it. + +## What this site collects + +Depending on how you use the site, that may include: + +- Your name, email address, and message, when you submit the contact form. +- Your email address, if you sign up for the free short story. +- Your email address and name, if given, if you opt in to ongoing updates. + +This site doesn't use third-party analytics, ad trackers, or tracking +pixels. Everything above runs on infrastructure operated directly — +nothing is handed to a third-party marketing platform. + +## Why this site collects it + +- To respond to messages sent through the contact form. +- To deliver the short story you signed up for. +- To send periodic updates to people who've explicitly opted in — never + as a side effect of simply using the site. + +## What this site doesn't do + +- This site doesn't sell or rent your information to anyone. +- This site doesn't share it with third parties except the infrastructure + providers necessary to run the 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 ongoing-update email includes an unsubscribe + link. Clicking it removes you from that list immediately. +- **The free short story**: this is a one-off signup with no ongoing + subscription — once you've received it, there's nothing further to opt + out of unless you also joined the ongoing updates list. +- **Deletion**: reach out through [the contact form](/contact/) and + your information will be deleted from these systems. + +## Security + +Your information is stored on infrastructure controlled 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. + +## Changes + +If this policy changes in a way that matters, the date at the top of this +page will be updated. + +## Questions + +[Get in touch](/contact/). diff --git a/content/terms.md b/content/terms.md new file mode 100644 index 0000000..83bb9db --- /dev/null +++ b/content/terms.md @@ -0,0 +1,39 @@ +--- +title: "Terms of Use" +description: "Terms for using michaelferrara.org and its free short story." +--- + +_Last updated: July 2026_ + +These terms cover using michaelferrara.org, its free short story, and +submitting the contact form. + +## Using this site + +This site and its free short story are provided as-is, for entertainment +purposes. + +## The short story + +The story belongs to Michael Ferrara. You're welcome to read and enjoy it; +you're not entitled to redistribute, resell, or republish it without +asking first. + +## No warranty + +This site and its content are provided without warranties of any kind, +express or implied. + +## Liability + +To the extent the law allows, Michael Ferrara isn't liable for damages +arising from your use of this site or its content. + +## Changes + +These terms may be updated as the site changes. Material changes will +update the date at the top of this page. + +## Questions + +[Get in touch](/contact/). diff --git a/hugo.toml b/hugo.toml new file mode 100644 index 0000000..d455d5e --- /dev/null +++ b/hugo.toml @@ -0,0 +1,30 @@ +baseURL = "https://michaelferrara.org/" +locale = "en-US" +title = "Michael Ferrara" + +enableRobotsTXT = true +buildDrafts = false +buildFuture = false +buildExpired = false +disableKinds = ["taxonomy", "term"] + +[params] + env = "production" + description = "TODO: one-line author bio/description" + author = "Michael Ferrara" + eec_base_url = "https://eec.michaelferrara.org" + +[[menu.footer]] + name = "Contact" + url = "/contact/" + weight = 10 + +[[menu.footer]] + name = "Privacy" + url = "/privacy/" + weight = 20 + +[[menu.footer]] + name = "Terms" + url = "/terms/" + weight = 30 diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html new file mode 100644 index 0000000..cf80d68 --- /dev/null +++ b/layouts/_default/baseof.html @@ -0,0 +1,48 @@ + + + + + + {{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} · {{ .Site.Title }}{{ end }} + + + + + {{ $css := resources.Get "css/main.css" }} + {{ if hugo.IsProduction }} + {{ $css = $css | minify | fingerprint }} + + {{ else }} + + {{ end }} + + + + +
+
+ {{ .Site.Title }} + +
+
+ +
+ {{ block "main" . }}{{ end }} +
+ + + + diff --git a/layouts/_default/contact.html b/layouts/_default/contact.html new file mode 100644 index 0000000..27a2f74 --- /dev/null +++ b/layouts/_default/contact.html @@ -0,0 +1,60 @@ +{{ define "main" }} +

Get in touch

+

Tell me a bit about what you're working on — I read every message myself.

+ +
+ + + + + + + + + + + + +
+

+ + +{{ end }} diff --git a/layouts/_default/list.html b/layouts/_default/list.html new file mode 100644 index 0000000..426e8db --- /dev/null +++ b/layouts/_default/list.html @@ -0,0 +1,9 @@ +{{ define "main" }} +

{{ .Title }}

+{{ .Content }} + +{{ end }} diff --git a/layouts/_default/single.html b/layouts/_default/single.html new file mode 100644 index 0000000..17b274b --- /dev/null +++ b/layouts/_default/single.html @@ -0,0 +1,4 @@ +{{ define "main" }} +

{{ .Title }}

+{{ .Content }} +{{ end }} diff --git a/layouts/courses/single.html b/layouts/courses/single.html new file mode 100644 index 0000000..08712f6 --- /dev/null +++ b/layouts/courses/single.html @@ -0,0 +1,42 @@ +{{ define "main" }} +
+

{{ .Title }}

+ {{ with .Params.hero_subhead }}

{{ . }}

{{ end }} + + {{ partial "course-enroll-form.html" (dict "id" "top" "page" .) }} + + {{ with .Params.social_proof }}

{{ . }}

{{ end }} +
+ +{{ if or .Params.credibility_heading .Params.credibility_bullets }} +
+ {{ with .Params.credibility_heading }}

{{ . }}

{{ end }} + {{ with .Params.credibility_bullets }} + + {{ end }} +
+{{ end }} + +{{ if or .Params.curriculum_heading .Params.curriculum_items }} +
+ {{ with .Params.curriculum_heading }}

{{ . }}

{{ end }} + {{ with .Params.curriculum_intro }}

{{ . }}

{{ end }} + {{ with .Params.curriculum_items }} +
    + {{ range . }} +
  1. {{ with .label }}{{ . }} {{ end }}{{ .text | markdownify }}
  2. + {{ end }} +
+ {{ end }} +
+{{ end }} + +{{ .Content }} + +
+ {{ with .Params.repeat_cta_heading }}

{{ . }}

{{ end }} + {{ partial "course-enroll-form.html" (dict "id" "bottom" "page" .) }} +
+{{ end }} diff --git a/layouts/index.html b/layouts/index.html new file mode 100644 index 0000000..17b274b --- /dev/null +++ b/layouts/index.html @@ -0,0 +1,4 @@ +{{ define "main" }} +

{{ .Title }}

+{{ .Content }} +{{ end }} diff --git a/layouts/partials/course-enroll-form.html b/layouts/partials/course-enroll-form.html new file mode 100644 index 0000000..e0dcd76 --- /dev/null +++ b/layouts/partials/course-enroll-form.html @@ -0,0 +1,55 @@ +{{ $id := .id }} +{{ $page := .page }} +
+ + + + + + +
+

+{{ with $page.Params.download_url }} + +{{ end }} + +