Skip to content

Commit 2e24f58

Browse files
authored
Replace medals with fixed-width star pills (#69)
Medals suggested a judged ranking when the order is only star count. Every entry now starts with a 46px star pill SVG under badges/stars/, generated with the badges, so the names line up in one column and the trailing count is gone. The site keeps its numeric rank and podium highlight.
1 parent 202e8a1 commit 2e24f58

9 files changed

Lines changed: 96 additions & 49 deletions

File tree

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Thanks for helping keep this list short and honest. The whole point of the list
99
- `README.md`, `docs/index.html`, `metadata.json` and everything under `badges/` are generated. CI rejects pull requests that touch them.
1010
- The generated `docs/` folder is published with GitHub Pages at [floatdrop.github.io/awesome-go](https://floatdrop.github.io/awesome-go/), with search, filtering and sorting over the same data.
1111
- A nightly job refreshes star counts, removes repositories that were archived or deleted, and regenerates the README and badges. Nobody has to remember to clean up.
12-
- Within each category the five most-starred projects are shown first, the top three with medals (🥇🥈🥉), and the rest are folded under a collapsed "More" block. Stars decide the order, humans decide who is on the list at all.
12+
- Within each category the five most-starred projects are shown first, each with its star count, and the rest are folded under a collapsed "More" block. Stars decide the order, humans decide who is on the list at all.
1313

1414
## Adding an entry
1515

‎cmd/awesome/sync.go‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,15 +185,30 @@ func runGenerate(args []string) error {
185185
if err := os.MkdirAll(dir, 0o755); err != nil {
186186
return err
187187
}
188-
old, _ := filepath.Glob(filepath.Join(dir, "*.svg"))
189-
for _, f := range old {
190-
if err := os.Remove(f); err != nil {
191-
return err
188+
stars := filepath.Join(dir, render.StarsDir)
189+
if err := os.MkdirAll(stars, 0o755); err != nil {
190+
return err
191+
}
192+
for _, pattern := range []string{filepath.Join(dir, "*.svg"), filepath.Join(stars, "*.svg")} {
193+
old, _ := filepath.Glob(pattern)
194+
for _, f := range old {
195+
if err := os.Remove(f); err != nil {
196+
return err
197+
}
192198
}
193199
}
194200
generated := 0
195201
skipped := 0
196202
for _, e := range l.Entries {
203+
rm, known := meta.Repos[e.Repo]
204+
title := "Stars not fetched yet"
205+
if known {
206+
title = fmt.Sprintf("%s has %d stars on GitHub", e.Repo, rm.Stars)
207+
}
208+
pill := badge.Stars(render.StarsText(rm.Stars, known), title)
209+
if err := os.WriteFile(filepath.Join(p.root, render.StarsPath(e.Repo)), pill, 0o644); err != nil {
210+
return err
211+
}
197212
if e.Added == "" {
198213
skipped++ // refresh assigns the acceptance date; the badge title mentions it.
199214
continue

‎internal/badge/badge.go‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,28 @@ func textWidth(s string) int {
176176
}
177177
return int(total + 0.5)
178178
}
179+
180+
// StarsWidth is the width of every star pill, so the names after them line
181+
// up regardless of how many digits the count has.
182+
const StarsWidth = 46
183+
184+
// Stars renders the star pill that precedes each entry in the README: the
185+
// count in a fixed-width rounded box, in the same style as the badge, so the
186+
// list reads as a single column of numbers.
187+
func Stars(text, title string) []byte {
188+
var b strings.Builder
189+
w := func(format string, args ...any) { fmt.Fprintf(&b, format, args...) }
190+
w(`<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="20" role="img" aria-label="%s">`, StarsWidth, html.EscapeString(title))
191+
w(`<title>%s</title>`, html.EscapeString(title))
192+
w(`<linearGradient id="s" x2="0" y2="100%%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient>`)
193+
w(`<clipPath id="r"><rect width="%d" height="20" rx="3" fill="#fff"/></clipPath>`, StarsWidth)
194+
w(`<g clip-path="url(#r)"><rect width="%d" height="20" fill="#555"/><rect width="%d" height="20" fill="url(#s)"/></g>`, StarsWidth, StarsWidth)
195+
w(`<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">`)
196+
cx := StarsWidth / 2
197+
t := html.EscapeString(text)
198+
w(`<text x="%d" y="15" fill="#010101" fill-opacity=".3">★ %s</text>`, cx, t)
199+
w(`<text x="%d" y="14"><tspan fill="#dfb317">★</tspan> %s</text>`, cx, t)
200+
w(`</g></svg>`)
201+
b.WriteString("\n")
202+
return []byte(b.String())
203+
}

‎internal/badge/badge_test.go‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,15 @@ func TestColorIsStableAndVaried(t *testing.T) {
102102
t.Fatalf("expected several colours across seeds, got %d", len(seen))
103103
}
104104
}
105+
106+
func TestStarsPillHasFixedWidth(t *testing.T) {
107+
for _, text := range []string{"120", "2.1k", "89k", "new"} {
108+
svg := string(Stars(text, "x has stars"))
109+
if !strings.Contains(svg, `width="46"`) {
110+
t.Errorf("Stars(%q) must be %d wide:\n%s", text, StarsWidth, svg)
111+
}
112+
if !strings.Contains(svg, "★</tspan> "+text+"</text>") {
113+
t.Errorf("Stars(%q) must show the count:\n%s", text, svg)
114+
}
115+
}
116+
}

‎internal/render/readme.go‎

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,22 @@ import (
1111
"github.com/floatdrop/awesome-go/internal/list"
1212
)
1313

14-
var medals = []string{"🥇", "🥈", "🥉"}
14+
// StarsDir is the directory under badges/ that holds one star pill per entry.
15+
const StarsDir = "stars"
1516

16-
// blank pads podium entries without a medal so the names line up with the
17-
// medalled ones above them. Emoji render about a third wider than one em, so
18-
// an em space plus a three-per-em space is the closest text-only match.
19-
const blank = "&emsp;&#8196;"
17+
// StarsPath is the README-relative path of an entry's star pill.
18+
func StarsPath(repo string) string {
19+
return "badges/" + StarsDir + "/" + list.Slug(repo) + ".svg"
20+
}
21+
22+
// StarsText is what the star pill shows: the rounded count, or "new" for an
23+
// entry whose stars have not been fetched yet.
24+
func StarsText(stars int, known bool) string {
25+
if !known {
26+
return "new"
27+
}
28+
return FormatStars(stars)
29+
}
2030

2131
// Header is the first line of the generated README; CI uses it to recognise
2232
// the file as generated.
@@ -105,13 +115,13 @@ func README(l *list.List, m *list.Metadata, now time.Time) []byte {
105115
if podium <= 0 || podium >= len(rs) {
106116
podium = len(rs)
107117
}
108-
for i, r := range rs[:podium] {
109-
w("- %s\n", item(r, medal(i, l.Meta.Podium)))
118+
for _, r := range rs[:podium] {
119+
w("- %s\n", item(r))
110120
}
111121
if rest := rs[podium:]; len(rest) > 0 {
112122
w("\n<details>\n<summary>More (%d)</summary>\n\n", len(rest))
113123
for _, r := range rest {
114-
w("- %s\n", item(r, ""))
124+
w("- %s\n", item(r))
115125
}
116126
w("\n</details>\n")
117127
}
@@ -147,28 +157,14 @@ func README(l *list.List, m *list.Metadata, now time.Time) []byte {
147157
return []byte(b.String())
148158
}
149159

150-
func medal(i, podium int) string {
151-
if podium <= 0 {
152-
return ""
153-
}
154-
if i >= len(medals) {
155-
return blank
156-
}
157-
return medals[i]
158-
}
159-
160-
// item renders one entry line. The star and the count are joined by a
161-
// non-breaking space so a narrow page never wraps between them.
162-
func item(r ranked, prefix string) string {
163-
var b strings.Builder
164-
if prefix != "" {
165-
b.WriteString(prefix + " ")
166-
}
167-
fmt.Fprintf(&b, "[%s](%s) - %s", r.entry.DisplayName(), r.entry.URL(), r.entry.Description)
160+
// item renders one entry line. Every line starts with the entry's star pill,
161+
// a fixed-width image, so the names line up in one column.
162+
func item(r ranked) string {
163+
alt := "new"
168164
if r.known {
169-
fmt.Fprintf(&b, " ★&nbsp;%s", FormatStars(r.stars))
165+
alt = FormatStars(r.stars) + " stars"
170166
}
171-
return b.String()
167+
return fmt.Sprintf("![%s](%s) [%s](%s) - %s", alt, StarsPath(r.entry.Repo), r.entry.DisplayName(), r.entry.URL(), r.entry.Description)
172168
}
173169

174170
// FormatStars renders 1234 as "1.2k" and 80123 as "80k".

‎internal/render/readme_test.go‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ func TestREADMEOrderAndPodium(t *testing.T) {
3838
"# Awesome Go\n\n> Curated.\n",
3939
"## Contents\n\n- [Web & HTTP](#web--http)\n",
4040
"## Web & HTTP\n\nServers.\n\n",
41-
"- 🥇 [big](https://github.com/b/big) - Big. ★&nbsp;46k\n",
42-
"- 🥈 [middle](https://github.com/c/mid) - Mid. ★&nbsp;2.1k\n",
43-
"<summary>More (2)</summary>\n\n- [small](https://github.com/a/small) - Small. ★&nbsp;120\n- [unknown](https://github.com/d/unknown) - Unknown.\n",
41+
"- ![46k stars](badges/stars/b--big.svg) [big](https://github.com/b/big) - Big.\n",
42+
"- ![2.1k stars](badges/stars/c--mid.svg) [middle](https://github.com/c/mid) - Mid.\n",
43+
"<summary>More (2)</summary>\n\n- ![120 stars](badges/stars/a--small.svg) [small](https://github.com/a/small) - Small.\n- ![new](badges/stars/d--unknown.svg) [unknown](https://github.com/d/unknown) - Unknown.\n",
4444
"last refreshed on 2026-09-12",
4545
"An entry earns its place by covering something no listed project does, or by doing it better.",
4646
"Search, filter and sort the full list at [x.github.io/y](https://x.github.io/y/).",
@@ -52,16 +52,16 @@ func TestREADMEOrderAndPodium(t *testing.T) {
5252
if strings.Contains(out, "## Empty") || strings.Contains(out, "#empty") {
5353
t.Errorf("empty category should be skipped")
5454
}
55-
if strings.Contains(out, "🥉") {
56-
t.Errorf("podium of 2 must not award bronze")
55+
if strings.Contains(out, "🥇") || strings.Contains(out, "★") {
56+
t.Errorf("medals and inline star counts were replaced by star pills")
5757
}
5858
}
5959

60-
func TestREADMEPadsPodiumEntriesWithoutMedal(t *testing.T) {
60+
func TestREADMEPodiumWithoutMore(t *testing.T) {
6161
l, m := fixture(4)
6262
out := string(README(l, m, time.Now()))
63-
if !strings.Contains(out, "- 🥉 [small](") || !strings.Contains(out, "- &emsp;&#8196; [unknown](") {
64-
t.Fatalf("fourth podium entry must be padded:\n%s", out)
63+
if !strings.Contains(out, "- ![120 stars](badges/stars/a--small.svg) [small](") || !strings.Contains(out, "- ![new](badges/stars/d--unknown.svg) [unknown](") {
64+
t.Fatalf("every podium entry must carry a star pill:\n%s", out)
6565
}
6666
if strings.Contains(out, "<details>") {
6767
t.Fatalf("no More block expected when every entry is on the podium")
@@ -124,7 +124,7 @@ func TestREADMELinkSections(t *testing.T) {
124124
func TestREADMEFlatWithoutPodium(t *testing.T) {
125125
l, m := fixture(0)
126126
out := string(README(l, m, time.Now()))
127-
if strings.Contains(out, "<details>") || strings.Contains(out, "🥇") {
127+
if strings.Contains(out, "<details>") {
128128
t.Fatalf("podium disabled but rendered:\n%s", out)
129129
}
130130
big := strings.Index(out, "[big]")

‎internal/render/site.go‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ type siteCategory struct {
5656

5757
type siteEntry struct {
5858
Rank int
59-
Medal string
59+
Podium bool
6060
Slug string
6161
Name string
6262
Repo string
@@ -117,9 +117,7 @@ func Site(l *list.List, m *list.Metadata, now time.Time) ([]byte, error) {
117117
})
118118
for i := range sc.Entries {
119119
sc.Entries[i].Rank = i + 1
120-
if l.Meta.Podium > 0 && i < len(medals) && i < l.Meta.Podium {
121-
sc.Entries[i].Medal = medals[i]
122-
}
120+
sc.Entries[i].Podium = l.Meta.Podium > 0 && i < l.Meta.Podium
123121
}
124122
page.Categories = append(page.Categories, sc)
125123
}

‎internal/render/site.tmpl‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ footer{margin:48px 0 40px;color:var(--muted);font-size:.9rem;border-top:1px soli
6767
<h2><a href="#{{.ID}}">{{.Name}}</a></h2>
6868
{{with .Description}}<p class="desc">{{.}}</p>{{end}}
6969
<ol class="entries">
70-
{{range .Entries}}<li class="entry{{if .Medal}} podium{{end}}" id="{{.Slug}}" data-name="{{.Name}}" data-stars="{{.Stars}}" data-added="{{.Added}}" data-search="{{.Search}}">
71-
<span class="rank">{{if .Medal}}{{.Medal}}{{else}}{{.Rank}}{{end}}</span>
70+
{{range .Entries}}<li class="entry{{if .Podium}} podium{{end}}" id="{{.Slug}}" data-name="{{.Name}}" data-stars="{{.Stars}}" data-added="{{.Added}}" data-search="{{.Search}}">
71+
<span class="rank">{{.Rank}}</span>
7272
<div class="body"><a class="name" href="{{.URL}}">{{.Name}}</a><span class="repo">{{.Repo}}</span><p>{{.Description}}</p></div>
7373
<span class="stars" title="GitHub stars">★ {{.StarsText}}</span>
7474
</li>

‎internal/render/site_test.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ func TestSite(t *testing.T) {
4545
`<option value="web">Web &amp; HTTP</option>`,
4646
`<section class="category" id="web" data-id="web">`,
4747
`id="b--big" data-name="big" data-stars="45678" data-added=""`,
48-
`<span class="rank">🥇</span>`,
48+
`<li class="entry podium"`,
49+
`<span class="rank">1</span>`,
4950
`<span class="rank">4</span>`,
5051
"Small &lt;b&gt;&amp;&lt;/b&gt; escaped.",
5152
"stars refreshed 2026-09-12",

0 commit comments

Comments
 (0)