{"id":29117215,"url":"https://github.com/yaitoo/xun","last_synced_at":"2026-04-25T03:48:36.427Z","repository":{"id":268848383,"uuid":"846335172","full_name":"yaitoo/xun","owner":"yaitoo","description":"Xun is a web framework built on Go's built-in html/template and net/http package’s router (1.22).","archived":false,"fork":false,"pushed_at":"2025-05-12T15:38:44.000Z","size":245,"stargazers_count":84,"open_issues_count":1,"forks_count":3,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-12T16:49:07.321Z","etag":null,"topics":["alpinejs","golang","htmx","mux","mvc","page-router","restful-api","ssr","taildwindcss","web"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yaitoo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-08-23T01:53:59.000Z","updated_at":"2025-05-12T15:38:47.000Z","dependencies_parsed_at":"2024-12-19T09:28:19.551Z","dependency_job_id":"1292b21b-e70f-4088-9b0c-deda80f57c50","html_url":"https://github.com/yaitoo/xun","commit_stats":null,"previous_names":["yaitoo/htmx","yaitoo/xun"],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/yaitoo/xun","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yaitoo%2Fxun","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yaitoo%2Fxun/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yaitoo%2Fxun/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yaitoo%2Fxun/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yaitoo","download_url":"https://codeload.github.com/yaitoo/xun/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yaitoo%2Fxun/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262581514,"owners_count":23331925,"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":["alpinejs","golang","htmx","mux","mvc","page-router","restful-api","ssr","taildwindcss","web"],"created_at":"2025-06-29T11:14:17.007Z","updated_at":"2025-12-24T22:27:42.719Z","avatar_url":"https://github.com/yaitoo.png","language":"Go","funding_links":[],"categories":["Web Frameworks","Web框架"],"sub_categories":["Utility/Miscellaneous","实用程序/Miscellaneous"],"readme":"# Xun\nXun is a web framework built on Go's built-in html/template and net/http package’s router. It is designed to be lightweight, fast, and easy to use. Xun provides a simple and intuitive API for building web applications, while also offering advanced features such as middleware, routing, and template rendering.\n\nXun [ʃʊn] (pronounced 'shoon'), derived from the Chinese character 迅, signifies being lightweight and fast.\n\n[![Tests](https://github.com/yaitoo/xun/actions/workflows/tests.yml/badge.svg)](https://github.com/yaitoo/xun/actions/workflows/tests.yml)\n[![Codecov](https://codecov.io/gh/yaitoo/xun/branch/main/graph/badge.svg)](https://codecov.io/gh/yaitoo/xun)\n[![Go Report Card](https://goreportcard.com/badge/github.com/yaitoo/xun)](https://goreportcard.com/report/github.com/yaitoo/xun)\n[![Go Reference](https://pkg.go.dev/badge/github.com/yaitoo/xun.svg)](https://pkg.go.dev/github.com/yaitoo/xun)\n[![GitHub Release](https://img.shields.io/github/v/release/yaitoo/xun)](https://github.com/yaitoo/xun/releases)\n[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)\n[![PRs welcome](https://img.shields.io/badge/PRs-welcome-blue.svg)](https://github.com/yaitoo/xun/compare)\n\n## Features\n- Works with Go's built-in `net/http.ServeMux` router that was introduced in 1.22. [Routing Enhancements for Go 1.22](https://go.dev/blog/routing-enhancements).\n- Works with Go's built-in `html/template`. It is built-in support for Server-Side Rendering (SSR).\n- Built-in response compression support for `gzip` and `deflate`. \n- Built-in Form and Validate feature with i18n support.\n- Built-in `AutoTLS` feature. It automatic SSL certificate issuance and renewal through Let's Encrypt and other ACME-based CAs\n- Support Page Router in `StaticViewEngine` and `HtmlViewEngine`.\n- Support multiple viewers by ViewEngines: `StaticViewEngine`, `JsonViewEngine` and `HtmlViewEngine`. You can feel free to add custom view engine, eg `XmlViewEngine`.\n- Support to reload changed static files automatically in development environment.\n\n\n\n## Getting Started\n\u003e See full source code on [xun-examples](https://github.com/yaitoo/xun-examples)\n\n### Install Xun\n- install latest commit from `main` branch\n```\ngo get github.com/yaitoo/xun@main\n```\n\n- install latest release\n```\ngo get github.com/yaitoo/xun@latest\n```\n\n### Project structure\n`Xun` has some specified directories that is used to organize code, routing and static assets.\n- `public`: Static assets to be served. \n- `components` A partial view that is shared between layouts/pages/views.\n- `views`: An internal page view that can be referenced in `context.View` to render different UI for current routing.\n- `layouts`: A layout is shared between multiple pages/views\n- `pages`: A public page view that will create public page routing automatically.\n- `text`: An internal text view that can be referenced in `context.View` to render with a data model.\n\n**NOTE: All html files(component,layout, view and page) will be parsed by [html/template](https://pkg.go.dev/html/template). You can feel free to use all built-in [Actions,Pipelines and Functions](https://pkg.go.dev/text/template), and your custom functions that is registered in `HtmlViewEngine`.**\n\n### Layouts and Pages\n`Xun` uses file-system based routing, meaning you can use folders and files to define routes. This section will guide you through how to create layouts and pages, and link between them.\n\n\n#### Creating a page\nA page is UI that is rendered on a specific route. To create a page, add a page file(.html) inside the `pages` directory. For example, to create an index page (`/`):\n```\n└── app\n    └── pages\n        └── index.html\n```\n\n\u003e index.html\n``` html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003cmeta charset=\"utf-8\"\u003e\n    \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\"\u003e\n    \u003ctitle\u003eXun-Admin\u003c/title\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003cdiv id=\"app\"\u003ehello world\u003c/div\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n#### Creating a layout\nA layout is UI that is shared between multiple pages/views. \n\nYou can create a layout(.html) file inside the `layouts` directory.\n```\n└── app\n    ├── layouts\n    │   └── home.html\n    └── pages\n        └── index.html\n```\n\n\u003e layouts/home.html\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003cmeta charset=\"utf-8\"\u003e\n    \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\"\u003e\n    \u003ctitle\u003eXun-Admin\u003c/title\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    {{ block \"content\" .}} {{ end }}\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\u003e pages/index.html\n```html\n\u003c!--layout:home--\u003e\n{{ define \"content\" }}\n    \u003cdiv id=\"app\"\u003ehello world\u003c/div\u003e\n{{ end }}\n```\n\n### Static assets\nYou can store static files, like images, fonts, js and css, under a directory called `public` in the root directory. Files inside public can then be referenced by your code starting from the base URL (/).\n\n**NOTE: `public/index.html` will be exposed by `/` instead of `/index.html`.**\n\n#### Creating a component\nA component is a partial view that is shared between multiple layouts/pages/views. \n\n```\n└── app\n    ├── components\n    │   └── assets.html\n    ├── layouts\n    │   └── home.html\n    ├── pages\n    │   └── index.html\n    └── public\n        ├── app.js\n        └── skin.css\n```      \n\u003e components/assets.html\n```html\n\u003clink rel=\"stylesheet\" href=\"/skin.css\"\u003e\n\u003cscript type=\"text/javascript\" src=\"/app.js\"\u003e\u003c/script\u003e\n```\n\u003e layouts/home.html\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003cmeta charset=\"utf-8\"\u003e\n    \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\"\u003e\n    \u003ctitle\u003eXun-Admin\u003c/title\u003e\n    {{ block \"components/assets\" . }} {{ end }}\n  \u003c/head\u003e\n  \u003cbody\u003e\n    {{ block \"content\" .}} {{ end }}\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n### Text View\nA text view is UI that is referenced in `context.View` to render the view with a data model.\n\n**NOTE: Text files are parsed using the `text/template` package. This is different from the `html/template` package used in `pages/layouts/views/components`. While `text/template` is designed for generating textual output based on data, it does not automatically secure HTML output against certain attacks. Therefore, please ensure your output is safe to prevent code injection.**\n\n#### Creating a text view\n```\n└── app\n    ├── components\n    │   └── assets.html\n    ├── layouts\n    │   └── home.html\n    ├── pages\n    │   └── index.html\n    └── public\n    │   ├── app.js\n    │   └── skin.css\n    └── text\n        ├── sitemap.xml\n```\n\n#### Render the view with a data model\n```go\n\tapp.Get(\"/sitemap.xml\", func(c *xun.Context) error {\n\t\treturn c.View(Sitemap{\n\t\t\tLastMod: time.Now(),\n\t\t}, \"text/sitemap.xml\") // use `text/sitemap.xml` as current Viewer to render\n\t})\n```\n\n\u003e curl --header \"Accept: application/xml, text/xml,text/plain, */*\" -v http://127.0.0.1/sitemap.xml\n\n```bash\n*   Trying 127.0.0.1:80...\n* Connected to 127.0.0.1 (127.0.0.1) port 80\n\u003e GET /sitemap.xml HTTP/1.1\n\u003e Host: 127.0.0.1\n\u003e User-Agent: curl/8.7.1\n\u003e Accept: application/xml, text/xml,text/plain, */*\n\u003e\n* Request completely sent off\n\u003c HTTP/1.1 200 OK\n\u003c Date: Wed, 15 Jan 2025 11:51:56 GMT\n\u003c Content-Length: 277\n\u003c Content-Type: text/xml; charset=utf-8\n\u003c\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003curlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\u003e\n  \u003curl\u003e\n  \u003cloc\u003ehttps://github.com/yaitoo/xun\u003c/loc\u003e\n  \u003clastmod\u003e2025-01-15T19:51:56+08:00\u003c/lastmod\u003e\n  \u003cchangefreq\u003ehourly\u003c/changefreq\u003e\n  \u003cpriority\u003e1.0\u003c/priority\u003e\n  \u003c/url\u003e\n* Connection #0 to host 127.0.0.1 left intact\n\u003c/urlset\u003e%\n```\n\n\n\n## Building your application\n### Routing\n#### Route Handler\nPage Router only serve static content from html files. We have to define router handler in go to process request and bind data to the template file via `HtmlViewer`. \n\n\u003e pages/index.html\n```html\n\u003c!--layout:home--\u003e\n{{ define \"content\" }}\n    \u003cdiv id=\"app\"\u003ehello {{.Data.Name}}\u003c/div\u003e\n{{ end }}\n```\n\n\u003e main.go\n```go\n\tapp.Get(\"/{$}\", func(c *xun.Context) error {\n\t\treturn c.View(map[string]string{\n\t\t\t\"Name\": \"go-xun\",\n\t\t})\n\t})\n```\n\n\n**NOTE: An `/index.html` always be registered as `/{$}` in routing table. See more detail on [Routing Enhancements for Go 1.22](https://go.dev/blog/routing-enhancements).**\n\u003e There is one last bit of syntax. As we showed above, patterns ending in a slash, like /posts/, match all paths beginning with that string. To match only the path with the trailing slash, you can write /posts/{$}. That will match /posts/ but not /posts or /posts/234.\n\n#### Dynamic Routes\nWhen you don't know the exact segment names ahead of time and want to create routes from dynamic data, you can use Dynamic Segments that are filled in at request time. `{var}` can be used in folder name and file name as same as router handler in `http.ServeMux`. \n\nFor examples, below patterns will be generated automatically, and registered in routing table.\n- `/user/{id}.html` generates pattern `/user/{id}` \n- `/{id}/user.html` generates pattern `/{id}/user`\n\n```\n├── app\n│   ├── components\n│   │   └── assets.html\n│   ├── layouts\n│   │   └── home.html\n│   ├── pages\n│   │   ├── index.html\n│   │   └── user\n│   │       └── {id}.html\n│   └── public\n│       ├── app.js\n│       └── skin.css\n├── go.mod\n├── go.sum\n└── main.go\n```\n\n\u003e pages/user/{id}.html\n```html\n\u003c!--layout:home--\u003e\n{{ define \"content\" }}\n    \u003cdiv id=\"app\"\u003ehello {{.Data.Name}}\u003c/div\u003e\n{{ end }}\n```\n\n\u003e main.go\n```go\n\tapp.Get(\"/user/{id}\", func(c *xun.Context) error {\n\t\tid := c.Request.PathValue(\"id\")\n\t\tuser := getUserById(id)\n\t\treturn c.View(user)\n\t})\n```\n\n\n### Multiple Viewers\nIn our application, a route can support multiple viewers. The response is rendered based on the `Accept` request header. If no viewer matches the `Accept` header, first registered viewer is used. For more examples, see the [Tests](app_test.go).\n\n```bash\ncurl -v http://127.0.0.1\n\u003e GET / HTTP/1.1\n\u003e Host: 127.0.0.1\n\u003e User-Agent: curl/8.7.1\n\u003e Accept: */*\n\u003e\n* Request completely sent off\n\u003c HTTP/1.1 200 OK\n\u003c Date: Thu, 26 Dec 2024 07:46:13 GMT\n\u003c Content-Length: 19\n\u003c Content-Type: text/plain; charset=utf-8\n\u003c\n{\"Name\":\"go-xun\"}\n```\n\n\u003e curl --header \"Accept: text/html; \\*/\\*\" http://127.0.0.1\n```\n\u003e GET / HTTP/1.1\n\u003e Host: 127.0.0.1\n\u003e User-Agent: curl/8.7.1\n\u003e Accept: text/html; */*\n\u003e\n* Request completely sent off\n\u003c HTTP/1.1 200 OK\n\u003c Date: Thu, 26 Dec 2024 07:49:47 GMT\n\u003c Content-Length: 343\n\u003c Content-Type: text/html; charset=utf-8\n\u003c\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003cmeta charset=\"utf-8\"\u003e\n    \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\"\u003e\n    \u003ctitle\u003eXun-Admin\u003c/title\u003e\n    \u003clink rel=\"stylesheet\" href=\"/skin.css\"\u003e\n\u003cscript type=\"text/javascript\" src=\"/app.js\"\u003e\u003c/script\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n\n    \u003cdiv id=\"app\"\u003ehello go-xun\u003c/div\u003e\n\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n### Middleware\nMiddleware allows you to run code before a request is completed. Then, based on the incoming request, you can modify the response by rewriting, redirecting, modifying the request or response headers, or responding directly.\n\nIntegrating Middleware into your application can lead to significant improvements in performance, security, and user experience. Some common scenarios where Middleware is particularly effective include:\n\n- Authentication and Authorization: Ensure user identity and check session cookies before granting access to specific pages or API routes.\n- Server-Side Redirects: Redirect users at the server level based on certain conditions (e.g., locale, user role).\n- Path Rewriting: Support A/B testing, feature rollout, or legacy paths by dynamically rewriting paths to API routes or pages based on request properties.\n- Bot Detection: Protect your resources by detecting and blocking bot traffic.\n- Logging and Analytics: Capture and analyze request data for insights before processing by the page or API.\n- Feature Flagging: Enable or disable features dynamically for seamless feature rollout or testing.\n\n\u003e Authentication\n```go\n\tadmin := app.Group(\"/admin\")\n\n\tadmin.Use(func(next xun.HandleFunc) xun.HandleFunc {\n\t\treturn func(c *xun.Context) error {\n\t\t\ttoken := c.Request.Header.Get(\"X-Token\")\n\t\t\tif !checkToken(token) {\n\t\t\t\tc.WriteStatus(http.StatusUnauthorized)\n\t\t\t\treturn xun.ErrCancelled\n\t\t\t}\n\t\t\treturn next(c)\n\t\t}\n\t})\n\n```\n\n\u003e Logging\n```go\n\tapp.Use(func(next xun.HandleFunc) xun.HandleFunc {\n\t\treturn func(c *xun.Context) error {\n\t\t\tn := time.Now()\n\t\t\tdefer func() {\n\t\t\t\tduration := time.Since(n)\n\n\t\t\t\tlog.Println(c.Routing.Pattern, duration)\n\t\t\t}()\n\t\t\treturn next(c)\n\t\t}\n\t})\n```\n\n### Multiple VirtualHosts\n`net/http` package's router supports multiple host names that resolve to a single address by precedence rule. \nFor examples\n```go\n mux.HandleFunc(\"GET /\", func(w http.ResponseWriter, req *http.Request) {...})\n mux.HandleFunc(\"GET abc.com/\", func(w http.ResponseWriter, req *http.Request) {...})\n mux.HandleFunc(\"GET 123.com/\", func(w http.ResponseWriter, req *http.Request) {...})\n```\n\nIn Page Router, we use `@` in top folder name to setup host rules in routing table. See more examples on [Tests](app_test.go)\n```\n├── app\n│   ├── components\n│   │   └── assets.html\n│   ├── layouts\n│   │   └── home.html\n│   ├── pages\n│   │   ├── @123.com\n│   │   │   └── index.html\n│   │   ├── index.html\n│   │   └── user\n│   │       └── {id}.html\n│   └── public\n│       ├── @abc.com\n│       │   └── index.html\n│       ├── app.js\n│       └── skin.css\n```\n\n### Form and Validate\nIn an api application, we always need to collect data from request, and validate them. It is integrated with i18n feature as built-in feature now.\n\n\u003e check full examples on [Tests](binder_test.go)\n\n\n```go\ntype Login struct {\n\t\tEmail  string `form:\"email\" json:\"email\" validate:\"required,email\"`\n\t\tPasswd string `json:\"passwd\" validate:\"required\"`\n\t}\n```\n\n#### BindQuery\n```go\n\tapp.Get(\"/login\", func(c *Context) error {\n\t\tit, err := form.BindQuery[Login](c.Request)\n\t\tif err != nil {\n\t\t\tc.WriteStatus(http.StatusBadRequest)\n\t\t\treturn ErrCancelled\n\t\t}\n\n\t\tif it.Validate(c.AcceptLanguage()...) \u0026\u0026 it.Data.Email == \"xun@yaitoo.cn\" \u0026\u0026 it.Data.Passwd == \"123\" {\n\t\t\treturn c.View(it)\n\t\t}\n\t\tc.WriteStatus(http.StatusBadRequest)\n\t\treturn ErrCancelled\n\t})\n```\n\n#### BindForm\n```go\napp.Post(\"/login\", func(c *Context) error {\n\t\tit, err := form.BindForm[Login](c.Request)\n\t\tif err != nil {\n\t\t\tc.WriteStatus(http.StatusBadRequest)\n\t\t\treturn ErrCancelled\n\t\t}\n\n\t\tif it.Validate(c.AcceptLanguage()...) \u0026\u0026 it.Data.Email == \"xun@yaitoo.cn\" \u0026\u0026 it.Data.Passwd == \"123\" {\n\t\t\treturn c.View(it)\n\t\t}\n\t\tc.WriteStatus(http.StatusBadRequest)\n\t\treturn ErrCancelled\n\t})\n```\n\n#### BindJson\n```go\napp.Post(\"/login\", func(c *Context) error {\n\t\tit, err := form.BindJson[Login](c.Request)\n\t\tif err != nil {\n\t\t\tc.WriteStatus(http.StatusBadRequest)\n\t\t\treturn ErrCancelled\n\t\t}\n\n\t\tif it.Validate(c.AcceptLanguage()...) \u0026\u0026 it.Data.Email == \"xun@yaitoo.cn\" \u0026\u0026 it.Data.Passwd == \"123\" {\n\t\t\treturn c.View(it)\n\t\t}\n\t\tc.WriteStatus(http.StatusBadRequest)\n\t\treturn ErrCancelled\n\t})\n```\n\n#### Validate Rules\nMany [baked-in validations](https://github.com/go-playground/validator) are ready to use. Please feel free to check [docs](https://github.com/go-playground/validator?tab=readme-ov-file#usage-and-documentation) and write your custom validation methods.\n\n#### i18n\nEnglish is default locale for all validate message. It is easy to add other locale.\n```go\nimport(\n  \"github.com/go-playground/locales/zh\"\n  ut \"github.com/go-playground/universal-translator\"\n  trans \"github.com/go-playground/validator/v10/translations/zh\"\n\n)\n\nxun.AddValidator(ut.New(zh.New()).GetFallback(), trans.RegisterDefaultTranslations)\n```\n\n\u003e check more translations on [here](https://github.com/go-playground/validator/tree/master/translations)\n\n### Extensions\n#### GZip/Deflate handler\nSet up the compression extension to interpret and respond to `Accept-Encoding` headers in client requests, supporting both GZip and Deflate compression methods.\n\n```go\napp := xun.New(WithCompressor(\u0026GzipCompressor{}, \u0026DeflateCompressor{}))\n```\n\n#### AutoTLS\nUse `autotls.Configure` to set up servers for automatic obtaining and renewing of TLS certificates from Let's Encrypt.\n\n```go\nmux := http.NewServeMux()\n\napp := xun.New(xun.WithMux(mux))\n\n//...\n\nhttpServer := \u0026http.Server{\n\tAddr: \":http\",\n\t//...\n}\n\nhttpsServer := \u0026http.Server{\n\tAddr: \":https\",\n\t//...\n}\n\nautotls.\n\tNew(autotls.WithCache(autocert.DirCache(\"./certs\")),\n\t\tautotls.WithHosts(\"abc.com\", \"123.com\")).\n\tConfigure(httpServer, httpsServer)\n\ngo httpServer.ListenAndServe()\ngo httpsServer.ListenAndServeTLS(\"\", \"\")\n```\n\n#### Cookie\nCookies are a way to store information at the client end. see [more examples](./ext/cookie/cookie_test.go)\n\u003e Write cookie with base64(URL Encoding) to client\n```go\ncookie.Set(ctx,  http.Cookie{Name: \"test\", Value: \"value\"}) // Set-Cookie: test=dmFsdWU=\n```\n\n\u003e Read and decoded cookie from client's request\n```go\nv, err := cookie.Get(ctx,\"test\")\n\nfmt.Println(v) // value\n```\n\nWhen signed, the cookies can't be forged, because their values are validated using HMAC. \n```go\nts, err := cookie.SetSigned(ctx,http.Cookie{Name: \"test\", Value: \"value\"},[]byte(\"secret\")) // ts is current timestamp\n\nv, ts, err := cookie.GetSigned(ctx, \"test\",[]byte(\"secret\")) // v is value, ts is the timestamp that was signed\n```\n\n\u003e Delete a cookie \n```go\ncookie.Delete(ctx, http.Cookie{Name: \"test\", Value: \"dmFsdWU=\"}) // Set-Cookie: test=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0\n```\n\n#### HSTS\nHTTP Strict Transport Security (HSTS) is a simple and widely supported standard to protect visitors by ensuring that their browsers always connect to a website over HTTPS.\n\n\n\u003e Redirect redirects plain HTTP requests to HTTPS. **DON'T use it if HTTPs is unsupported on your server.**\n```go\napp.Use(hsts.Redirect())\n```\n\n\u003e Write HSTS header if it is a HTTPs request. **It is only applied in HTTPs request.**\n```go\napp.Use(hsts.WriteHeader())\n```\n\n#### Proxy Protocol\nThe PROXY protocol allows our application to receive client connection information that is passed through proxy servers and load balancers. Both PROXY protocol versions 1 and 2 are supported.\n\n[How to use the Proxy Protocol to preserve a client's ip address?](https://www.haproxy.com/blog/use-the-proxy-protocol-to-preserve-a-clients-ip-address)\n\n**Security Note: Do not enable the PROXY protocol on your servers unless they are located behind a proxy server or load balancer. If the PROXY protocol is enabled without such intermediaries, any client could potentially send fake IP addresses or other misleading information, posing a security risk.**\n\n\u003e ListenAndServe\n\n```go\n\tmux := http.NewServeMux()\n\n\tsrv := \u0026http.Server{\n\t\tAddr:    \":80\",\n\t\tHandler: mux,\n\t}\n\n\tapp := xun.New(WithMux(mux))\n\tapp.Start()\n\tdefer app.Close()\n\n\t//   srv.ListenAndServe() \n\tproxyproto.ListenAndServe(srv)\n```\n\n\u003e ListenAndServeTLS\n\n```go\n\thttpsServer := \u0026http.Server{\n\t\tAddr:    \":443\",\n\t\tHandler: mux,\n\t}\n\n\tautotls.New(autotls.WithCache(autocert.DirCache(\"./certs\")),\n\t\tautotls.WithHosts(\"yaitoo.cn\", \"www.yaitoo.cn\")).\n\t\tConfigure(srv, httpsServer)\n\n  // httpsServer.ListenAndServeTLS( \"\", \"\") \n\tproxyproto.ListenAndServeTLS(httpsServer, \"\", \"\") \n```\n\n#### Logging \n\nLogs each incoming request to the provided logger. The format of the log messages is customizable using the `Format` option. The default format is the combined log format (XLF/ELF).\n\n\u003e Enable `reqlog` middleware \n\n```go\nfunc main(){\n \t//....\n  logger, _ := setupLogger()\n\n  app.Use(reqlog.New(reqlog.WithLogger(logger),\n\t\treqlog.WithUser(getUserID),\n\t\treqlog.WithVisitor(getVisitorID),\n\t\treqlog.WithFormat(reqlog.Combined))))\n \t//...\n}\n\nfunc setupLogger() (*log.Logger, error) {\n\tlogFile, err := os.OpenFile(\"./access.log\", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn log.New(logFile, \"\", 0), nil\n}\n\nfunc getVisitorID(c *xun.Context) string {\n\tv, err := c.Request.Cookie(\"visitor_id\") // use fingerprintjs to generate visitor id in client's cookie\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn v.Value\n}\n\nfunc getUserID(c *xun.Context) string {\n\tv, _, err := cookie.GetSigned(c, \"session_id\", secretKey)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn v\n}\n\n```\n\n\u003e Install GoAccess to generate real-time analysis report\n\n[How to install GoAccess](https://goaccess.io/get-started)\n\n```bash\ngoaccess ./access.log --geoip-database=./GeoLite2-ASN.mmdb --geoip-database=./GeoLite2-City.mmdb -o ./realtime.html --log-format=COMBINED --real-time-html\n```\n\n\u003e Serve the online real-time analysis report\n```go\n\tapp.Get(\"/reports/realtime.html\", func(c *xun.Context) error {\n\t\thttp.ServeFile(c.Response, c.Request, \"./realtime.html\")\n\t\treturn nil\n\t})\n```\n\n#### CSRF Token\nA CSRF (Cross-Site Request Forgery) token is a unique security measure designed to protect web applications from unauthorized or malicious requests. see more [examples](./ext/csrf/csrf_test.go)\n\n\u003e Enable `csrf` middleware\n```go\nfunc main(){\n \t//....\n  secretKey := []byte(\"your-secret-key\")\n\n  app.Use(csrf.New(secretKey))\n \t//...\n}\n```\n\n\u003e Enable `JsToken` to prevent bot requests on POST/PUT/DELETE\n\n- enable `csrf` with JsToken\n```go\nfunc main(){\n \t//....\n  secretKey := []byte(\"your-secret-key\")\n  app.Use(csrf.New(secretKey,csrf.WithJsToken()))\n  // ...\n  app.Get(\"/assets/csrf.js\",csrf.HandleFunc(secretKey))\n  //...\n}\n```\n\n- load `csrf.js` on html\n```html\n\u003cscript type=\"text/javascript\" src=\"/assets/csrf.js\" defer\u003e\u003c/script\u003e\t\n```\n\n\n#### Access Control List ([ACL](./ext/acl/))\nThe ACL filters and monitors HTTP traffic through granular rule sets, designed to protect web applications/APIs from malicious bots, exploit attempts, and unauthorized access.\n\n##### Core Filtering Dimensions\n- Host-Based Filtering (AllowHosts)\n\n    Restrict access to explicitly permitted domains/subdomains\n- IP Range Control (AllowIPNets/DenyIPNets)\n\n    Allow/block traffic from specific IP addresses or CIDR-notated subnets. IPv4/IPv6 are both supported.\n- Geolocation Filtering (AllowCountries/DenyCountries)\n\n    Permit/restrict access based on client geolocation\n\n##### Enforcement Actions\n- Block unauthorized requests with 403 Forbidden status\n- Host Redirection (Conditional):\n  \n    When AllowHosts validation fails:\n    - Redirect to HostRedirectURL\n    - Use customizable HTTP status HostRedirectStatusCode (e.g., 307 Temporary Redirect)\n\n##### Code Examples\nsee more [examples](./ext/acl/acl_test.go)\n\n\u003e AllowHosts\n```go\napp.Use(acl.New(acl.AllowHosts(\"abc.com\",\"123.com\"), acl.WithHostRedirect(\"https://abc.com\", 302)))\n\n```\n\n\u003e Whitelist Mode by IPNets\n```go\napp.Use(acl.New(acl.AllowIPNets(\"172.0.0.1\",\"2000::1/8\")),acl.DenyIPNets(\"*\")) \n```\n\n\u003e Whitelist Mode by Countries\n```go\nfunc lookup(ip string)string {\n\tdb, _ := geoip2.Open(\"./GeoLite2-City.mmdb\")\n\tnip := net.ParseIP(ip)\n\n\tc, _ := db.cityDB.City(nip)\n\n\treturn c.CountryCode\n}\n\napp.Use(acl.New(acl.WithLookupFunc(lookup),\n\tacl.AllowCountries(\"CN\"),acl.DenyCountries(\"*\")))\n```\n\n\u003e Blacklist Mode by IPNets\n```go\napp.Use(acl.DenyIPNets(\"172.0.0.0/24\")) \n```\n\n\u003e Blacklist Mode by Countries\n```go\napp.Use(acl.New(acl.WithLookupFunc(lookup),acl.DenyCountries(\"us\",\"cn\")))\n```\n\n##### Config Example\nThe optimal solution is to load the rules from a configuration file rather than hard-coding them. The ACL system also monitors the configuration file for changes and automatically reloads the rules. see more [examples](./ext/acl//config_test.go) \n\n\u003e config file\n```ini\n[allow_hosts]\nabc.com\nwww.abc.com\n[allow_ipnets]\n89.207.132.170/24\n# ::1  \n; 127.0.0.1\n[deny_ipnets]\n*\n[allow_countries]\n\n[deny_countries]\nus\n\n[host_redirect]\nurl=http://yaitoo.cn\nstatus_code=302\n\n```\n\n\u003e use middleware with config\n```go\napp.Use(acl.New(acl.WithConfig(\"./acl.ini\")))\n```\n\n#### Server-Sent Events ([SSE](./ext/sse/))\nServer-Sent Events (SSE) is a server push technology enabling a client to receive automatic updates from a server via an HTTP connection.\n\n\u003e use `sse` extension to handle SSE request\n```go\nss := sse.New()\n\napp.Get(\"/topic/{id}\", func(ctx *xun.Context)error {\n\tid := c.Get(\"SessionID\").(string)\n\ts, err := ss.Join(c.Request.Context(), id, c.Response)\n\tif err != nil {\n\t\tc.WriteStatus(http.StatusBadRequest)\n\t\treturn xun.ErrCancelled\n\t}\n\n\ts.Wait()\n\n\tss.Leave(id)\n\n\treturn nil\n})\n\n```\n\n\u003e push an event to the user\n```go\nu := ss.Get(\"user_id\")\nif u != nil {\n\tu.Send(sse.TextEvent{\n\t\tName:\"showMessage\",\n\t\tData:\"Hello\",\n\t})\n}\n```\n\n\u003e broadcast an event to all users\n```go\nctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\ndefer cancel()\nss.Broadcast(ctx, sse.TextEvent{\n\tName:\"shutdown\",\n\tData:\"Server is shutting down\",\n}\n```\n\n\u003e shutdown server and close all user connections\n```go\nss.Shutdown()\n```\n\n\u003e use [htmx-ext-sse](https://htmx.org/extensions/sse/) extension to send SSE request\n```html\n\n\u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/htmx/2.0.4/ext/sse.min.js\" integrity=\"sha512-uROW42fbC8XT6OsVXUC00tuak//shtU8zZE9BwxkT2kOxnZux0Ws8kypRr2UV4OhTEVmUSPIoUOrBN5DXeRNAQ==\" \ncrossorigin=\"anonymous\" referrerpolicy=\"no-referrer\"\u003e\u003c/script\u003e\n\n\u003cdiv class=\"w-full\" hx-ext=\"sse\" sse-connect=\"/topic/{id}\" \u003e\n...\n\u003c/div\u003e\n```\n\n### Works with [tailwindcss](https://tailwindcss.com/docs/installation)\n#### 1. Install Tailwind CSS\nInstall tailwindcss via npm, and create your tailwind.config.js file.\n```bash\nnpm install -D tailwindcss\nnpx tailwindcss init\n```\n#### 2. Configure your template paths\nAdd the paths to all of your template files in your tailwind.config.js file.\n\n\u003e tailwind.config.js\n```json\n/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: [\"./app/**/*.{html,js}\"],\n  theme: {\n    extend: {},\n  },\n  plugins: [],\n}\n```\n\n#### 3. Add the Tailwind directives to your CSS\nAdd the @tailwind directives for each of Tailwind’s layers to your main CSS file.\n\u003e app/tailwind.css\n```css\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n```\n\n#### 4. Start the Tailwind CLI build process\nRun the CLI tool to scan your template files for classes and build your CSS.\n\n```bash\nnpx tailwindcss -i ./app/tailwind.css -o ./app/public/theme.css --watch\n```\n\n#### 5. Start using Tailwind in your HTML\nAdd your compiled CSS file to the `assets.html` and start using Tailwind’s utility classes to style your content.\n\n\u003e components/assets.html\n```html\n\u003clink rel=\"stylesheet\" href=\"/skin.css\"\u003e\n\u003clink rel=\"stylesheet\" href=\"/theme.css\"\u003e\n\u003cscript type=\"text/javascript\" src=\"/app.js\"\u003e\u003c/script\u003e\n```\n\n### Works with [htmx.js](https://htmx.org/docs/)\n#### 1. Add new pages\n\u003e `pages/admin/index.html` and `pages/login.html`\n```\n├── app\n│   ├── components\n│   │   └── assets.html\n│   ├── layouts\n│   │   └── home.html\n│   ├── pages\n│   │   ├── @123.com\n│   │   │   └── index.html\n│   │   ├── admin\n│   │   │   └── index.html\n│   │   ├── index.html\n│   │   ├── login.html\n│   │   └── user\n│   │       └── {id}.html\n│   ├── public\n│   │   ├── @abc.com\n│   │   │   └── index.html\n│   │   ├── app.js\n│   │   ├── skin.css\n│   │   └── theme.css\n│   ├── tailwind.css\n```\n\n#### 2. Serve [htmx-ext.js](./ext/htmx/htmx.js) library\nThe library to enable seamless integration between native JavaScript methods and htmx features, enhancing interactive capabilities without compromising core functionality.\n\n```go\n\tapp.Get(\"/htmx-ext.js\", htmx.HandleFunc())\n```\n\n#### 3. Install htmx.js and htmx-ext.js\n\n\u003e components/assets.html\n```html\n\u003clink rel=\"stylesheet\" href=\"/skin.css\"\u003e\n\u003clink rel=\"stylesheet\" href=\"/theme.css\"\u003e\n\u003cscript src=\"https://unpkg.com/htmx.org@2.0.4\" integrity=\"sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+\" crossorigin=\"anonymous\"\u003e\u003c/script\u003e\n\u003cscript type=\"text/javascript\" src=\"/htmx-ext.js\"\u003e\u003c/script\u003e\n\u003cscript type=\"text/javascript\" src=\"/app.js\" defer\u003e\u003c/script\u003e\n```\n\n#### 4. Enabled `htmx` feature on pages\n\u003e pages/index.html\n```html\n\u003c!--layout:home--\u003e\n{{ define \"content\" }}\n    \u003cdiv id=\"app\" class=\"text-3xl font-bold underline\" hx-boost=\"true\"\u003e\n\n\t\t\t{{ if .TempData.Session }}\n\t\t\t\tHello {{ .TempData.Session }}, go \u003ca href=\"/admin\"\u003eAdmin\u003c/\u003e\n\t\t\t{{ else }}\n        Hello guest, please \u003ca href=\"/login\"\u003eLogin\u003c/a\u003e\t\n\t\t\t{{ end }}    \n    \u003c/div\u003e\n\n{{ end }}\n```\n\n\u003e pages/login.html\n```html\n\u003c!--layout:home--\u003e\n{{ define \"content\" }}\n\n\u003cdiv class=\"flex min-h-full flex-col justify-center px-6 py-12 lg:px-8\"\u003e\n  \u003cdiv class=\"sm:mx-auto sm:w-full sm:max-w-sm\"\u003e\n    \u003ch2 class=\"mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900\"\u003eSign in to your account\u003c/h2\u003e\n  \u003c/div\u003e\n\n  \u003cdiv class=\"mt-10 sm:mx-auto sm:w-full sm:max-w-sm\"\u003e\n    \u003cform class=\"space-y-6\" action=\"#\" method=\"POST\" hx-post=\"/login\"\u003e\n      \u003cdiv\u003e\n        \u003clabel for=\"email\" class=\"block text-sm/6 font-medium text-gray-900\"\u003eEmail address\u003c/label\u003e\n        \u003cdiv class=\"mt-2\"\u003e\n          \u003cinput type=\"email\" name=\"email\" id=\"email\" autocomplete=\"email\" required class=\"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6\"\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n\n      \u003cdiv\u003e\n        \u003cdiv class=\"flex items-center justify-between\"\u003e\n          \u003clabel for=\"password\" class=\"block text-sm/6 font-medium text-gray-900\"\u003ePassword\u003c/label\u003e\n        \u003c/div\u003e\n        \u003cdiv class=\"mt-2\"\u003e\n          \u003cinput type=\"password\" name=\"password\" id=\"password\" autocomplete=\"current-password\" required class=\"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6\"\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n\n      \u003cdiv\u003e\n        \u003cbutton type=\"submit\" class=\"flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600\"\u003eSign in\u003c/button\u003e\n      \u003c/div\u003e\n    \u003c/form\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n\n{{ end }}\n```\n\n\u003e pages/admin/index.html\n```html\n\u003c!--layout:home--\u003e\n{{ define \"content\" }}\n    \u003cdiv id=\"app\" class=\"text-3xl font-bold underline\"\u003e\n\t\t\t\tHello admin: {{ .Data.Name }}\n\t\t\t\u003c/div\u003e\n{{ end }}\n```\n\n#### 5. Setup Hx-Trigger listener\n\u003e app.js\n```js\n$x.ready(function(evt) {\n\tdocument.addEventListener(\"showMessage\", function(evt){\n    alert(evt.detail);\n  })\n},'body');\n\n```\n\n#### 6. Apply `htmx` interceptor \n```go\n\n\tapp := xun.New(xun.WithInterceptor(htmx.New()))\n\n```\n\n#### 7. Create router handler to process request\ncreate an `admin` group router, and apply a middleware to check if it's logged. if not, redirect to /login.\n\n\n```go\n\tadmin := app.Group(\"/admin\")\n\n\tadmin.Use(func(next xun.HandleFunc) xun.HandleFunc {\n\t\treturn func(c *xun.Context) error {\n\t\t\ts, err := c.Request.Cookie(\"session\")\n\t\t\tif err != nil || s == nil || s.Value == \"\" {\n\t\t\t\tc.Redirect(\"/login?return=\" + c.Request.URL.String())\n\t\t\t\treturn xun.ErrCancelled\n\t\t\t}\n\n\t\t\t// set session in Context.TempData, \n\t\t\t// and get it by `.TempData.Session on text/html template files\n\t\t\tc.Set(\"Session\", s.Value)\n\t\t\treturn next(c)\n\t\t}\n\t})\n\n\tadmin.Get(\"/{$}\", func(c *xun.Context) error {\n\t\treturn c.View(User{\n\t\t\tName: c.Get(\"session\").(string),\n\t\t})\n\t})\n\n\tapp.Post(\"/login\", func(c *xun.Context) error {\n\n\t\tit, err := form.BindForm[Login](c.Request)\n\n\t\tif err != nil {\n\t\t\tc.WriteStatus(http.StatusBadRequest)\n\t\t\treturn xun.ErrCancelled\n\t\t}\n\n\t\tif !it.Validate(c.AcceptLanguage()...) {\n\t\t\tc.WriteStatus(http.StatusBadRequest)\n\t\t\treturn c.View(it)\n\t\t}\n\n\t\tif it.Data.Email != \"xun@yaitoo.cn\" || it.Data.Password != \"123\" {\n\t\t\thtmx.WriteHeader(c,htmx.HxTrigger, htmx.HxHeader[string]{\n\t\t\t\t\"showMessage\": \"Email or password is incorrect\",\n\t\t\t})\n\t\t\tc.WriteStatus(http.StatusBadRequest)\n\t\t\treturn c.View(it)\n\t\t}\n\n\t\tcookie := http.Cookie{\n\t\t\tName:     \"session\",\n\t\t\tValue:    it.Data.Email,\n\t\t\tPath:     \"/\",\n\t\t\tMaxAge:   3600,\n\t\t\tHttpOnly: true,\n\t\t\tSecure:   true,\n\t\t\tSameSite: http.SameSiteLaxMode,\n\t\t}\n\n\t\thttp.SetCookie(c.Response, \u0026cookie)\n\n    u, _ := url.Parse(c.RequestReferer())\n\n\t\tc.Redirect(u.Query().Get(\"return\"))\n\t\treturn nil\n\t})\n```\n\n## Deploy your application\nLeveraging Go's built-in `//go:embed` directive and the standard library's `fs.FS` interface, we can compile all static assets and configuration files into a single self-contained binary. This dependency-free approach enables seamless deployment to any server environment.\n\n```go\n\n//go:embed app\nvar fsys embed.FS\n\nfunc main() {\n\tvar dev bool\n\tflag.BoolVar(\u0026dev, \"dev\", false, \"it is development environment\")\n\n\tflag.Parse()\n\n\tvar opts []xun.Option\n\tif dev {\n\t\t// use local filesystem in development, and watch files to reload automatically\n\t\topts = []xun.Option{xun.WithFsys(os.DirFS(\"./app\")), xun.WithWatch()}\n\t} else {\n\t\t// use embed resources in production environment\n\t\tviews, _ := fs.Sub(fsys, \"app\")\n\t\topts = []xun.Option{xun.WithFsys(views)}\n\t}\n\n\tapp := xun.New(opts...)\n\t//...\n\n\tapp.Start()\n\tdefer app.Close()\n\n\tif dev {\n\t\tslog.Default().Info(\"xun-admin is running in development\")\n\t} else {\n\t\tslog.Default().Info(\"xun-admin is running in production\")\n\t}\n\n\terr := http.ListenAndServe(\":80\", http.DefaultServeMux)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n```\n\n## Contributing\nContributions are welcome! If you're interested in contributing, please feel free to [contribute to Xun](CONTRIBUTING.md)\n\n\n## License\n[Apache-2.0 license](LICENSE)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyaitoo%2Fxun","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyaitoo%2Fxun","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyaitoo%2Fxun/lists"}