diff --git a/.github/workflows/site-deploy.yaml b/.github/workflows/site-deploy.yaml index 2a37437a..eb609f53 100644 --- a/.github/workflows/site-deploy.yaml +++ b/.github/workflows/site-deploy.yaml @@ -18,8 +18,16 @@ jobs: - uses: actions/checkout@v6 - name: Get dependencies run: go get -v -t -d ./... + - name: Restore GitHub metadata cache + uses: actions/cache@v4 + with: + path: .cache/repos + key: repo-meta-${{ github.run_id }} + restore-keys: repo-meta- - name: Make awesome-go.com run: go run . + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Setup node uses: actions/setup-node@v6 with: diff --git a/.gitignore b/.gitignore index c43b1528..5d92202b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ out/ awesome-go +.cache/ # Folders .idea diff --git a/main.go b/main.go index 5d65e75c..77ad9bc3 100644 --- a/main.go +++ b/main.go @@ -4,13 +4,19 @@ package main import ( "bytes" "embed" + "encoding/json" "errors" "fmt" template2 "html/template" + "io" + "log" + "net/http" "net/url" "os" "path/filepath" + "strings" "text/template" + "time" "github.com/avelino/awesome-go/pkg/markdown" cp "github.com/otiai10/copy" @@ -24,6 +30,7 @@ type Link struct { Title string URL string Description string + ProjectSlug string // non-empty if project page exists β†’ internal link } // Category describe link category @@ -34,6 +41,40 @@ type Category struct { Links []Link } +// RepoMeta holds metadata fetched from GitHub/GitLab API +type RepoMeta struct { + Stars int `json:"stars"` + Forks int `json:"forks"` + License string `json:"license"` + Language string `json:"language"` + Topics []string `json:"topics"` + LastPush string `json:"last_push"` + OpenIssues int `json:"open_issues"` + Archived bool `json:"archived"` + FetchedAt string `json:"fetched_at"` +} + +// Project represents an individual project page +type Project struct { + Title string + URL string + Description string + Slug string + Host string // "github" or "gitlab" + Owner string + Repo string + Meta *RepoMeta + CategoryTitle string + CategorySlug string + Related []Link +} + +// SitemapData holds data for sitemap generation +type SitemapData struct { + Categories map[string]Category + Projects []*Project +} + // Source files const readmePath = "README.md" @@ -47,7 +88,18 @@ var staticFiles = []string{ //go:embed tmpl/*.tmpl.html tmpl/*.tmpl.xml var tplFs embed.FS -var tpl = template.Must(template.ParseFS(tplFs, "tmpl/*.tmpl.html", "tmpl/*.tmpl.xml")) +var tpl = template.Must( + template.New("").Funcs(template.FuncMap{ + "now": func() string { return time.Now().Format("2006-01-02") }, + "jsonEscape": func(s string) string { + b, _ := json.Marshal(s) + if len(b) < 2 { + return "" + } + return string(b[1 : len(b)-1]) + }, + }).ParseFS(tplFs, "tmpl/*.tmpl.html", "tmpl/*.tmpl.xml"), +) // Output files const outDir = "out/" // NOTE: trailing slash is required @@ -85,15 +137,24 @@ func buildStaticSite() error { return fmt.Errorf("extract categories: %w", err) } + projects := buildProjects(categories) + if err := fetchProjectMeta(projects); err != nil { + return fmt.Errorf("fetch project metadata: %w", err) + } + if err := renderCategories(categories); err != nil { return fmt.Errorf("render categories: %w", err) } + if err := renderProjects(projects); err != nil { + return fmt.Errorf("render projects: %w", err) + } + if err := rewriteLinksInIndex(doc, categories); err != nil { return fmt.Errorf("rewrite links in index: %w", err) } - if err := renderSitemap(categories); err != nil { + if err := renderSitemap(categories, projects); err != nil { return fmt.Errorf("render sitemap: %w", err) } @@ -179,7 +240,7 @@ func renderCategories(categories map[string]Category) error { return nil } -func renderSitemap(categories map[string]Category) error { +func renderSitemap(categories map[string]Category, projects []*Project) error { f, err := os.Create(outSitemapFile) if err != nil { return fmt.Errorf("create sitemap file `%s`: %w", outSitemapFile, err) @@ -187,7 +248,12 @@ func renderSitemap(categories map[string]Category) error { fmt.Printf("Render Sitemap to: %s\n", outSitemapFile) - if err := tpl.Lookup("sitemap.tmpl.xml").Execute(f, categories); err != nil { + data := SitemapData{ + Categories: categories, + Projects: projects, + } + + if err := tpl.Lookup("sitemap.tmpl.xml").Execute(f, data); err != nil { return fmt.Errorf("render sitemap: %w", err) } @@ -198,9 +264,14 @@ func extractCategories(doc *goquery.Document) (map[string]Category, error) { categories := make(map[string]Category) var rootErr error - doc. - Find("body #contents"). - NextFiltered("ul"). + contentsHeading := doc.Find("body #contents") + // Support both direct