{"id":13770413,"url":"https://github.com/pitr/gig","last_synced_at":"2025-10-31T10:10:42.413Z","repository":{"id":43253317,"uuid":"271628444","full_name":"pitr/gig","owner":"pitr","description":"Gemini framework","archived":false,"fork":false,"pushed_at":"2022-07-09T21:15:36.000Z","size":575,"stargazers_count":64,"open_issues_count":4,"forks_count":5,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-04-14T12:05:47.087Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pitr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-06-11T19:16:14.000Z","updated_at":"2025-02-17T16:36:29.000Z","dependencies_parsed_at":"2022-07-15T17:00:29.999Z","dependency_job_id":null,"html_url":"https://github.com/pitr/gig","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pitr%2Fgig","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pitr%2Fgig/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pitr%2Fgig/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pitr%2Fgig/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pitr","download_url":"https://codeload.github.com/pitr/gig/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248877985,"owners_count":21176243,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-03T17:00:37.195Z","updated_at":"2025-10-09T22:07:51.302Z","avatar_url":"https://github.com/pitr.png","language":"Go","funding_links":[],"categories":["Programming"],"sub_categories":["Graphical"],"readme":"# Gig - Gemini framework\n\n[![Used By](https://img.shields.io/badge/used%20by-5%2B%20projects-brightgreen)](#who-uses-gig)\n[![godocs.io](https://godocs.io/github.com/pitr/gig?status.svg)](https://godocs.io/github.com/pitr/gig)\n[![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://pkg.go.dev/github.com/pitr/gig)\n[![Go Report Card](https://goreportcard.com/badge/github.com/pitr/gig?style=flat-square)](https://goreportcard.com/report/github.com/pitr/gig)\n[![Codecov](https://img.shields.io/codecov/c/github/pitr/gig.svg?style=flat-square)](https://codecov.io/gh/pitr/gig)\n[![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/pitr/gig/master/LICENSE)\n\nAPI is subject to change until v1.0\n\n## Protocol compatibility\n\n| Version | Supported Gemini version |\n| ------- | ------------------------ |\n| 0.9.4   | v0.14.*                  |\n| \u003c 0.9.4 | v0.13.*                  |\n\n## Contents\n\n* [Feature Overview](#feature-overview)\n* [Guide](#guide)\n   * [Quick Start](#quick-start)\n   * [Parameters in path](#parameters-in-path)\n   * [Query](#query)\n   * [Client Certificate](#client-certificate)\n   * [Grouping routes](#grouping-routes)\n   * [Blank Gig without middleware by default](#blank-gig-without-middleware-by-default)\n   * [Using middleware](#using-middleware)\n   * [Writing logs to file](#writing-logs-to-file)\n   * [Custom Log Format](#custom-log-format)\n   * [Serving static files](#serving-static-files)\n   * [Serving data from file](#serving-data-from-file)\n   * [Serving data from reader](#serving-data-from-reader)\n   * [Templates](#templates)\n   * [Redirects](#redirects)\n   * [Subdomains](#subdomains)\n   * [Username/password authentication middleware](#usernamepassword-authentication-middleware)\n   * [Custom middleware](#custom-middleware)\n   * [Custom port](#custom-port)\n   * [Custom TLS config](#custom-tls-config)\n   * [Testing](#testing)\n* [Who uses Gig](#who-uses-gig)\n* [Benchmarks](#benchmarks)\n* [Contribute](#contribute)\n* [License](#license)\n\n## Feature Overview\n\n- Client certificate suppport (access `x509.Certificate` directly from context)\n- Highly optimized router with zero dynamic memory allocation which smartly prioritizes routes\n- Group APIs\n- Extensible middleware framework\n- Define middleware at root, group or route level\n- Handy functions to send variety of Gemini responses\n- Centralized error handling\n- Template rendering with any template engine\n- Define your format for the logger\n- Highly customizable\n\n## Guide\n\n### Quick Start\n\n```go\npackage main\n\nimport \"github.com/pitr/gig\"\n\nfunc main() {\n  // Gig instance\n  g := gig.Default()\n\n  // Routes\n  g.Handle(\"/\", func(c gig.Context) error {\n    return c.Gemini(\"# Hello, World!\")\n  })\n\n  // Start server on PORT or default port\n  g.Run(\"my.crt\", \"my.key\")\n}\n```\n```bash\n$ go run main.go\n```\n\n### Parameters in path\n\n```go\npackage main\n\nimport \"github.com/pitr/gig\"\n\nfunc main() {\n  g := gig.Default()\n\n  g.Handle(\"/user/:name\", func(c gig.Context) error {\n    return c.Gemini(\"# Hello, %s!\", c.Param(\"name\"))\n  })\n\n  g.Run(\"my.crt\", \"my.key\")\n}\n```\n\n### Query\n\n```go\npackage main\n\nimport \"github.com/pitr/gig\"\n\nfunc main() {\n  g := gig.Default()\n\n  g.Handle(\"/user\", func(c gig.Context) error {\n    query, err := c.QueryString()\n    if err != nil {\n      return err\n    }\n    return c.Gemini(\"# Hello, %s!\", query)\n  })\n\n  g.Run(\"my.crt\", \"my.key\")\n}\n```\n\n### Client Certificate\n\n```go\npackage main\n\nimport \"github.com/pitr/gig\"\n\nfunc main() {\n  g := gig.Default()\n\n  g.Handle(\"/user\", func(c gig.Context) error {\n    cert := c.Certificate()\n    if cert == nil {\n      return c.NoContent(gig.StatusClientCertificateRequired, \"We need a certificate\")\n    }\n    return c.Gemini(\"# Hello, %s!\", cert.Subject.CommonName)\n  })\n\n  // OR using middleware\n\n  g.Handle(\"/user\", func(c gig.Context) error {\n    return c.Gemini(\"# Hello, %s!\", c.Get(\"subject\"))\n  }, gig.CertAuth(gig.ValidateHasCertificate))\n\n  g.Run(\"my.crt\", \"my.key\")\n}\n```\n\n### Grouping routes\n```go\nfunc main() {\n  g := gig.Default()\n\n  // Simple group: v1\n  v1 := g.Group(\"/v1\")\n  {\n    v1.Handle(\"/page1\", page1Endpoint)\n    v1.Handle(\"/page2\", page2Endpoint)\n  }\n\n  // Simple group: v2\n  v2 := g.Group(\"/v2\")\n  {\n    v2.Handle(\"/page1\", page1Endpoint)\n    v2.Handle(\"/page2\", page2Endpoint)\n  }\n\n  g.Run(\"my.crt\", \"my.key\")\n}\n```\n\n### Blank Gig without middleware by default\nUse\n```go\ng := gig.New()\n```\ninstead of\n```go\n// Default With the Logger and Recovery middleware already attached\ng := gig.Default()\n```\n\n### Using middleware\n```go\nfunc main() {\n  // Creates a router without any middleware by default\n  g := gig.New()\n\n  // Global middleware\n  // Logger middleware will write the logs to gig.DefaultWriter.\n  // By default gig.DefaultWriter = os.Stdout\n  g.Use(gig.Logger())\n\n  // Recovery middleware recovers from any panics and return StatusPermanentFailure.\n  g.Use(gig.Recovery())\n\n  // Private group\n  // same as private := g.Group(\"/private\", gig.CertAuth(gig.ValidateHasCertificate))\n  private := g.Group(\"/private\")\n  private.Use(gig.CertAuth(gig.ValidateHasCertificate))\n  {\n    private.Handle(\"/user\", userEndpoint)\n  }\n\n  g.Run(\"my.crt\", \"my.key\")\n}\n```\n\n### Writing logs to file\n```go\nfunc main() {\n  f, _ := os.Create(\"access.log\")\n  gig.DefaultWriter = io.MultiWriter(f)\n\n  // Use the following code if you need to write the logs to file and console at the same time.\n  // gig.DefaultWriter = io.MultiWriter(f, os.Stdout)\n\n  g := gig.Default()\n\n  g.Handle(\"/\", func(c gig.Context) error {\n      return c.Gemini(\"# Hello, World!\")\n  })\n\n  g.Run(\"my.crt\", \"my.key\")\n}\n```\n\n### Custom Log Format\n```go\nfunc main() {\n  g := gig.New()\n\n  // See LoggerConfig documentation for format\n  g.Use(gig.LoggerWithConfig(gig.LoggerConfig{Format: \"${remote_ip} ${status}\"}))\n\n  g.Handle(\"/\", func(c gig.Context) error {\n      return c.Gemini(\"# Hello, World!\")\n  })\n\n  g.Run(\"my.crt\", \"my.key\")\n}\n```\n\n### Serving static files\n```go\nfunc main() {\n  g := gig.Default()\n\n  // Register /images/* to serve files in my_images/ folder.\n  // Requests to /images/ will show directory listing.\n  g.Static(\"/images\", \"my_images\")\n\n  g.File(\"/robots.txt\", \"files/robots.txt\")\n\n  g.Run(\"my.crt\", \"my.key\")\n}\n```\n\n### Serving data from file\n```go\nfunc main() {\n  g := gig.Default()\n\n  g.Handle(\"/robots.txt\", func(c gig.Context) error {\n      return c.File(\"robots.txt\")\n  })\n\n  g.Run(\"my.crt\", \"my.key\")\n}\n```\n\n### Serving data from reader\n```go\nfunc main() {\n  g := gig.Default()\n\n  g.Handle(\"/data\", func(c gig.Context) error {\n    response, err := http.Get(\"https://google.com/\")\n    if err != nil || response.StatusCode != http.StatusOK {\n      return c.NoContent(gig.StatusProxyError, \"could not fetch google\")\n    }\n\n    return c.Stream(\"text/html\", response.Body)\n  })\n\n  g.Run(\"my.crt\", \"my.key\")\n}\n```\n\n### Templates\n\nSet `Gig.Renderer` to something that responds to `Render(io.Writer, string, interface{}, gig.Context) error`.\n\nUse any templating library, such as `text/template`, [https://github.com/valyala/quicktemplate](https://github.com/valyala/quicktemplate), etc. The following example uses `text/template`:\n\n```go\nimport (\n  \"text/template\"\n\n  \"github.com/pitr/gig\"\n)\n\ntype Template struct {\n  templates *template.Template\n}\n\nfunc (t *Template) Render(w io.Writer, name string, data interface{}, c gig.Context) error {\n  // Execute named template with data\n  return t.templates.ExecuteTemplate(w, name, data)\n}\n\nfunc main() {\n  g := gig.Default()\n\n  // Register renderer\n  g.Renderer = \u0026Template{template.Must(template.ParseGlob(\"public/views/*.gmi\"))}\n\n  g.Handle(\"/user/:name\", func(c gig.Context) error {\n    // Render template \"user\" with username passed as data.\n    return c.Render(\"user\", c.Param(\"name\"))\n  })\n\n  g.Run(\"my.crt\", \"my.key\")\n}\n```\n\nConsider bundling assets with the binary by using [go:ember](https://tip.golang.org/pkg/embed/), [go-assets](https://github.com/jessevdk/go-assets) or similar.\n\n### Redirects\n```go\nfunc main() {\n  g := gig.Default()\n\n  g.Handle(\"/old\", func(c gig.Context) error {\n    return c.NoContent(gig.StatusRedirectPermanent, \"/new\")\n  })\n\n  g.Run(\"my.crt\", \"my.key\")\n}\n```\n\n### Subdomains\n\n```go\nfunc main() {\n  apps := map[string]*gig.Gig{}\n\n  // App A\n  a := gig.Default()\n  apps[\"app-a.example.com\"] = a\n\n  a.Handle(\"/\", func(c gig.Context) error {\n      return c.Gemini(\"I am App A\")\n  })\n\n  // App B\n  b := gig.Default()\n  apps[\"app-b.example.com\"] = b\n\n  b.Handle(\"/\", func(c gig.Context) error {\n      return c.Gemini(\"I am App B\")\n  })\n\n  // Server (without default middleware to prevent double logging)\n  g := gig.New()\n  g.Handle(\"/*\", func(c gig.Context) error {\n      app := apps[c.URL().Host]\n\n      if app == nil {\n          return gig.ErrNotFound\n      }\n\n      app.ServeGemini(c)\n      return nil\n  })\n\n  g.Run(\"my.crt\", \"my.key\") // must be wildcard SSL certificate for *.example.com\n}\n```\n\n### Username/password authentication middleware\n\nStatus: EXPERIMENTAL\n\n`PassAuth` middleware ensures that request has a client certificate, validates its fingerprint using function passed to middleware. If authentication is required, this function should return a path where user should be redirect to.\n\nLogin handlers are setup using `PassAuthLoginHandle` function, which collects username and password, and passes them to the provided function. That function should return an error if login failed, or absolute path to redirect user to.\n\nUser registration is expected to be implemented by developer.\n\nThe example assumes that there is a `db` module that does user management.\n\n```go\nfunc main() {\n  g := gig.Default()\n\n  secret := g.Group(\"/secret\", gig.PassAuth(func(sig string, c gig.Context) (string, error) {\n    ok, err := db.CheckValid(sig)\n    if err != nil {\n      return \"/login\", err\n    }\n    if !ok {\n      return \"/login\", nil\n    }\n    return \"\", nil\n  }))\n  // secret.Handle(\"/page\", func(c gig.Context) {...})\n\n  g.PassAuthLoginHandle(\"/login\", func(user, pass, sig string, c Context) (string, error) {\n    // check user/pass combo, and activate cert signature if valid\n    err := db.Login(user, pass, sig)\n    if err != nil {\n      return \"\", err\n    }\n    return \"/secret/page\", nil\n  })\n\n  g.Run(\"my.crt\", \"my.key\")\n}\n```\n\n### Custom middleware\n```go\nfunc MyMiddleware(next gig.HandlerFunc) gig.HandlerFunc {\n  return func(c gig.Context) error {\n    // Set example variable\n    c.Set(\"example\", \"123\")\n\n    if err := next(c); err != nil {\n      c.Error(err)\n    }\n\n    // Do something after request is done\n    // ...\n\n    return err\n  }\n}\n\nfunc main() {\n  g := gig.Default()\n  g.Use(MyMiddleware)\n\n  g.Handle(\"/\", func(c gig.Context) error {\n    return c.Gemini(\"# Example %s\", c.Get(\"example\"))\n  })\n\n  g.Run(\"my.crt\", \"my.key\")\n}\n```\n\n### Custom port\n\nUse `PORT` environment variable:\n\n```\nPORT=12345 ./myapp\n```\n\nAlternatively, pass it to Run:\n\n```go\nfunc main() {\n  g := gig.Default()\n\n  g.Handle(\"/\", func(c gig.Context) error {\n    return c.Gemini(\"# Hello world\")\n  })\n\n  g.Run(\":12345\", \"my.crt\", \"my.key\")\n}\n```\n\n### Custom TLS config\n```go\nfunc main() {\n  g := gig.Default()\n  g.TLSConfig.MinVersion = tls.VersionTLS13\n\n  g.Handle(\"/\", func(c gig.Context) error {\n    return c.Gemini(\"# Hello world\")\n  })\n\n  g.Run(\"my.crt\", \"my.key\")\n}\n```\n\n### Testing\n```go\nfunc setupServer() *gig.Gig {\n  g := gig.Default()\n\n  g.Handle(\"/private\", func(c gig.Context) error {\n    return c.Gemini(\"Hello %s\", c.Get(\"subject\"))\n  }, gig.CertAuth(gig.ValidateHasCertificate))\n\n  return g\n}\n\nfunc TestServer(t *testing.T) {\n  g := setupServer()\n  c, res := g.NewFakeContext(\"/private\", nil)\n\n  g.ServeGemini(c)\n\n  if res.Written != \"60 Client Certificate Required\\r\\n\" {\n    t.Fail()\n  }\n}\n\nfunc TestCertificate(t *testing.T) {\n  g := setupServer()\n  c, res := g.NewFakeContext(\"/\", \u0026tls.ConnectionState{\n    PeerCertificates: []*x509.Certificate{\n      {Subject: pkix.Name{CommonName: \"john\"}},\n    },\n  })\n\n  g.ServeGemini(c)\n\n  if resp.Written != \"20 text/gemini\\r\\nHello john\" {\n    t.Fail()\n  }\n}\n```\n\n## Who uses Gig\n\nGig is used by the following capsules:\n\n- [gemif.fedi.farm](https://portal.mozz.us/gemini/gemif.fedi.farm) - GemIf, Interactive Fiction engine\n- [geddit.glv.one](https://portal.mozz.us/gemini/geddit.glv.one) - Link aggregator\n- [wp.glv.one](https://portal.mozz.us/gemini/wp.glv.one) - Wikipedia proxy\n- [egsam.glv.one](https://portal.mozz.us/gemini/egsam.glv.one) - Egsam, client torture test\n- [paste.gemigrep.com](https://portal.mozz.us/gemini/paste.gemigrep.com) - Paste service\n- [gemini.tunerapp.org](https://portal.mozz.us/gemini/gemini.tunerapp.org) - Internet Radio Stations Directory\n- [pon.ix.tc](https://portal.mozz.us/gemini/pon.ix.tc) - YouTube Proxy and other utiltiies\n- [gemfic.xyz](https://gemfic.xyz) - Fiction hub (Offprint proxy)\n\nIf you use Gig, open a PR to add your capsule to this list.\n\n## Benchmarks\n\n| Benchmark name                 |      (1) |           (2) |        (3) |            (4) |\n| ------------------------------ | --------:| -------------:| ----------:| --------------:|\n| BenchmarkRouterStaticRoutes    |   104677 |   11105 ns/op |     0 B/op |    0 allocs/op |\n| BenchmarkRouterGitHubAPI       |    50859 |   22973 ns/op |     0 B/op |    0 allocs/op |\n| BenchmarkRouterParseAPI        |   302828 |    3717 ns/op |     0 B/op |    0 allocs/op |\n| BenchmarkRouterGooglePlusAPI   |   185558 |    6136 ns/op |     0 B/op |    0 allocs/op |\n\nGenerated using `make bench` in [router_test.go](https://github.com/pitr/gig/blob/master/router_test.go). APIs are based on [Go HTTP Router Benchmark repository](https://github.com/gin-gonic/go-http-routing-benchmark) and adapted to Gemini protocol, eg. verbs\nGET/POST/etc are ignored since Gemini does not support them.\n\n- (1): Total Repetitions achieved in constant time, higher means more confident result\n- (2): Single Repetition Duration (ns/op), lower is better\n- (3): Heap Memory (B/op), lower is better\n- (4): Average Allocations per Repetition (allocs/op), lower is better\n\n## Contribute\n\nIf something is missing, please open an issue. If possible, send a PR.\n\n## License\n\n[MIT](https://github.com/pitr/gig/blob/master/LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpitr%2Fgig","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpitr%2Fgig","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpitr%2Fgig/lists"}