| 1 | --- |
| 2 | import Logo from "./Logo.astro"; |
| 3 | import { CONTRIBUTORS } from "../../data/contributors"; |
| 4 | |
| 5 | interface FooterLink { |
| 6 | label: string; |
| 7 | href: string; |
| 8 | } |
| 9 | interface FooterCol { |
| 10 | h: string; |
| 11 | links: FooterLink[]; |
| 12 | } |
| 13 | |
| 14 | const cols: FooterCol[] = [ |
| 15 | { |
| 16 | h: "Product", |
| 17 | links: [ |
| 18 | { label: "Docs", href: "/getting-started" }, |
| 19 | { label: "Tutorial", href: "/cloudflare/tutorial/part-1" }, |
| 20 | { label: "Compare", href: "/compare" }, |
| 21 | { label: "Effect", href: "https://effect.website" }, |
| 22 | // todo(sam): create an /examples landing page (or sidebar entry) |
| 23 | // and re-enable: { label: "Examples", href: "/examples" } |
| 24 | // todo(sam): create /providers/index.md that lists every provider |
| 25 | // (Starlight currently autogenerates the sidebar from the |
| 26 | // providers/ folder but there's no overview page to link to). |
| 27 | // Re-enable: { label: "Providers", href: "/providers" } |
| 28 | // todo(sam): wire a /changelog page that mirrors the GitHub |
| 29 | // releases / CHANGELOG.md (either fetch at build time or add a |
| 30 | // generator that drops a markdown file under content/docs). |
| 31 | // Re-enable: { label: "Changelog", href: "/changelog" } |
| 32 | ], |
| 33 | }, |
| 34 | { |
| 35 | h: "Community", |
| 36 | links: [ |
| 37 | { label: "Discord", href: "/discord" }, |
| 38 | { label: "GitHub", href: "https://github.com/alchemy-run/alchemy" }, |
| 39 | { label: "X / Twitter", href: "/twitter" }, |
| 40 | { label: "Blog", href: "/blog" }, |
| 41 | { label: "Privacy", href: "/privacy" }, |
| 42 | ], |
| 43 | }, |
| 44 | // todo(sam): /compare covers SST, Pulumi, and Terraform on one page. |
| 45 | // Split into per-tool pages (and add CDK) to re-enable this column. |
| 46 | // { |
| 47 | // h: "Compare", |
| 48 | // links: [ |
| 49 | // { label: "vs Pulumi", href: "/compare/pulumi" }, |
| 50 | // { label: "vs SST", href: "/compare/sst" }, |
| 51 | // { label: "vs Terraform", href: "/compare/terraform" }, |
| 52 | // { label: "vs CDK", href: "/compare/cdk" }, |
| 53 | // ], |
| 54 | // }, |
| 55 | ]; |
| 56 | |
| 57 | // Community contributors, most commits first: a few faces, then the rest |
| 58 | // as a count linking to GitHub's contributors graph. |
| 59 | const SHOWN = 8; |
| 60 | const faces = CONTRIBUTORS.slice(0, SHOWN); |
| 61 | const rest = CONTRIBUTORS.length - faces.length; |
| 62 | const graph = |
| 63 | "https://github.com/alchemy-run/alchemy/graphs/contributors?all=1"; |
| 64 | --- |
| 65 | |
| 66 | <footer class="alc-footer"> |
| 67 | <div class="alc-footer__grid" style={`--footer-cols:${cols.length}`}> |
| 68 | <div> |
| 69 | <Logo /> |
| 70 | <p class="alc-footer__tag"> |
| 71 | Infrastructure as Effects. A single, type-safe TypeScript program for your cloud. |
| 72 | </p> |
| 73 | <div class="alc-footer__copy">Apache-2.0 · © 2026 Functionless, Inc.</div> |
| 74 | <div class="alc-footer__people"> |
| 75 | <ul class="alc-footer__faces" aria-label="Contributors"> |
| 76 | {faces.map((c) => ( |
| 77 | <li> |
| 78 | <a |
| 79 | href={`https://github.com/${c.login}`} |
| 80 | title={`@${c.login} · ${c.commits} commits`} |
| 81 | > |
| 82 | <img |
| 83 | src={`https://github.com/${c.login}.png?size=64`} |
| 84 | alt={`@${c.login}`} |
| 85 | width="28" |
| 86 | height="28" |
| 87 | loading="lazy" |
| 88 | decoding="async" |
| 89 | /> |
| 90 | </a> |
| 91 | </li> |
| 92 | ))} |
| 93 | </ul> |
| 94 | {rest > 0 && ( |
| 95 | <a class="alc-footer__more" href={graph} aria-label={`${rest} more contributors on GitHub`}> |
| 96 | +{rest} |
| 97 | </a> |
| 98 | )} |
| 99 | </div> |
| 100 | </div> |
| 101 | {cols.map((col) => ( |
| 102 | <div> |
| 103 | <div class="alc-footer__heading">{col.h}</div> |
| 104 | <ul class="alc-footer__list"> |
| 105 | {col.links.map((l) => ( |
| 106 | <li><a href={l.href} class="alc-footer__link">{l.label}</a></li> |
| 107 | ))} |
| 108 | </ul> |
| 109 | </div> |
| 110 | ))} |
| 111 | </div> |
| 112 | </footer> |
| 113 | |
| 114 | <style> |
| 115 | .alc-footer { |
| 116 | padding: 40px 32px; |
| 117 | border-top: 1px solid var(--alc-hairline); |
| 118 | background: var(--alc-bg-nav); |
| 119 | } |
| 120 | .alc-footer__grid { |
| 121 | max-width: 1152px; |
| 122 | margin: 0 auto; |
| 123 | display: grid; |
| 124 | /* Brand on the left, link columns sized to their content and pinned to |
| 125 | the right edge, so the footer spans the same width as the page. */ |
| 126 | grid-template-columns: minmax(0, 1fr) repeat(var(--footer-cols), max-content); |
| 127 | column-gap: clamp(48px, 8vw, 120px); |
| 128 | row-gap: 40px; |
| 129 | } |
| 130 | @media (max-width: 880px) { |
| 131 | .alc-footer__grid { |
| 132 | grid-template-columns: repeat(var(--footer-cols), minmax(0, 1fr)); |
| 133 | gap: 28px 24px; |
| 134 | } |
| 135 | .alc-footer__grid > :first-child { |
| 136 | grid-column: 1 / -1; |
| 137 | max-width: 520px; |
| 138 | } |
| 139 | } |
| 140 | @media (max-width: 640px) { |
| 141 | .alc-footer { padding: 32px 20px; } |
| 142 | .alc-footer__grid { |
| 143 | grid-template-columns: 1fr 1fr; |
| 144 | gap: 24px 20px; |
| 145 | } |
| 146 | } |
| 147 | @media (max-width: 380px) { |
| 148 | .alc-footer__grid { grid-template-columns: 1fr; } |
| 149 | } |
| 150 | .alc-footer__tag { |
| 151 | max-width: 380px; |
| 152 | font-family: var(--alc-font-sans); |
| 153 | font-size: 13px; |
| 154 | line-height: 1.6; |
| 155 | color: var(--alc-fg-3); |
| 156 | margin: 14px 0 0; |
| 157 | } |
| 158 | .alc-footer__copy { |
| 159 | margin-top: 16px; |
| 160 | font-family: var(--alc-font-mono); |
| 161 | font-size: 11px; |
| 162 | color: var(--alc-fg-4); |
| 163 | } |
| 164 | .alc-footer__people { |
| 165 | margin-top: 22px; |
| 166 | display: flex; |
| 167 | align-items: center; |
| 168 | gap: 10px; |
| 169 | } |
| 170 | .alc-footer__faces { |
| 171 | list-style: none; |
| 172 | margin: 0; |
| 173 | padding: 0; |
| 174 | display: flex; |
| 175 | } |
| 176 | .alc-footer__faces li + li { |
| 177 | margin-left: -12px; |
| 178 | } |
| 179 | .alc-footer__faces a { |
| 180 | position: relative; |
| 181 | display: block; |
| 182 | border-radius: 50%; |
| 183 | transition: transform 160ms ease; |
| 184 | } |
| 185 | .alc-footer__faces a:hover, |
| 186 | .alc-footer__faces a:focus-visible { |
| 187 | z-index: 1; |
| 188 | transform: translateY(-2px); |
| 189 | } |
| 190 | .alc-footer__faces a:focus-visible { |
| 191 | outline: 2px solid var(--alc-accent); |
| 192 | outline-offset: 2px; |
| 193 | } |
| 194 | .alc-footer__faces img { |
| 195 | display: block; |
| 196 | width: 28px; |
| 197 | height: 28px; |
| 198 | border-radius: 50%; |
| 199 | /* A ring in the footer color separates the overlapping faces. */ |
| 200 | box-shadow: 0 0 0 2px var(--alc-bg-nav); |
| 201 | background: var(--alc-hairline); |
| 202 | } |
| 203 | .alc-footer__more { |
| 204 | font-family: var(--alc-font-mono); |
| 205 | font-size: 13px; |
| 206 | color: var(--alc-accent-deep); |
| 207 | text-decoration: none; |
| 208 | } |
| 209 | .alc-footer__more:hover { |
| 210 | text-decoration: underline; |
| 211 | } |
| 212 | .alc-footer__heading { |
| 213 | font-family: var(--alc-font-mono); |
| 214 | font-size: 11px; |
| 215 | letter-spacing: 0.12em; |
| 216 | text-transform: uppercase; |
| 217 | color: var(--alc-fg-4); |
| 218 | margin-bottom: 12px; |
| 219 | } |
| 220 | .alc-footer__list { |
| 221 | list-style: none; |
| 222 | padding: 0; |
| 223 | margin: 0; |
| 224 | display: flex; |
| 225 | flex-direction: column; |
| 226 | gap: 8px; |
| 227 | } |
| 228 | .alc-footer__link { |
| 229 | text-decoration: none; |
| 230 | color: var(--alc-fg-2); |
| 231 | font-size: 13px; |
| 232 | } |
| 233 | .alc-footer__link:hover { |
| 234 | color: var(--alc-accent-deep); |
| 235 | } |
| 236 | </style> |