Add landing page for kjol, documentation

This commit is contained in:
2026-07-13 16:51:21 -04:00
parent 5230bd6702
commit fec8ef4a3e
54 changed files with 3529 additions and 547 deletions

View File

@@ -74,43 +74,78 @@ func DataPage(d Deps) func() *VNode {
fetchRepo(repoQuery.Get())
}
return Div(Attr("class", "space-y-8"),
Div(
H2(Attr("class", "text-2xl font-semibold tracking-tight text-text-heading"), Text("Data fetching")),
P(Attr("class", "mt-1 text-neutral-500"), Text("Two client-side fetches: gob from our own server, and JSON from a third-party API you choose.")),
),
return docPage("Rendering", "Data fetching",
"Fetching happens in the browser, so a server-rendered page ships its LOADING state and the "+
"client fills it in. Two shapes are shown here: gob against your own server, where the same "+
"Go type crosses the wire untranslated, and JSON against somebody else's API.",
ui.Card("",
ui.CardHeader("", Text("gob — from our server")),
P(Attr("class", "mb-3 text-sm text-neutral-500"),
Text("The client GETs /api/quotes; the server responds with httputil.RespondGob "+
"(a gob-encoded []Quote) and httputil.FetchGob decodes it straight into []Quote — "+
"the same Go type on both ends, no JSON.")),
quotesBody(qLoading.Get(), qErr.Get(), quotes.Get()),
),
ui.Card("",
ui.CardHeader("", Text("JSON — from a third-party API")),
P(Attr("class", "mb-3 text-sm text-neutral-500"),
Text("Enter a GitHub repo; the client GETs api.github.com and httputil.FetchJSON "+
"decodes the response into a Go struct with `json:\"…\"` tags.")),
row("mb-4 flex items-end gap-2",
row("flex grow flex-col gap-1 max-w-sm",
ui.FormLabel(ui.FormLabelProps{}, Text("GitHub repo (owner/name)")),
ui.FormInput(ui.FormInputProps{
Value: repoQuery.Get(),
Placeholder: "golang/go",
OnInput: func(v string) { repoQuery.Set(v) },
}),
),
ui.Button(ui.ButtonProps{Color: ui.ButtonPrimary, Text: "Fetch", OnClick: func() { fetchRepo(repoQuery.Get()) }}),
docSection("gob", "gob — the same Go type on both ends",
prose("Your server already speaks Go and so does your client, so there is no reason to translate "+
"through JSON in between. The handler answers with httputil.RespondGob([]Quote) and the "+
"client decodes straight back into []Quote — one type, declared once, with no tags and no "+
"hand-written unmarshalling to drift out of sync with it."),
code("app/data.go + server/main.go", gobSnippet),
demo("GET /api/quotes, decoded into []Quote",
quotesBody(qLoading.Get(), qErr.Get(), quotes.Get()),
),
),
docSection("json", "JSON — for everyone else's API",
prose("A third-party API does not speak gob, so httputil.FetchJSON decodes into a tagged struct "+
"the ordinary way. Enter a repository and the browser calls api.github.com directly."),
demo("GET api.github.com/repos/…, decoded into a tagged struct",
row("mb-4 flex items-end gap-2",
row("flex grow flex-col gap-1 max-w-sm",
ui.FormLabel(ui.FormLabelProps{}, Text("GitHub repo (owner/name)")),
ui.FormInput(ui.FormInputProps{
Value: repoQuery.Get(),
Placeholder: "golang/go",
OnInput: func(v string) { repoQuery.Set(v) },
}),
),
ui.Button(ui.ButtonProps{Color: ui.ButtonPrimary, Text: "Fetch", OnClick: func() { fetchRepo(repoQuery.Get()) }}),
),
repoBody(rLoading.Get(), rErr.Get(), repo.Get()),
),
),
docSection("ssr", "What the server renders",
prose("This route is static, so the server pre-renders it — but there is no fetch on the server: "+
"no transport is installed there, and inventing one would mean the server quietly making "+
"requests on the user's behalf. So a fetch started during SSR does nothing at all, the page "+
"renders its spinner, and the client runs the fetch for real once it has hydrated."),
note("A fetch that fails on the server is a bug in the framework, not in your page",
"An earlier version of this returned an error from SSR, and every static page that fetched "+
"anything rendered \"no client transport installed\" into its own HTML. Loading is the "+
"correct server-side answer to \"have you fetched this yet?\"."),
apiTable(
apiRow{"httputil.RespondGob", "Server: write a Go value as gob."},
apiRow{"httputil.FetchGob", "Client: decode a gob response into a Go value."},
apiRow{"httputil.FetchJSON", "Client: decode a JSON response into a tagged struct."},
apiRow{"httputil.SetClientTransport", "Override the transport — a base URL, auth headers. The runtime installs a fetch-based one for you."},
),
repoBody(rLoading.Get(), rErr.Get(), repo.Get()),
),
)
}
}
const gobSnippet = `// One type. Both ends. No tags, no JSON.
type Quote struct {
Author string
Text string
}
// --- server ---
mux.HandleFunc("GET /api/quotes", func(w http.ResponseWriter, r *http.Request) {
httputil.RespondGob(w, http.StatusOK, sampleQuotes()) // []Quote
})
// --- client ---
httputil.FetchGob("/api/quotes", func(qs []Quote, err error) {
if err != nil { qErr.Set(err.Error()); return }
quotes.Set(qs) // []Quote
})`
func quotesBody(loading bool, failed string, quotes []Quote) *VNode {
switch {
case failed != "":
@@ -121,8 +156,8 @@ func quotesBody(loading bool, failed string, quotes []Quote) *VNode {
cards := make([]*VNode, 0, len(quotes))
for _, q := range quotes {
cards = append(cards, ui.BorderCard("",
P(Attr("class", "text-neutral-800"), Text("“"+q.Text+"”")),
P(Attr("class", "mt-2 text-sm text-neutral-500"), Text("— "+q.Author)),
P(Attr("class", "text-ink"), Text("“"+q.Text+"”")),
P(Attr("class", "mt-2 text-sm text-ink-muted"), Text("— "+q.Author)),
))
}
return row("grid gap-3 sm:grid-cols-2", cards...)
@@ -138,10 +173,10 @@ func repoBody(loading bool, failed string, r repoInfo) *VNode {
default:
return ui.BorderCard("",
row("flex items-center gap-2",
Strong(Attr("class", "text-neutral-800"), Text(r.FullName)),
Strong(Attr("class", "text-ink"), Text(r.FullName)),
ui.Badge(ui.BadgeProps{Color: ui.BadgeAmber}, Text("★ "+strconv.Itoa(r.Stars))),
),
P(Attr("class", "mt-2 text-sm text-neutral-600"), Text(r.Description)),
P(Attr("class", "mt-2 text-sm text-ink-soft"), Text(r.Description)),
)
}
}