From 24fa1a4d2c0f5447939f1a5052f01b70b1aad4f1 Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Sun, 27 Sep 2026 11:20:10 -0700 Subject: [PATCH 1/5] docs: generate source-backed middleware reference in echox --- .github/workflows/deploy.yaml | 6 +- .github/workflows/docs.yml | 35 ++ .gitignore | 4 + README.md | 19 +- reference/cmd/config-fields/main.go | 132 +++++ reference/cmd/config-fields/main_test.go | 45 ++ reference/go.mod | 7 + reference/go.sum | 16 + reference/request-logger/main.go | 40 ++ reference/static/main.go | 21 + reference/static/public/index.html | 10 + site/echo-source.json | 4 + site/package.json | 11 +- site/reference-baseline.json | 552 ++++++++++++++++++ site/scripts/accept-source.mjs | 9 + site/scripts/accept-translations.mjs | 11 + site/scripts/check-source.mjs | 37 ++ site/scripts/prepare-echo-source.mjs | 58 ++ site/scripts/translation-status.mjs | 17 + site/src/components/ConfigReference.astro | 50 ++ site/src/content/docs/es/middleware/logger.md | 242 -------- .../src/content/docs/es/middleware/logger.mdx | 54 ++ site/src/content/docs/es/middleware/static.md | 148 ----- .../src/content/docs/es/middleware/static.mdx | 44 ++ site/src/content/docs/ja/middleware/logger.md | 242 -------- .../src/content/docs/ja/middleware/logger.mdx | 54 ++ site/src/content/docs/ja/middleware/static.md | 147 ----- .../src/content/docs/ja/middleware/static.mdx | 44 ++ site/src/content/docs/middleware/logger.md | 242 -------- site/src/content/docs/middleware/logger.mdx | 54 ++ site/src/content/docs/middleware/static.md | 148 ----- site/src/content/docs/middleware/static.mdx | 44 ++ .../content/docs/pt-br/middleware/logger.md | 242 -------- .../content/docs/pt-br/middleware/logger.mdx | 54 ++ .../content/docs/pt-br/middleware/static.md | 148 ----- .../content/docs/pt-br/middleware/static.mdx | 44 ++ .../content/docs/zh-cn/middleware/logger.md | 240 -------- .../content/docs/zh-cn/middleware/logger.mdx | 54 ++ .../content/docs/zh-cn/middleware/static.md | 147 ----- .../content/docs/zh-cn/middleware/static.mdx | 44 ++ site/translation-baseline.json | 18 + 41 files changed, 1587 insertions(+), 1951 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 reference/cmd/config-fields/main.go create mode 100644 reference/cmd/config-fields/main_test.go create mode 100644 reference/go.mod create mode 100644 reference/go.sum create mode 100644 reference/request-logger/main.go create mode 100644 reference/static/main.go create mode 100644 reference/static/public/index.html create mode 100644 site/echo-source.json create mode 100644 site/reference-baseline.json create mode 100644 site/scripts/accept-source.mjs create mode 100644 site/scripts/accept-translations.mjs create mode 100644 site/scripts/check-source.mjs create mode 100644 site/scripts/prepare-echo-source.mjs create mode 100644 site/scripts/translation-status.mjs create mode 100644 site/src/components/ConfigReference.astro delete mode 100644 site/src/content/docs/es/middleware/logger.md create mode 100644 site/src/content/docs/es/middleware/logger.mdx delete mode 100644 site/src/content/docs/es/middleware/static.md create mode 100644 site/src/content/docs/es/middleware/static.mdx delete mode 100644 site/src/content/docs/ja/middleware/logger.md create mode 100644 site/src/content/docs/ja/middleware/logger.mdx delete mode 100644 site/src/content/docs/ja/middleware/static.md create mode 100644 site/src/content/docs/ja/middleware/static.mdx delete mode 100644 site/src/content/docs/middleware/logger.md create mode 100644 site/src/content/docs/middleware/logger.mdx delete mode 100644 site/src/content/docs/middleware/static.md create mode 100644 site/src/content/docs/middleware/static.mdx delete mode 100644 site/src/content/docs/pt-br/middleware/logger.md create mode 100644 site/src/content/docs/pt-br/middleware/logger.mdx delete mode 100644 site/src/content/docs/pt-br/middleware/static.md create mode 100644 site/src/content/docs/pt-br/middleware/static.mdx delete mode 100644 site/src/content/docs/zh-cn/middleware/logger.md create mode 100644 site/src/content/docs/zh-cn/middleware/logger.mdx delete mode 100644 site/src/content/docs/zh-cn/middleware/static.md create mode 100644 site/src/content/docs/zh-cn/middleware/static.mdx create mode 100644 site/translation-baseline.json diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 8268945a..53be1e80 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -31,6 +31,10 @@ jobs: steps: - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 + with: + go-version: '1.27' + - uses: actions/setup-node@v6 with: node-version: 24 @@ -66,4 +70,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..867a5589 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,35 @@ +name: Documentation checks + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 + with: + go-version: '1.27' + cache-dependency-path: | + go.sum + reference/go.sum + - uses: actions/setup-node@v6 + with: + node-version: '24' + cache: npm + cache-dependency-path: site/package-lock.json + - name: Test cookbook + run: go test ./... + - name: Install site dependencies + run: npm ci --ignore-scripts + working-directory: site + - name: Check source reference and build site + run: npm run build + working-directory: site + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index ae35effc..b73e8988 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ vendor .superpowers/ .serena/ .playwright-mcp/ +.cache/ +site/src/generated/ +site/dist/ +site/node_modules/ diff --git a/README.md b/README.md index 1d16acd4..05c94025 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,15 @@ the runnable cookbook recipes the docs reference. | ----------- | --------------------------------------------------------------------------------------------------- | | `site/` | The docs site — [Astro](https://astro.build) + [Starlight](https://starlight.astro.build). Content lives in `site/src/content/docs/`. | | `cookbook/` | Standalone, runnable Go example apps referenced from the docs. | +| `reference/` | Source-owned middleware examples and the config-field extractor used by the site build. | | `docs/` | Internal design specs. | ## Documentation site -Requires [Node.js](https://nodejs.org) (LTS). +Requires [Node.js](https://nodejs.org) (LTS), Go 1.27, and Git. The site build +fetches the Echo commit recorded in `site/echo-source.json`, compiles the +`reference/` examples against it, extracts exported middleware fields, and +checks them against `site/reference-baseline.json` before building pages. ```bash cd site @@ -24,6 +28,19 @@ npm run build # production build to site/dist npm run preview # preview the production build ``` +The first build fetches the pinned Echo source into `.cache/`. To test a proposed +Echo checkout instead, set `ECHO_SOURCE_DIR` to its absolute path when running +`npm run build`. The build reports changed fields and stops; review the affected +pages, run `npm run source:prepare` and `npm run source:accept`, then review the +baseline diff before committing it. The generated files in `site/src/generated/` +are never edited or committed. + +`npm run translations:status` reports whether the reviewed Spanish, Japanese, +Portuguese, and Chinese Request Logger and Static pages still match the current +English source. After reviewing those translations, run +`npm run translations:accept` and review the hash changes. This report tracks +freshness; it does not validate translation quality. + Content is Markdown/MDX under `site/src/content/docs/` (`guide/`, `middleware/`, `cookbook/`). To add a page, drop a file in the right folder — the sidebar is generated from each page's `sidebar.order` frontmatter. Every page needs a diff --git a/reference/cmd/config-fields/main.go b/reference/cmd/config-fields/main.go new file mode 100644 index 00000000..1057f285 --- /dev/null +++ b/reference/cmd/config-fields/main.go @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT + +// config-fields emits source-backed middleware config fields for the website. +// It does not claim to determine runtime defaults or behavior. +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "os" + "path/filepath" + "sort" + "strings" +) + +type field struct { + Name string `json:"name"` + Type string `json:"type"` + Deprecated bool `json:"deprecated,omitempty"` + Line int `json:"line"` +} + +type config struct { + Name string `json:"name"` + File string `json:"file"` + Line int `json:"line"` + Fields []field `json:"fields"` +} + +type manifest struct { + Module string `json:"module"` + Revision string `json:"revision,omitempty"` + Configs []config `json:"configs"` +} + +func main() { + root := flag.String("root", "", "Echo repository root") + revision := flag.String("revision", "", "exact Echo commit or tag used for this build") + flag.Parse() + if *root == "" { + fmt.Fprintln(os.Stderr, "-root is required") + os.Exit(2) + } + + result, err := extract(*root, *revision) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + encoder := json.NewEncoder(os.Stdout) + encoder.SetIndent("", " ") + if err := encoder.Encode(result); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func extract(root, revision string) (manifest, error) { + root, err := filepath.Abs(root) + if err != nil { + return manifest{}, err + } + fs := token.NewFileSet() + packageDir := filepath.Join(root, "middleware") + packages, err := parser.ParseDir(fs, packageDir, func(info os.FileInfo) bool { + return strings.HasSuffix(info.Name(), ".go") && !strings.HasSuffix(info.Name(), "_test.go") + }, parser.ParseComments) + if err != nil { + return manifest{}, err + } + pkg, ok := packages["middleware"] + if !ok { + return manifest{}, fmt.Errorf("middleware package not found in %s", packageDir) + } + + result := manifest{Module: "github.com/labstack/echo/v5", Revision: revision, Configs: []config{}} + for filename, source := range pkg.Files { + for _, declaration := range source.Decls { + group, ok := declaration.(*ast.GenDecl) + if !ok || group.Tok != token.TYPE { + continue + } + for _, item := range group.Specs { + typeSpec := item.(*ast.TypeSpec) + structure, ok := typeSpec.Type.(*ast.StructType) + if !ok || !ast.IsExported(typeSpec.Name.Name) || !strings.HasSuffix(typeSpec.Name.Name, "Config") { + continue + } + entry := config{ + Name: typeSpec.Name.Name, + File: filepath.ToSlash(strings.TrimPrefix(filename, root+string(filepath.Separator))), + Line: fs.Position(typeSpec.Pos()).Line, + Fields: []field{}, + } + for _, sourceField := range structure.Fields.List { + var rendered bytes.Buffer + if err := format.Node(&rendered, fs, sourceField.Type); err != nil { + return manifest{}, err + } + fieldDoc := comment(sourceField.Doc) + for _, name := range sourceField.Names { + if !ast.IsExported(name.Name) { + continue + } + entry.Fields = append(entry.Fields, field{ + Name: name.Name, + Type: rendered.String(), + Deprecated: strings.Contains(fieldDoc, "Deprecated:"), + Line: fs.Position(name.Pos()).Line, + }) + } + } + result.Configs = append(result.Configs, entry) + } + } + } + sort.Slice(result.Configs, func(i, j int) bool { return result.Configs[i].Name < result.Configs[j].Name }) + return result, nil +} + +func comment(group *ast.CommentGroup) string { + if group == nil { + return "" + } + return strings.TrimSpace(group.Text()) +} diff --git a/reference/cmd/config-fields/main_test.go b/reference/cmd/config-fields/main_test.go new file mode 100644 index 00000000..33a40296 --- /dev/null +++ b/reference/cmd/config-fields/main_test.go @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT + +package main + +import ( + "os" + "path/filepath" + "testing" +) + +func TestExtractOnlyExportedConfigFields(t *testing.T) { + root := t.TempDir() + dir := filepath.Join(root, "middleware") + if err := os.Mkdir(dir, 0755); err != nil { + t.Fatal(err) + } + source := `package middleware +type ExampleConfig struct { + // Deprecated: use New instead. + Old bool + New string + private int +} +type hiddenConfig struct { Visible bool } +type OtherType struct { Visible bool } +` + if err := os.WriteFile(filepath.Join(dir, "example.go"), []byte(source), 0644); err != nil { + t.Fatal(err) + } + + got, err := extract(root, "abc123") + if err != nil { + t.Fatal(err) + } + if got.Revision != "abc123" || len(got.Configs) != 1 { + t.Fatalf("unexpected manifest: %#v", got) + } + fields := got.Configs[0].Fields + if got.Configs[0].File != "middleware/example.go" || len(fields) != 2 { + t.Fatalf("unexpected config: %#v", got.Configs[0]) + } + if fields[0].Name != "Old" || fields[0].Type != "bool" || !fields[0].Deprecated || fields[1].Name != "New" { + t.Fatalf("unexpected fields: %#v", fields) + } +} diff --git a/reference/go.mod b/reference/go.mod new file mode 100644 index 00000000..f390045d --- /dev/null +++ b/reference/go.mod @@ -0,0 +1,7 @@ +module github.com/labstack/echox/reference + +go 1.25.0 + +require github.com/labstack/echo/v5 v5.3.0 + +require golang.org/x/time v0.15.0 // indirect diff --git a/reference/go.sum b/reference/go.sum new file mode 100644 index 00000000..2929860c --- /dev/null +++ b/reference/go.sum @@ -0,0 +1,16 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/labstack/echo/v5 v5.3.0 h1:KT74Mprk053PQEHwSZdeCDIz1BigTZOZhavMD0c9Fjs= +github.com/labstack/echo/v5 v5.3.0/go.mod h1:Q3j2+clBRgJr0O3DDONQeXNsM7RHgSwUhcuo47unqm8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/reference/request-logger/main.go b/reference/request-logger/main.go new file mode 100644 index 00000000..edbcbe57 --- /dev/null +++ b/reference/request-logger/main.go @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +// This complete example is the source for the Request Logger documentation. +package main + +import ( + "log/slog" + "net/http" + "os" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + e := echo.New() + logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + + e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogURI: true, + LogStatus: true, + HandleError: true, + LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { + if v.Error != nil { + logger.Error("request failed", "uri", v.URI, "status", v.Status, "error", v.Error) + return nil + } + logger.Info("request", "uri", v.URI, "status", v.Status) + return nil + }, + })) + + e.GET("/", func(c *echo.Context) error { + return c.String(http.StatusOK, "hello") + }) + + if err := e.Start(":1323"); err != nil { + e.Logger.Error("server stopped", "error", err) + } +} diff --git a/reference/static/main.go b/reference/static/main.go new file mode 100644 index 00000000..7c066599 --- /dev/null +++ b/reference/static/main.go @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +// This complete example is the source for the Static middleware documentation. +package main + +import ( + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + e := echo.New() + e.Use(middleware.StaticWithConfig(middleware.StaticConfig{ + Root: "public", + EnablePathUnescaping: false, // Keep encoded slashes encoded when route guards protect files. + })) + + if err := e.Start(":1323"); err != nil { + e.Logger.Error("server stopped", "error", err) + } +} diff --git a/reference/static/public/index.html b/reference/static/public/index.html new file mode 100644 index 00000000..5fbf861c --- /dev/null +++ b/reference/static/public/index.html @@ -0,0 +1,10 @@ + + + + + Echo Static example + + +

Hello from Echo Static middleware

+ + diff --git a/site/echo-source.json b/site/echo-source.json new file mode 100644 index 00000000..81bb76d9 --- /dev/null +++ b/site/echo-source.json @@ -0,0 +1,4 @@ +{ + "repository": "https://github.com/labstack/echo.git", + "revision": "68ac4cf7e0b862001a9de7cdbc50d9447287a07c" +} diff --git a/site/package.json b/site/package.json index 3c03aec5..b80c9e9e 100644 --- a/site/package.json +++ b/site/package.json @@ -3,10 +3,15 @@ "type": "module", "version": "0.1.0", "scripts": { - "dev": "astro dev", - "build": "astro build", + "dev": "npm run source:prepare && astro dev", + "build": "npm run source:check && npm run translations:status && astro build", "preview": "astro preview", - "astro": "astro" + "astro": "astro", + "source:prepare": "node scripts/prepare-echo-source.mjs", + "source:check": "npm run source:prepare && node scripts/check-source.mjs", + "source:accept": "node scripts/accept-source.mjs", + "translations:status": "node scripts/translation-status.mjs", + "translations:accept": "node scripts/accept-translations.mjs" }, "dependencies": { "@astrojs/starlight": "^0.40.0", diff --git a/site/reference-baseline.json b/site/reference-baseline.json new file mode 100644 index 00000000..9672f031 --- /dev/null +++ b/site/reference-baseline.json @@ -0,0 +1,552 @@ +{ + "configs": { + "AddTrailingSlashConfig": { + "Skipper": { + "type": "Skipper", + "deprecated": false + }, + "RedirectCode": { + "type": "int", + "deprecated": false + } + }, + "BasicAuthConfig": { + "Skipper": { + "type": "Skipper", + "deprecated": false + }, + "Validator": { + "type": "BasicAuthValidator", + "deprecated": false + }, + "Realm": { + "type": "string", + "deprecated": false + }, + "AllowedCheckLimit": { + "type": "uint", + "deprecated": false + } + }, + "BodyDumpConfig": { + "Skipper": { + "type": "Skipper", + "deprecated": false + }, + "Handler": { + "type": "BodyDumpHandler", + "deprecated": false + }, + "MaxRequestBytes": { + "type": "int64", + "deprecated": false + }, + "MaxResponseBytes": { + "type": "int64", + "deprecated": false + } + }, + "BodyLimitConfig": { + "Skipper": { + "type": "Skipper", + "deprecated": false + }, + "LimitBytes": { + "type": "int64", + "deprecated": false + } + }, + "CORSConfig": { + "Skipper": { + "type": "Skipper", + "deprecated": false + }, + "AllowOrigins": { + "type": "[]string", + "deprecated": false + }, + "UnsafeAllowOriginFunc": { + "type": "func(c *echo.Context, origin string) (allowedOrigin string, allowed bool, err error)", + "deprecated": false + }, + "AllowMethods": { + "type": "[]string", + "deprecated": false + }, + "AllowHeaders": { + "type": "[]string", + "deprecated": false + }, + "AllowCredentials": { + "type": "bool", + "deprecated": false + }, + "ExposeHeaders": { + "type": "[]string", + "deprecated": false + }, + "MaxAge": { + "type": "int", + "deprecated": false + } + }, + "CSRFConfig": { + "Skipper": { + "type": "Skipper", + "deprecated": false + }, + "TrustedOrigins": { + "type": "[]string", + "deprecated": false + }, + "AllowSecFetchSiteFunc": { + "type": "func(c *echo.Context) (bool, error)", + "deprecated": false + }, + "TokenLength": { + "type": "uint8", + "deprecated": false + }, + "TokenLookup": { + "type": "string", + "deprecated": false + }, + "Generator": { + "type": "func() string", + "deprecated": false + }, + "ContextKey": { + "type": "string", + "deprecated": false + }, + "CookieName": { + "type": "string", + "deprecated": false + }, + "CookieDomain": { + "type": "string", + "deprecated": false + }, + "CookiePath": { + "type": "string", + "deprecated": false + }, + "CookieMaxAge": { + "type": "int", + "deprecated": false + }, + "CookieSecure": { + "type": "bool", + "deprecated": false + }, + "CookieHTTPOnly": { + "type": "bool", + "deprecated": false + }, + "CookieSameSite": { + "type": "http.SameSite", + "deprecated": false + }, + "ErrorHandler": { + "type": "func(c *echo.Context, err error) error", + "deprecated": false + } + }, + "ContextTimeoutConfig": { + "Skipper": { + "type": "Skipper", + "deprecated": false + }, + "ErrorHandler": { + "type": "func(c *echo.Context, err error) error", + "deprecated": false + }, + "Timeout": { + "type": "time.Duration", + "deprecated": false + } + }, + "DecompressConfig": { + "Skipper": { + "type": "Skipper", + "deprecated": false + }, + "GzipDecompressPool": { + "type": "Decompressor", + "deprecated": false + }, + "MaxDecompressedSize": { + "type": "int64", + "deprecated": false + } + }, + "GzipConfig": { + "Skipper": { + "type": "Skipper", + "deprecated": false + }, + "Level": { + "type": "int", + "deprecated": false + }, + "MinLength": { + "type": "int", + "deprecated": false + } + }, + "KeyAuthConfig": { + "Skipper": { + "type": "Skipper", + "deprecated": false + }, + "KeyLookup": { + "type": "string", + "deprecated": false + }, + "AllowedCheckLimit": { + "type": "uint", + "deprecated": false + }, + "Validator": { + "type": "KeyAuthValidator", + "deprecated": false + }, + "ErrorHandler": { + "type": "KeyAuthErrorHandler", + "deprecated": false + }, + "ContinueOnIgnoredError": { + "type": "bool", + "deprecated": false + } + }, + "MethodOverrideConfig": { + "Skipper": { + "type": "Skipper", + "deprecated": false + }, + "Getter": { + "type": "MethodOverrideGetter", + "deprecated": false + } + }, + "ProxyConfig": { + "Skipper": { + "type": "Skipper", + "deprecated": false + }, + "Balancer": { + "type": "ProxyBalancer", + "deprecated": false + }, + "RetryCount": { + "type": "int", + "deprecated": false + }, + "RetryFilter": { + "type": "func(c *echo.Context, e error) bool", + "deprecated": false + }, + "ErrorHandler": { + "type": "func(c *echo.Context, err error) error", + "deprecated": false + }, + "Rewrite": { + "type": "map[string]string", + "deprecated": false + }, + "RegexRewrite": { + "type": "map[*regexp.Regexp]string", + "deprecated": false + }, + "ContextKey": { + "type": "string", + "deprecated": false + }, + "Transport": { + "type": "http.RoundTripper", + "deprecated": false + }, + "ModifyResponse": { + "type": "func(*http.Response) error", + "deprecated": false + } + }, + "RateLimiterConfig": { + "Skipper": { + "type": "Skipper", + "deprecated": false + }, + "BeforeFunc": { + "type": "BeforeFunc", + "deprecated": false + }, + "IdentifierExtractor": { + "type": "Extractor", + "deprecated": false + }, + "Store": { + "type": "RateLimiterStore", + "deprecated": false + }, + "ErrorHandler": { + "type": "func(c *echo.Context, err error) error", + "deprecated": false + }, + "DenyHandler": { + "type": "func(c *echo.Context, identifier string, err error) error", + "deprecated": false + } + }, + "RateLimiterMemoryStoreConfig": { + "Rate": { + "type": "float64", + "deprecated": false + }, + "Burst": { + "type": "int", + "deprecated": false + }, + "ExpiresIn": { + "type": "time.Duration", + "deprecated": false + } + }, + "RecoverConfig": { + "Skipper": { + "type": "Skipper", + "deprecated": false + }, + "StackSize": { + "type": "int", + "deprecated": false + }, + "DisableStackAll": { + "type": "bool", + "deprecated": false + }, + "DisablePrintStack": { + "type": "bool", + "deprecated": false + } + }, + "RedirectConfig": { + "Code": { + "type": "int", + "deprecated": false + } + }, + "RemoveTrailingSlashConfig": { + "Skipper": { + "type": "Skipper", + "deprecated": false + }, + "RedirectCode": { + "type": "int", + "deprecated": false + } + }, + "RequestIDConfig": { + "Skipper": { + "type": "Skipper", + "deprecated": false + }, + "Generator": { + "type": "func() string", + "deprecated": false + }, + "RequestIDHandler": { + "type": "func(c *echo.Context, requestID string)", + "deprecated": false + }, + "TargetHeader": { + "type": "string", + "deprecated": false + } + }, + "RequestLoggerConfig": { + "Skipper": { + "type": "Skipper", + "deprecated": false + }, + "BeforeNextFunc": { + "type": "func(c *echo.Context)", + "deprecated": false + }, + "LogValuesFunc": { + "type": "func(c *echo.Context, v RequestLoggerValues) error", + "deprecated": false + }, + "HandleError": { + "type": "bool", + "deprecated": false + }, + "LogLatency": { + "type": "bool", + "deprecated": false + }, + "LogProtocol": { + "type": "bool", + "deprecated": false + }, + "LogRemoteIP": { + "type": "bool", + "deprecated": false + }, + "LogHost": { + "type": "bool", + "deprecated": false + }, + "LogMethod": { + "type": "bool", + "deprecated": false + }, + "LogURI": { + "type": "bool", + "deprecated": false + }, + "LogURIPath": { + "type": "bool", + "deprecated": false + }, + "LogRoutePath": { + "type": "bool", + "deprecated": false + }, + "LogRequestID": { + "type": "bool", + "deprecated": false + }, + "LogReferer": { + "type": "bool", + "deprecated": false + }, + "LogUserAgent": { + "type": "bool", + "deprecated": false + }, + "LogStatus": { + "type": "bool", + "deprecated": false + }, + "LogContentLength": { + "type": "bool", + "deprecated": false + }, + "LogResponseSize": { + "type": "bool", + "deprecated": false + }, + "LogHeaders": { + "type": "[]string", + "deprecated": false + }, + "LogQueryParams": { + "type": "[]string", + "deprecated": false + }, + "LogFormValues": { + "type": "[]string", + "deprecated": false + } + }, + "RewriteConfig": { + "Skipper": { + "type": "Skipper", + "deprecated": false + }, + "Rules": { + "type": "map[string]string", + "deprecated": false + }, + "RegexRules": { + "type": "map[*regexp.Regexp]string", + "deprecated": false + } + }, + "SecureConfig": { + "Skipper": { + "type": "Skipper", + "deprecated": false + }, + "XSSProtection": { + "type": "string", + "deprecated": false + }, + "ContentTypeNosniff": { + "type": "string", + "deprecated": false + }, + "XFrameOptions": { + "type": "string", + "deprecated": false + }, + "HSTSMaxAge": { + "type": "int", + "deprecated": false + }, + "HSTSExcludeSubdomains": { + "type": "bool", + "deprecated": false + }, + "ContentSecurityPolicy": { + "type": "string", + "deprecated": false + }, + "CSPReportOnly": { + "type": "bool", + "deprecated": false + }, + "HSTSPreloadEnabled": { + "type": "bool", + "deprecated": false + }, + "ReferrerPolicy": { + "type": "string", + "deprecated": false + } + }, + "StaticConfig": { + "Skipper": { + "type": "Skipper", + "deprecated": false + }, + "Root": { + "type": "string", + "deprecated": false + }, + "Filesystem": { + "type": "fs.FS", + "deprecated": false + }, + "Index": { + "type": "string", + "deprecated": false + }, + "HTML5": { + "type": "bool", + "deprecated": false + }, + "Browse": { + "type": "bool", + "deprecated": false + }, + "IgnoreBase": { + "type": "bool", + "deprecated": false + }, + "DisablePathUnescaping": { + "type": "bool", + "deprecated": true + }, + "EnablePathUnescaping": { + "type": "bool", + "deprecated": false + }, + "DirectoryListTemplate": { + "type": "string", + "deprecated": false + } + } + } +} diff --git a/site/scripts/accept-source.mjs b/site/scripts/accept-source.mjs new file mode 100644 index 00000000..192fb708 --- /dev/null +++ b/site/scripts/accept-source.mjs @@ -0,0 +1,9 @@ +import { readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const siteDir = fileURLToPath(new URL('..', import.meta.url)); +const manifest = JSON.parse(readFileSync(join(siteDir, 'src/generated/config-fields.json'), 'utf8')); +const configs = Object.fromEntries(manifest.configs.map((config) => [config.name, Object.fromEntries(config.fields.map((field) => [field.name, { type: field.type, deprecated: field.deprecated ?? false }]))])); +writeFileSync(join(siteDir, 'reference-baseline.json'), `${JSON.stringify({ configs }, null, 2)}\n`); +console.log(`Accepted ${manifest.configs.length} Echo config types for review`); diff --git a/site/scripts/accept-translations.mjs b/site/scripts/accept-translations.mjs new file mode 100644 index 00000000..d6319f3c --- /dev/null +++ b/site/scripts/accept-translations.mjs @@ -0,0 +1,11 @@ +import { createHash } from 'node:crypto'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const siteDir = fileURLToPath(new URL('..', import.meta.url)); +const hash = (page) => createHash('sha256').update(readFileSync(join(siteDir, 'src/content/docs/middleware', `${page}.mdx`))).digest('hex'); +const pages = { logger: hash('logger'), static: hash('static') }; +const baseline = Object.fromEntries(['es', 'ja', 'pt-br', 'zh-cn'].map((locale) => [locale, pages])); +writeFileSync(join(siteDir, 'translation-baseline.json'), `${JSON.stringify(baseline, null, 2)}\n`); +console.log('Accepted Request Logger and Static translations for review'); diff --git a/site/scripts/check-source.mjs b/site/scripts/check-source.mjs new file mode 100644 index 00000000..f43264aa --- /dev/null +++ b/site/scripts/check-source.mjs @@ -0,0 +1,37 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const siteDir = fileURLToPath(new URL('..', import.meta.url)); +const manifest = JSON.parse(readFileSync(join(siteDir, 'src/generated/config-fields.json'), 'utf8')); +const baseline = JSON.parse(readFileSync(join(siteDir, 'reference-baseline.json'), 'utf8')); + +function fieldsByConfig(configs) { + return Object.fromEntries(configs.map((config) => [config.name, Object.fromEntries(config.fields.map((field) => [field.name, { type: field.type, deprecated: field.deprecated ?? false }]))])); +} + +const actual = fieldsByConfig(manifest.configs); +const differences = []; +for (const name of new Set([...Object.keys(baseline.configs), ...Object.keys(actual)])) { + const before = baseline.configs[name] || {}; + const after = actual[name] || {}; + for (const field of new Set([...Object.keys(before), ...Object.keys(after)])) { + if (JSON.stringify(before[field]) !== JSON.stringify(after[field])) { + differences.push(`${name}.${field}: ${JSON.stringify(before[field] ?? null)} -> ${JSON.stringify(after[field] ?? null)}`); + } + } +} +if (differences.length) { + throw new Error(`Echo config fields changed. Review the affected pages, then update reference-baseline.json:\n${differences.join('\n')}`); +} + +for (const locale of ['', 'es/', 'ja/', 'pt-br/', 'zh-cn/']) { + for (const [page, config, example] of [['logger', 'RequestLoggerConfig', 'request-logger.go'], ['static', 'StaticConfig', 'static.go']]) { + const file = join(siteDir, 'src/content/docs', locale, 'middleware', `${page}.mdx`); + const content = readFileSync(file, 'utf8'); + if (!content.includes(`name="${config}"`) || !content.includes(example) || (page === 'logger' && content.includes('LogError'))) { + throw new Error(`${file} must display the source-backed ${config} reference and ${example} example without removed fields`); + } + } +} +console.log(`Source reference matches Echo ${manifest.revision}`); diff --git a/site/scripts/prepare-echo-source.mjs b/site/scripts/prepare-echo-source.mjs new file mode 100644 index 00000000..8db8fb96 --- /dev/null +++ b/site/scripts/prepare-echo-source.mjs @@ -0,0 +1,58 @@ +import { execFileSync } from 'node:child_process'; +import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const siteDir = fileURLToPath(new URL('..', import.meta.url)); +const repoDir = dirname(siteDir); +const pin = JSON.parse(readFileSync(join(siteDir, 'echo-source.json'), 'utf8')); +const override = process.env.ECHO_SOURCE_DIR; +const sourceDir = override ? resolve(siteDir, override) : join(repoDir, '.cache', 'echo-source'); +const referenceDir = join(repoDir, 'reference'); +const generatedDir = join(siteDir, 'src', 'generated'); + +function run(command, args, cwd = repoDir, environment = {}) { + try { + return execFileSync(command, args, { cwd, env: { ...process.env, ...environment }, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); + } catch (error) { + const detail = error.stderr?.toString().trim() || error.message; + throw new Error(`${command} ${args.join(' ')} failed: ${detail}`); + } +} + +if (!override) { + mkdirSync(dirname(sourceDir), { recursive: true }); + if (!existsSync(join(sourceDir, '.git'))) { + run('git', ['clone', '--filter=blob:none', '--no-checkout', pin.repository, sourceDir]); + } + const current = run('git', ['rev-parse', 'HEAD'], sourceDir); + if (current !== pin.revision || !existsSync(join(sourceDir, 'go.mod'))) { + if (current !== pin.revision) run('git', ['fetch', '--depth=1', 'origin', pin.revision], sourceDir); + run('git', ['checkout', '--detach', pin.revision], sourceDir); + } +} + +const revision = run('git', ['rev-parse', 'HEAD'], sourceDir); +if (!override && revision !== pin.revision) { + throw new Error(`Echo checkout is ${revision}; expected ${pin.revision}`); +} +if (run('git', ['status', '--porcelain', '--untracked-files=no'], sourceDir)) { + throw new Error(`Echo checkout has tracked changes: ${sourceDir}`); +} + +const workspaceDir = join(repoDir, '.cache', 'echo-work'); +mkdirSync(workspaceDir, { recursive: true }); +const workspaceFile = join(workspaceDir, 'go.work'); +writeFileSync(workspaceFile, `go 1.27.0\n\nuse (\n\t${JSON.stringify(sourceDir)}\n\t${JSON.stringify(referenceDir)}\n)\n`); +const goEnvironment = { GOWORK: workspaceFile }; +const manifestText = run('go', ['run', './cmd/config-fields', '-root', sourceDir, '-revision', revision], referenceDir, goEnvironment); +const manifest = JSON.parse(manifestText); +if (manifest.revision !== revision || !manifest.configs.some((config) => config.name === 'RequestLoggerConfig') || !manifest.configs.some((config) => config.name === 'StaticConfig')) { + throw new Error('Echo source extractor returned an incomplete or mismatched manifest'); +} +run('go', ['test', './...'], referenceDir, goEnvironment); +mkdirSync(generatedDir, { recursive: true }); +writeFileSync(join(generatedDir, 'config-fields.json'), `${JSON.stringify(manifest, null, 2)}\n`); +copyFileSync(join(referenceDir, 'request-logger', 'main.go'), join(generatedDir, 'request-logger.go.txt')); +copyFileSync(join(referenceDir, 'static', 'main.go'), join(generatedDir, 'static.go.txt')); +console.log(`Prepared Echo source ${revision}`); diff --git a/site/scripts/translation-status.mjs b/site/scripts/translation-status.mjs new file mode 100644 index 00000000..5848584f --- /dev/null +++ b/site/scripts/translation-status.mjs @@ -0,0 +1,17 @@ +import { createHash } from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const siteDir = fileURLToPath(new URL('..', import.meta.url)); +const baseline = JSON.parse(readFileSync(join(siteDir, 'translation-baseline.json'), 'utf8')); +const hash = (page) => createHash('sha256').update(readFileSync(join(siteDir, 'src/content/docs/middleware', `${page}.mdx`))).digest('hex'); +let stale = 0; +for (const [locale, pages] of Object.entries(baseline)) { + for (const [page, reviewedHash] of Object.entries(pages)) { + const status = reviewedHash === hash(page) ? 'current' : 'needs review'; + if (status !== 'current') stale++; + console.log(`${locale}/middleware/${page}: ${status}`); + } +} +console.log(`${stale} translation(s) need review; this report does not validate the prose`); diff --git a/site/src/components/ConfigReference.astro b/site/src/components/ConfigReference.astro new file mode 100644 index 00000000..98cf4ef3 --- /dev/null +++ b/site/src/components/ConfigReference.astro @@ -0,0 +1,50 @@ +--- +import manifest from '../generated/config-fields.json'; + +interface Props { + name: string; + locale?: 'en' | 'es' | 'ja' | 'pt-br' | 'zh-cn'; +} + +const { name, locale = 'en' } = Astro.props; +const config = manifest.configs.find((entry) => entry.name === name); +if (!config) throw new Error(`Missing Echo source reference for ${name}`); +const labels = { + en: { field: 'Field', type: 'Type', source: 'Source', deprecated: 'Deprecated', caption: 'Fields from Echo source' }, + es: { field: 'Campo', type: 'Tipo', source: 'Fuente', deprecated: 'Obsoleto', caption: 'Campos del código de Echo' }, + ja: { field: 'フィールド', type: '型', source: 'ソース', deprecated: '非推奨', caption: 'Echo のソースコードのフィールド' }, + 'pt-br': { field: 'Campo', type: 'Tipo', source: 'Código', deprecated: 'Obsoleto', caption: 'Campos do código do Echo' }, + 'zh-cn': { field: '字段', type: '类型', source: '源码', deprecated: '已弃用', caption: 'Echo 源码中的字段' }, +}[locale]; +const sourceUrl = (line: number) => `https://github.com/labstack/echo/blob/${manifest.revision}/${config.file}#L${line}`; +--- + +
+

{config.name} · Echo {manifest.revision.slice(0, 7)}

+
+ + + + + {config.fields.map((field) => + + + + )} + +
{labels.caption}
{labels.field}{labels.type}{labels.source}
{field.name}{field.deprecated && {labels.deprecated}}{field.type}L{field.line}
+
+
+ + diff --git a/site/src/content/docs/es/middleware/logger.md b/site/src/content/docs/es/middleware/logger.md deleted file mode 100644 index 25fb0640..00000000 --- a/site/src/content/docs/es/middleware/logger.md +++ /dev/null @@ -1,242 +0,0 @@ ---- -title: Request Logger -description: Logging de requests totalmente personalizable que se integra con bibliotecas de logging estructurado. -sidebar: - order: 12 ---- - -El middleware `RequestLogger` registra información sobre cada request HTTP. Te permite -personalizar por completo qué se registra y cómo, por lo que encaja bien con bibliotecas -de terceros de logging estructurado. - -Los valores que el logger puede extraer se controlan con los campos booleanos y slices de -`RequestLoggerConfig`. Habilita un campo (por ejemplo `LogStatus: true`) para que su valor -se rellene en `RequestLoggerValues`, que se pasa a tu `LogValuesFunc`. - -```go -type RequestLoggerConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // BeforeNextFunc is called before the next middleware or handler in the chain. - BeforeNextFunc func(c *echo.Context) - - // LogValuesFunc is called with the values extracted by the logger from the - // request/response. - // Mandatory. - LogValuesFunc func(c *echo.Context, v RequestLoggerValues) error - - // HandleError instructs the logger to call the global error handler when the next - // middleware/handler returns an error. A side effect is that the response is then - // committed and sent, so middlewares up the chain can no longer change the status - // code or body. - HandleError bool - - // LogLatency records the duration of the rest of the handler chain (the next(c) call). - LogLatency bool - // LogProtocol extracts the request protocol (for example HTTP/1.1 or HTTP/2). - LogProtocol bool - // LogRemoteIP extracts the request remote IP. See echo.Context.RealIP() for details. - LogRemoteIP bool - // LogHost extracts the request host value (for example example.com). - LogHost bool - // LogMethod extracts the request method (for example GET). - LogMethod bool - // LogURI extracts the request URI (for example /list?lang=en&page=1). - LogURI bool - // LogURIPath extracts the request URI path part (for example /list). - LogURIPath bool - // LogRoutePath extracts the route path the request matched (for example /user/:id). - LogRoutePath bool - // LogRequestID extracts the request ID from the X-Request-ID request header, or the - // response if the request did not have a value. - LogRequestID bool - // LogReferer extracts the request referer value. - LogReferer bool - // LogUserAgent extracts the request user agent value. - LogUserAgent bool - // LogStatus extracts the response status code. If the chain returns an echo.HTTPError, - // the status code is taken from it. - LogStatus bool - // LogError extracts the error returned from the handler chain. - LogError bool - // LogContentLength extracts the Content-Length header value. Note: this can differ - // from the actual request body size as it may be spoofed. - LogContentLength bool - // LogResponseSize extracts the response content length. Note: when used with Gzip - // middleware this value may not always be correct. - LogResponseSize bool - // LogHeaders extracts the given list of request headers. A slice of values is logged - // per header since a request can contain more than one. Names are canonicalized with - // http.CanonicalHeaderKey (for example "accept-encoding" becomes "Accept-Encoding"). - LogHeaders []string - // LogQueryParams extracts the given list of query parameters from the request URI. A - // slice of values is logged per name since a request can repeat a parameter. - LogQueryParams []string - // LogFormValues extracts the given list of form values from the request body and URI. - // A slice of values is logged per name since a request can repeat a value. - LogFormValues []string -} -``` - -Todo el middleware principal reside en el paquete `middleware`: - -```go -import "github.com/labstack/echo/v5/middleware" -``` - -## Ejemplos - -### fmt.Printf - -```go -skipper := func(c *echo.Context) bool { - // Skip the health check endpoint. - return c.Request().URL.Path == "/health" -} -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogStatus: true, - LogURI: true, - Skipper: skipper, - BeforeNextFunc: func(c *echo.Context) { - c.Set("customValueFromContext", 42) - }, - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - value, _ := c.Get("customValueFromContext").(int) - fmt.Printf("REQUEST: uri: %v, status: %v, custom-value: %v\n", v.URI, v.Status, value) - return nil - }, -})) -``` - -Salida de ejemplo: - -```text -REQUEST: uri: /hello, status: 200, custom-value: 42 -``` - -### slog ([log/slog](https://pkg.go.dev/log/slog)) - -```go -logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogStatus: true, - LogURI: true, - LogError: true, - HandleError: true, // forwards the error to the global error handler so it can pick the status code - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - if v.Error == nil { - logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST", - slog.String("uri", v.URI), - slog.Int("status", v.Status), - ) - } else { - logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR", - slog.String("uri", v.URI), - slog.Int("status", v.Status), - slog.String("err", v.Error.Error()), - ) - } - return nil - }, -})) -``` - -Salida de ejemplo: - -```text -{"time":"2024-12-30T20:55:46.2399999+08:00","level":"INFO","msg":"REQUEST","uri":"/hello","status":200} -``` - -### Zerolog ([rs/zerolog](https://github.com/rs/zerolog)) - -```go -logger := zerolog.New(os.Stdout) -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogURI: true, - LogStatus: true, - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - logger.Info(). - Str("URI", v.URI). - Int("status", v.Status). - Msg("request") - return nil - }, -})) -``` - -Salida de ejemplo: - -```text -{"level":"info","URI":"/hello","status":200,"message":"request"} -``` - -### Zap ([uber-go/zap](https://github.com/uber-go/zap)) - -```go -logger, _ := zap.NewProduction() -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogURI: true, - LogStatus: true, - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - logger.Info("request", - zap.String("URI", v.URI), - zap.Int("status", v.Status), - ) - return nil - }, -})) -``` - -Salida de ejemplo: - -```text -{"level":"info","ts":1735564026.3197417,"caller":"cmd/main.go:20","msg":"request","URI":"/hello","status":200} -``` - -### Logrus ([sirupsen/logrus](https://github.com/sirupsen/logrus)) - -```go -log := logrus.New() -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogURI: true, - LogStatus: true, - LogValuesFunc: func(c *echo.Context, values middleware.RequestLoggerValues) error { - log.WithFields(logrus.Fields{ - "URI": values.URI, - "status": values.Status, - }).Info("request") - return nil - }, -})) -``` - -Salida de ejemplo: - -```text -time="2024-12-30T21:08:49+08:00" level=info msg=request URI=/hello status=200 -``` - -## Solución de problemas - -### panic: missing LogValuesFunc callback function for request logger middleware - -Este panic ocurre cuando el callback obligatorio `LogValuesFunc` se deja sin configurar. -Define una función que coincida con la firma de `LogValuesFunc` y asígnala en la configuración: - -```go -func logValues(c *echo.Context, v middleware.RequestLoggerValues) error { - fmt.Printf("Request Method: %s, URI: %s\n", v.Method, v.URI) - return nil -} - -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogValuesFunc: logValues, -})) -``` - -### Los parámetros en los logs están vacíos - -Si valores como `v.URI` y `v.Status` están vacíos dentro de `LogValuesFunc`, comprueba que los -flags de extracción correspondientes (`LogStatus`, `LogURI`, etc.) estén establecidos en `true` -en la configuración. Cada valor solo se rellena cuando su flag está habilitado. diff --git a/site/src/content/docs/es/middleware/logger.mdx b/site/src/content/docs/es/middleware/logger.mdx new file mode 100644 index 00000000..1c65a829 --- /dev/null +++ b/site/src/content/docs/es/middleware/logger.mdx @@ -0,0 +1,54 @@ +--- +title: Request Logger +description: Registra valores seleccionados de la petición y la respuesta con tu biblioteca de logging. +sidebar: + order: 12 +--- + +import { Code } from '@astrojs/starlight/components'; +import ConfigReference from '../../../../components/ConfigReference.astro'; +import example from '../../../../generated/request-logger.go.txt?raw'; + +`RequestLogger` entrega los valores seleccionados de la petición y la respuesta a la función obligatoria `LogValuesFunc`. Puedes usar `slog` u otra biblioteca. Cada campo `Log*` activa la extracción de un valor; por ejemplo, `LogStatus: true` rellena `v.Status`. + +## Ejemplos + +### slog ([log/slog](https://pkg.go.dev/log/slog)) + +Este programa completo registra la URI y el estado como JSON. Desde el repositorio de `echox`, ejecuta `cd reference/request-logger && go run .` y solicita `http://localhost:1323/`. + + + +La página importa el mismo archivo que compila la CI de documentación. `v.Error` recibe el error devuelto por el siguiente handler. Con `HandleError: true`, el manejador global escribe la respuesta antes de ejecutar `LogValuesFunc`; los middleware anteriores ya no pueden cambiar el estado ni el cuerpo. + +### fmt.Printf + +En una aplicación pequeña, `LogValuesFunc` puede usar `fmt.Printf`. Activa los campos `Log*` de los valores que quieras leer. + +### Zerolog ([rs/zerolog](https://github.com/rs/zerolog)) + +Pasa los valores seleccionados a Zerolog dentro de `LogValuesFunc`. + +### Zap ([uber-go/zap](https://github.com/uber-go/zap)) + +Usa la misma función para enviar los valores a Zap. + +### Logrus ([sirupsen/logrus](https://github.com/sirupsen/logrus)) + +Crea los campos de Logrus a partir de `RequestLoggerValues`. + +## Configuración + +La tabla se genera a partir de los campos exportados de la revisión indicada de Echo. No presupone valores predeterminados en tiempo de ejecución. + + + +## Solución de problemas + +### panic: missing LogValuesFunc callback function for request logger middleware + +`LogValuesFunc` es obligatoria. Asígnala antes de llamar a `RequestLoggerWithConfig`; el ejemplo ejecutable incluye esta función. + +### Los parámetros en los logs están vacíos + +Activa el campo de extracción correspondiente: `LogURI: true` para `v.URI` o `LogStatus: true` para `v.Status`. diff --git a/site/src/content/docs/es/middleware/static.md b/site/src/content/docs/es/middleware/static.md deleted file mode 100644 index c06f6358..00000000 --- a/site/src/content/docs/es/middleware/static.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -title: Static -description: Sirve archivos estáticos desde un directorio raíz. -sidebar: - order: 24 ---- - -El middleware Static sirve archivos estáticos desde el directorio raíz proporcionado. - -Todo el middleware principal reside en el paquete `middleware`: - -```go -import "github.com/labstack/echo/v5/middleware" -``` - -## Uso - -```go -e := echo.New() -e.Use(middleware.Static("/static")) -``` - -Esto sirve archivos estáticos desde el directorio `static`. Por ejemplo, un request a -`/js/main.js` obtiene y sirve el archivo `static/js/main.js`. - -## Configuración personalizada - -```go -e := echo.New() -e.Use(middleware.StaticWithConfig(middleware.StaticConfig{ - Root: "static", - Browse: true, -})) -``` - -Esto sirve archivos estáticos desde el directorio `static` y habilita la exploración de directorios. - -El comportamiento por defecto al usarse con paths de URL no raíz es anexar el path de la URL al -path del filesystem. - -#### Ejemplo 1 - -```go -group := root.Group("somepath") -group.Use(middleware.Static(filepath.Join("filesystempath"))) -// When an incoming request comes for `/somepath`, the actual filesystem request goes to -// `filesystempath/somepath` instead of only `filesystempath`. -group.GET("/*", func(c *echo.Context) error { return echo.ErrNotFound }) -``` - -:::note -El middleware a nivel de group está ligado a la ruta y solo funciona si el group tiene al menos una ruta. -::: - -:::tip -Para desactivar este comportamiento, establece el parámetro de configuración `IgnoreBase` en `true`. -::: - -#### Ejemplo 2 - -Servir assets de SPA desde un filesystem embebido: - -```go -package main - -import ( - "embed" - "net/http" - - "github.com/labstack/echo/v5" - "github.com/labstack/echo/v5/middleware" -) - -//go:embed assets -var webAssets embed.FS - -func main() { - e := echo.New() - - e.Use(middleware.StaticWithConfig(middleware.StaticConfig{ - HTML5: true, - Root: "assets", // files are located in the `assets` directory of the webAssets fs - Filesystem: webAssets, - })) - api := e.Group("/api") - api.GET("/users", func(c *echo.Context) error { - return c.String(http.StatusOK, "users") - }) - - if err := e.Start(":8080"); err != nil { - e.Logger.Error("failed to start server", "error", err) - } -} -``` - -## Configuración - -```go -type StaticConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Root directory from where the static content is served (relative to given Filesystem). - // `Root: "."` means root folder from Filesystem. - // Required. - Root string - - // Filesystem provides access to the static content. - // Optional. Defaults to echo.Filesystem (serves files from `.` folder where executable is started). - Filesystem fs.FS - - // Index file for serving a directory. - // Optional. Default value "index.html". - Index string - - // Enable HTML5 mode by forwarding all not-found requests to root so that - // a SPA (single-page application) can handle the routing. - // Optional. Default value false. - HTML5 bool - - // Enable directory browsing. - // Optional. Default value false. - Browse bool - - // Enable ignoring of the base of the URL path. - // Example: when assigning a static middleware to a non-root path group, - // the filesystem path is not doubled. - // Optional. Default value false. - IgnoreBase bool - - // DisablePathUnescaping disables path parameter (param: *) unescaping. This is useful when the router is set to - // unescape all parameters and doing it again in this middleware would corrupt the filename that is requested. - DisablePathUnescaping bool - - // DirectoryListTemplate is the template used to list directory contents. - // Optional. Defaults to the `directoryListHTMLTemplate` constant. - DirectoryListTemplate string -} -``` - -### Configuración por defecto - -```go -DefaultStaticConfig = StaticConfig{ - Skipper: DefaultSkipper, - Index: "index.html", -} -``` diff --git a/site/src/content/docs/es/middleware/static.mdx b/site/src/content/docs/es/middleware/static.mdx new file mode 100644 index 00000000..5429e9d9 --- /dev/null +++ b/site/src/content/docs/es/middleware/static.mdx @@ -0,0 +1,44 @@ +--- +title: Static +description: Sirve archivos de un directorio y mantiene seguras las rutas codificadas por defecto. +sidebar: + order: 24 +--- + +import { Code } from '@astrojs/starlight/components'; +import ConfigReference from '../../../../components/ConfigReference.astro'; +import example from '../../../../generated/static.go.txt?raw'; + +El middleware Static sirve archivos desde un directorio raíz. Este ejemplo sirve `public/index.html` en `/`. + +## Uso + +Desde el repositorio de `echox`, ejecuta `cd reference/static && go run .` y abre `http://localhost:1323/`. + + + +La página importa el mismo archivo que compila la CI de documentación. El ejemplo mantiene `EnablePathUnescaping` en `false`, el valor seguro por defecto para las barras codificadas. + +## Configuración personalizada + +`Root` indica el directorio que se sirve. `Browse` permite listar directorios, `HTML5` reenvía las rutas no encontradas al archivo índice y `Filesystem` acepta un `fs.FS`. Usa `IgnoreBase` cuando el prefijo URL de un grupo no debe formar parte de la ruta del archivo. + +#### Ejemplo 1 + +En un grupo que no está en la raíz, Echo normalmente incluye el prefijo URL del grupo en la ruta del archivo. Usa `IgnoreBase: true` si el directorio raíz ya incluye ese prefijo. El grupo necesita una ruta coincidente para ejecutar su middleware. + +#### Ejemplo 2 + +Para servir un sistema de archivos embebido, asigna tu `embed.FS` a `Filesystem` y el directorio de recursos a `Root`. Consulta el [ejemplo de recursos embebidos](/es/cookbook/embed-resources/). + +## Configuración + +Esta tabla procede de los campos exportados de la revisión indicada de Echo. Marca los campos obsoletos, pero no deduce valores predeterminados ni reglas de seguridad. + + + +### Configuración por defecto + +`Index` usa `index.html` por defecto. `EnablePathUnescaping` es `false` por defecto: las barras codificadas en la ruta comodín permanecen codificadas. `DisablePathUnescaping` está obsoleto y se ignora; usa `EnablePathUnescaping` si necesitas habilitar la decodificación. + +Habilítala solo si necesitas caracteres codificados en los nombres de archivo y tus rutas no restringen el acceso a subdirectorios. Decodificar una barra después del enrutamiento puede eludir una ruta de protección. diff --git a/site/src/content/docs/ja/middleware/logger.md b/site/src/content/docs/ja/middleware/logger.md deleted file mode 100644 index 8aebf46d..00000000 --- a/site/src/content/docs/ja/middleware/logger.md +++ /dev/null @@ -1,242 +0,0 @@ ---- -title: リクエストロガー -description: 構造化ログライブラリと統合できる、完全にカスタマイズ可能なリクエストログです。 -sidebar: - order: 12 ---- - -`RequestLogger` ミドルウェアは各 HTTP リクエストの情報をログに記録します。 -何をどのように記録するかを完全にカスタマイズできるため、サードパーティの -(構造化ログ)ライブラリとの利用に適しています。 - -logger が抽出できる値は、`RequestLoggerConfig` の bool フィールドと slice フィールドで制御されます。 -フィールドを有効化する(例:`LogStatus: true`)と、その値が `LogValuesFunc` に渡される -`RequestLoggerValues` に設定されます。 - -```go -type RequestLoggerConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // BeforeNextFunc is called before the next middleware or handler in the chain. - BeforeNextFunc func(c *echo.Context) - - // LogValuesFunc is called with the values extracted by the logger from the - // request/response. - // Mandatory. - LogValuesFunc func(c *echo.Context, v RequestLoggerValues) error - - // HandleError instructs the logger to call the global error handler when the next - // middleware/handler returns an error. A side effect is that the response is then - // committed and sent, so middlewares up the chain can no longer change the status - // code or body. - HandleError bool - - // LogLatency records the duration of the rest of the handler chain (the next(c) call). - LogLatency bool - // LogProtocol extracts the request protocol (for example HTTP/1.1 or HTTP/2). - LogProtocol bool - // LogRemoteIP extracts the request remote IP. See echo.Context.RealIP() for details. - LogRemoteIP bool - // LogHost extracts the request host value (for example example.com). - LogHost bool - // LogMethod extracts the request method (for example GET). - LogMethod bool - // LogURI extracts the request URI (for example /list?lang=en&page=1). - LogURI bool - // LogURIPath extracts the request URI path part (for example /list). - LogURIPath bool - // LogRoutePath extracts the route path the request matched (for example /user/:id). - LogRoutePath bool - // LogRequestID extracts the request ID from the X-Request-ID request header, or the - // response if the request did not have a value. - LogRequestID bool - // LogReferer extracts the request referer value. - LogReferer bool - // LogUserAgent extracts the request user agent value. - LogUserAgent bool - // LogStatus extracts the response status code. If the chain returns an echo.HTTPError, - // the status code is taken from it. - LogStatus bool - // LogError extracts the error returned from the handler chain. - LogError bool - // LogContentLength extracts the Content-Length header value. Note: this can differ - // from the actual request body size as it may be spoofed. - LogContentLength bool - // LogResponseSize extracts the response content length. Note: when used with Gzip - // middleware this value may not always be correct. - LogResponseSize bool - // LogHeaders extracts the given list of request headers. A slice of values is logged - // per header since a request can contain more than one. Names are canonicalized with - // http.CanonicalHeaderKey (for example "accept-encoding" becomes "Accept-Encoding"). - LogHeaders []string - // LogQueryParams extracts the given list of query parameters from the request URI. A - // slice of values is logged per name since a request can repeat a parameter. - LogQueryParams []string - // LogFormValues extracts the given list of form values from the request body and URI. - // A slice of values is logged per name since a request can repeat a value. - LogFormValues []string -} -``` - -すべてのコアミドルウェアは `middleware` パッケージに含まれています: - -```go -import "github.com/labstack/echo/v5/middleware" -``` - -## 例 - -### fmt.Printf - -```go -skipper := func(c *echo.Context) bool { - // Skip the health check endpoint. - return c.Request().URL.Path == "/health" -} -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogStatus: true, - LogURI: true, - Skipper: skipper, - BeforeNextFunc: func(c *echo.Context) { - c.Set("customValueFromContext", 42) - }, - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - value, _ := c.Get("customValueFromContext").(int) - fmt.Printf("REQUEST: uri: %v, status: %v, custom-value: %v\n", v.URI, v.Status, value) - return nil - }, -})) -``` - -出力例: - -```text -REQUEST: uri: /hello, status: 200, custom-value: 42 -``` - -### slog ([log/slog](https://pkg.go.dev/log/slog)) - -```go -logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogStatus: true, - LogURI: true, - LogError: true, - HandleError: true, // forwards the error to the global error handler so it can pick the status code - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - if v.Error == nil { - logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST", - slog.String("uri", v.URI), - slog.Int("status", v.Status), - ) - } else { - logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR", - slog.String("uri", v.URI), - slog.Int("status", v.Status), - slog.String("err", v.Error.Error()), - ) - } - return nil - }, -})) -``` - -出力例: - -```text -{"time":"2024-12-30T20:55:46.2399999+08:00","level":"INFO","msg":"REQUEST","uri":"/hello","status":200} -``` - -### Zerolog ([rs/zerolog](https://github.com/rs/zerolog)) - -```go -logger := zerolog.New(os.Stdout) -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogURI: true, - LogStatus: true, - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - logger.Info(). - Str("URI", v.URI). - Int("status", v.Status). - Msg("request") - return nil - }, -})) -``` - -出力例: - -```text -{"level":"info","URI":"/hello","status":200,"message":"request"} -``` - -### Zap ([uber-go/zap](https://github.com/uber-go/zap)) - -```go -logger, _ := zap.NewProduction() -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogURI: true, - LogStatus: true, - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - logger.Info("request", - zap.String("URI", v.URI), - zap.Int("status", v.Status), - ) - return nil - }, -})) -``` - -出力例: - -```text -{"level":"info","ts":1735564026.3197417,"caller":"cmd/main.go:20","msg":"request","URI":"/hello","status":200} -``` - -### Logrus ([sirupsen/logrus](https://github.com/sirupsen/logrus)) - -```go -log := logrus.New() -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogURI: true, - LogStatus: true, - LogValuesFunc: func(c *echo.Context, values middleware.RequestLoggerValues) error { - log.WithFields(logrus.Fields{ - "URI": values.URI, - "status": values.Status, - }).Info("request") - return nil - }, -})) -``` - -出力例: - -```text -time="2024-12-30T21:08:49+08:00" level=info msg=request URI=/hello status=200 -``` - -## トラブルシューティング - -### panic: missing LogValuesFunc callback function for request logger middleware - -この panic は、必須の `LogValuesFunc` callback が未設定のときに発生します。 -`LogValuesFunc` シグネチャに一致する関数を定義し、設定に割り当ててください。 - -```go -func logValues(c *echo.Context, v middleware.RequestLoggerValues) error { - fmt.Printf("Request Method: %s, URI: %s\n", v.Method, v.URI) - return nil -} - -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogValuesFunc: logValues, -})) -``` - -### ログ内のパラメーターが空 - -`LogValuesFunc` 内で `v.URI` や `v.Status` などが空の場合、対応する抽出フラグ -(`LogStatus`、`LogURI` など)が設定で `true` になっているか確認してください。 -各値は、そのフラグが有効な場合にのみ設定されます。 diff --git a/site/src/content/docs/ja/middleware/logger.mdx b/site/src/content/docs/ja/middleware/logger.mdx new file mode 100644 index 00000000..1253b7f9 --- /dev/null +++ b/site/src/content/docs/ja/middleware/logger.mdx @@ -0,0 +1,54 @@ +--- +title: Request Logger +description: 選択したリクエストとレスポンスの値を、任意のログライブラリで記録します。 +sidebar: + order: 12 +--- + +import { Code } from '@astrojs/starlight/components'; +import ConfigReference from '../../../../components/ConfigReference.astro'; +import example from '../../../../generated/request-logger.go.txt?raw'; + +`RequestLogger` は、選択したリクエストとレスポンスの値を必須の `LogValuesFunc` に渡します。この関数で `slog` などに記録できます。各 `Log*` フィールドは対応する値の取得を有効にします。たとえば `LogStatus: true` を設定すると `v.Status` に値が入ります。 + +## 例 + +### slog ([log/slog](https://pkg.go.dev/log/slog)) + +この完全なプログラムは URI とステータスを JSON で記録します。`echox` のソースディレクトリで `cd reference/request-logger && go run .` を実行し、`http://localhost:1323/` にアクセスしてください。 + + + +このページはドキュメントの CI がコンパイルするファイルをそのまま読み込みます。次のハンドラーがエラーを返すと `v.Error` に値が入ります。`HandleError: true` の場合、`LogValuesFunc` より前にグローバルエラーハンドラーがレスポンスを書き込むため、上流のミドルウェアはそのステータスや本文を変更できません。 + +### fmt.Printf + +小さなアプリケーションでは、`LogValuesFunc` から `fmt.Printf` を呼べます。読み取る値に対応する `Log*` フィールドを有効にしてください。 + +### Zerolog ([rs/zerolog](https://github.com/rs/zerolog)) + +`LogValuesFunc` 内で、選択した値を Zerolog に渡します。 + +### Zap ([uber-go/zap](https://github.com/uber-go/zap)) + +同じコールバックを使って値を Zap に渡せます。 + +### Logrus ([sirupsen/logrus](https://github.com/sirupsen/logrus)) + +`RequestLoggerValues` から Logrus のフィールドを作成します。 + +## 設定 + +この表は、記載された Echo リビジョンの公開フィールドから生成されます。実行時のデフォルト値は推測しません。 + + + +## トラブルシューティング + +### panic: missing LogValuesFunc callback function for request logger middleware + +`LogValuesFunc` は必須です。`RequestLoggerWithConfig` を呼ぶ前に設定してください。上の実行可能な例にも含まれています。 + +### ログ内のパラメーターが空 + +対応する取得フィールドを有効にしてください。`v.URI` には `LogURI: true`、`v.Status` には `LogStatus: true` が必要です。 diff --git a/site/src/content/docs/ja/middleware/static.md b/site/src/content/docs/ja/middleware/static.md deleted file mode 100644 index 802bfd62..00000000 --- a/site/src/content/docs/ja/middleware/static.md +++ /dev/null @@ -1,147 +0,0 @@ ---- -title: 静的ファイル -description: ルートディレクトリから静的ファイルを配信します。 -sidebar: - order: 24 ---- - -Static ミドルウェアは、指定されたルートディレクトリから静的ファイルを配信します。 - -すべてのコアミドルウェアは `middleware` パッケージに含まれています: - -```go -import "github.com/labstack/echo/v5/middleware" -``` - -## 使い方 - -```go -e := echo.New() -e.Use(middleware.Static("/static")) -``` - -これは `static` ディレクトリから静的ファイルを配信します。たとえば `/js/main.js` へのリクエストは -`static/js/main.js` ファイルを取得して配信します。 - -## カスタム設定 - -```go -e := echo.New() -e.Use(middleware.StaticWithConfig(middleware.StaticConfig{ - Root: "static", - Browse: true, -})) -``` - -これは `static` ディレクトリから静的ファイルを配信し、ディレクトリ閲覧を有効にします。 - -非ルート URL パスで使った場合のデフォルト挙動は、URL パスをファイルシステムパスに追加することです。 - -#### 例 1 - -```go -group := root.Group("somepath") -group.Use(middleware.Static(filepath.Join("filesystempath"))) -// When an incoming request comes for `/somepath`, the actual filesystem request goes to -// `filesystempath/somepath` instead of only `filesystempath`. -group.GET("/*", func(c *echo.Context) error { return echo.ErrNotFound }) -``` - -:::note -グループレベルのミドルウェアはルートに紐づいており、そのグループに少なくとも 1 つのルートがある場合のみ機能します。 -::: - -:::tip -この挙動を無効にするには、`IgnoreBase` 設定パラメーターを `true` にします。 -::: - -#### 例 2 - -埋め込みファイルシステムから SPA アセットを配信します。 - -```go -package main - -import ( - "embed" - "net/http" - - "github.com/labstack/echo/v5" - "github.com/labstack/echo/v5/middleware" -) - -//go:embed assets -var webAssets embed.FS - -func main() { - e := echo.New() - - e.Use(middleware.StaticWithConfig(middleware.StaticConfig{ - HTML5: true, - Root: "assets", // files are located in the `assets` directory of the webAssets fs - Filesystem: webAssets, - })) - api := e.Group("/api") - api.GET("/users", func(c *echo.Context) error { - return c.String(http.StatusOK, "users") - }) - - if err := e.Start(":8080"); err != nil { - e.Logger.Error("failed to start server", "error", err) - } -} -``` - -## 設定 - -```go -type StaticConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Root directory from where the static content is served (relative to given Filesystem). - // `Root: "."` means root folder from Filesystem. - // Required. - Root string - - // Filesystem provides access to the static content. - // Optional. Defaults to echo.Filesystem (serves files from `.` folder where executable is started). - Filesystem fs.FS - - // Index file for serving a directory. - // Optional. Default value "index.html". - Index string - - // Enable HTML5 mode by forwarding all not-found requests to root so that - // a SPA (single-page application) can handle the routing. - // Optional. Default value false. - HTML5 bool - - // Enable directory browsing. - // Optional. Default value false. - Browse bool - - // Enable ignoring of the base of the URL path. - // Example: when assigning a static middleware to a non-root path group, - // the filesystem path is not doubled. - // Optional. Default value false. - IgnoreBase bool - - // DisablePathUnescaping disables path parameter (param: *) unescaping. This is useful when the router is set to - // unescape all parameters and doing it again in this middleware would corrupt the filename that is requested. - DisablePathUnescaping bool - - // DirectoryListTemplate is the template used to list directory contents. - // Optional. Defaults to the `directoryListHTMLTemplate` constant. - DirectoryListTemplate string -} -``` - -### デフォルト設定 - -```go -DefaultStaticConfig = StaticConfig{ - Skipper: DefaultSkipper, - Index: "index.html", -} -``` diff --git a/site/src/content/docs/ja/middleware/static.mdx b/site/src/content/docs/ja/middleware/static.mdx new file mode 100644 index 00000000..17138539 --- /dev/null +++ b/site/src/content/docs/ja/middleware/static.mdx @@ -0,0 +1,44 @@ +--- +title: Static +description: ディレクトリ内のファイルを配信し、エンコードされたパスを安全な既定値で扱います。 +sidebar: + order: 24 +--- + +import { Code } from '@astrojs/starlight/components'; +import ConfigReference from '../../../../components/ConfigReference.astro'; +import example from '../../../../generated/static.go.txt?raw'; + +Static ミドルウェアはルートディレクトリからファイルを配信します。この例では `/` で `public/index.html` を配信します。 + +## 使い方 + +`echox` のソースディレクトリで `cd reference/static && go run .` を実行し、`http://localhost:1323/` を開いてください。 + + + +このページはドキュメントの CI がコンパイルするファイルをそのまま読み込みます。この例の `EnablePathUnescaping` は `false` で、エンコードされたスラッシュに対する安全な既定値です。 + +## カスタム設定 + +`Root` は配信するディレクトリです。`Browse` はディレクトリ一覧を有効にし、`HTML5` は見つからないパスをインデックスファイルに転送し、`Filesystem` には `fs.FS` を指定できます。グループの URL プレフィックスをファイルパスに含めたくない場合は `IgnoreBase` を使います。 + +#### 例 1 + +ルート以外のグループに Static ミドルウェアを設定すると、通常はグループの URL プレフィックスがファイルパスに含まれます。ファイルシステムのルートがすでにその場所を指している場合は `IgnoreBase: true` を設定します。グループのミドルウェアを実行するには、一致するルートが必要です。 + +#### 例 2 + +埋め込みファイルシステムを配信するには、`Filesystem` に `embed.FS`、`Root` にその中のアセットディレクトリを設定します。完全なプログラムは[埋め込みリソースのクックブック](/ja/cookbook/embed-resources/)を参照してください。 + +## 設定 + +この表は、記載された Echo リビジョンの公開フィールドから生成されます。非推奨フィールドを示しますが、デフォルト値やセキュリティ上の動作は推測しません。 + + + +### デフォルト設定 + +`Index` の既定値は `index.html` です。`EnablePathUnescaping` の既定値は `false` で、ワイルドカードパス内のエンコードされたスラッシュをデコードしません。`DisablePathUnescaping` は非推奨で、現在の Echo では無視されます。明示的にデコードが必要な場合は `EnablePathUnescaping` を使います。 + +ファイル名に URL エンコードされた文字が必要で、ルートでサブディレクトリへのアクセスを制限していない場合にのみ有効にしてください。ルーティング後にスラッシュをデコードすると、保護用ルートを回避できる場合があります。 diff --git a/site/src/content/docs/middleware/logger.md b/site/src/content/docs/middleware/logger.md deleted file mode 100644 index de2b6dae..00000000 --- a/site/src/content/docs/middleware/logger.md +++ /dev/null @@ -1,242 +0,0 @@ ---- -title: Request Logger -description: Fully customizable request logging that integrates with structured logging libraries. -sidebar: - order: 12 ---- - -`RequestLogger` middleware logs information about each HTTP request. It lets you fully -customize what is logged and how, making it well suited for use with third-party -(structured logging) libraries. - -The values the logger can extract are controlled by the boolean and slice fields of -`RequestLoggerConfig`. Enable a field (for example `LogStatus: true`) to have its value -populated on the `RequestLoggerValues` passed to your `LogValuesFunc`. - -```go -type RequestLoggerConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // BeforeNextFunc is called before the next middleware or handler in the chain. - BeforeNextFunc func(c *echo.Context) - - // LogValuesFunc is called with the values extracted by the logger from the - // request/response. - // Mandatory. - LogValuesFunc func(c *echo.Context, v RequestLoggerValues) error - - // HandleError instructs the logger to call the global error handler when the next - // middleware/handler returns an error. A side effect is that the response is then - // committed and sent, so middlewares up the chain can no longer change the status - // code or body. - HandleError bool - - // LogLatency records the duration of the rest of the handler chain (the next(c) call). - LogLatency bool - // LogProtocol extracts the request protocol (for example HTTP/1.1 or HTTP/2). - LogProtocol bool - // LogRemoteIP extracts the request remote IP. See echo.Context.RealIP() for details. - LogRemoteIP bool - // LogHost extracts the request host value (for example example.com). - LogHost bool - // LogMethod extracts the request method (for example GET). - LogMethod bool - // LogURI extracts the request URI (for example /list?lang=en&page=1). - LogURI bool - // LogURIPath extracts the request URI path part (for example /list). - LogURIPath bool - // LogRoutePath extracts the route path the request matched (for example /user/:id). - LogRoutePath bool - // LogRequestID extracts the request ID from the X-Request-ID request header, or the - // response if the request did not have a value. - LogRequestID bool - // LogReferer extracts the request referer value. - LogReferer bool - // LogUserAgent extracts the request user agent value. - LogUserAgent bool - // LogStatus extracts the response status code. If the chain returns an echo.HTTPError, - // the status code is taken from it. - LogStatus bool - // LogError extracts the error returned from the handler chain. - LogError bool - // LogContentLength extracts the Content-Length header value. Note: this can differ - // from the actual request body size as it may be spoofed. - LogContentLength bool - // LogResponseSize extracts the response content length. Note: when used with Gzip - // middleware this value may not always be correct. - LogResponseSize bool - // LogHeaders extracts the given list of request headers. A slice of values is logged - // per header since a request can contain more than one. Names are canonicalized with - // http.CanonicalHeaderKey (for example "accept-encoding" becomes "Accept-Encoding"). - LogHeaders []string - // LogQueryParams extracts the given list of query parameters from the request URI. A - // slice of values is logged per name since a request can repeat a parameter. - LogQueryParams []string - // LogFormValues extracts the given list of form values from the request body and URI. - // A slice of values is logged per name since a request can repeat a value. - LogFormValues []string -} -``` - -All core middleware lives in the `middleware` package: - -```go -import "github.com/labstack/echo/v5/middleware" -``` - -## Examples - -### fmt.Printf - -```go -skipper := func(c *echo.Context) bool { - // Skip the health check endpoint. - return c.Request().URL.Path == "/health" -} -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogStatus: true, - LogURI: true, - Skipper: skipper, - BeforeNextFunc: func(c *echo.Context) { - c.Set("customValueFromContext", 42) - }, - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - value, _ := c.Get("customValueFromContext").(int) - fmt.Printf("REQUEST: uri: %v, status: %v, custom-value: %v\n", v.URI, v.Status, value) - return nil - }, -})) -``` - -Sample output: - -```text -REQUEST: uri: /hello, status: 200, custom-value: 42 -``` - -### slog ([log/slog](https://pkg.go.dev/log/slog)) - -```go -logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogStatus: true, - LogURI: true, - LogError: true, - HandleError: true, // forwards the error to the global error handler so it can pick the status code - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - if v.Error == nil { - logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST", - slog.String("uri", v.URI), - slog.Int("status", v.Status), - ) - } else { - logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR", - slog.String("uri", v.URI), - slog.Int("status", v.Status), - slog.String("err", v.Error.Error()), - ) - } - return nil - }, -})) -``` - -Sample output: - -```text -{"time":"2024-12-30T20:55:46.2399999+08:00","level":"INFO","msg":"REQUEST","uri":"/hello","status":200} -``` - -### Zerolog ([rs/zerolog](https://github.com/rs/zerolog)) - -```go -logger := zerolog.New(os.Stdout) -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogURI: true, - LogStatus: true, - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - logger.Info(). - Str("URI", v.URI). - Int("status", v.Status). - Msg("request") - return nil - }, -})) -``` - -Sample output: - -```text -{"level":"info","URI":"/hello","status":200,"message":"request"} -``` - -### Zap ([uber-go/zap](https://github.com/uber-go/zap)) - -```go -logger, _ := zap.NewProduction() -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogURI: true, - LogStatus: true, - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - logger.Info("request", - zap.String("URI", v.URI), - zap.Int("status", v.Status), - ) - return nil - }, -})) -``` - -Sample output: - -```text -{"level":"info","ts":1735564026.3197417,"caller":"cmd/main.go:20","msg":"request","URI":"/hello","status":200} -``` - -### Logrus ([sirupsen/logrus](https://github.com/sirupsen/logrus)) - -```go -log := logrus.New() -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogURI: true, - LogStatus: true, - LogValuesFunc: func(c *echo.Context, values middleware.RequestLoggerValues) error { - log.WithFields(logrus.Fields{ - "URI": values.URI, - "status": values.Status, - }).Info("request") - return nil - }, -})) -``` - -Sample output: - -```text -time="2024-12-30T21:08:49+08:00" level=info msg=request URI=/hello status=200 -``` - -## Troubleshooting - -### panic: missing LogValuesFunc callback function for request logger middleware - -This panic occurs when the mandatory `LogValuesFunc` callback is left unset. Define a -function matching the `LogValuesFunc` signature and assign it in the configuration: - -```go -func logValues(c *echo.Context, v middleware.RequestLoggerValues) error { - fmt.Printf("Request Method: %s, URI: %s\n", v.Method, v.URI) - return nil -} - -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogValuesFunc: logValues, -})) -``` - -### Parameters in logs are empty - -If values such as `v.URI` and `v.Status` are empty inside `LogValuesFunc`, check that the -corresponding extraction flags (`LogStatus`, `LogURI`, and so on) are set to `true` in -the configuration. Each value is only populated when its flag is enabled. diff --git a/site/src/content/docs/middleware/logger.mdx b/site/src/content/docs/middleware/logger.mdx new file mode 100644 index 00000000..0b2f8268 --- /dev/null +++ b/site/src/content/docs/middleware/logger.mdx @@ -0,0 +1,54 @@ +--- +title: Request Logger +description: Log selected request and response values with your logging library. +sidebar: + order: 12 +--- + +import { Code } from '@astrojs/starlight/components'; +import ConfigReference from '../../../components/ConfigReference.astro'; +import example from '../../../generated/request-logger.go.txt?raw'; + +`RequestLogger` passes selected request and response values to a required `LogValuesFunc` callback. Use that callback to write to `slog` or another logger. Each `Log*` field controls which value is collected; for example, `LogStatus: true` populates `v.Status`. + +## Examples + +### slog ([log/slog](https://pkg.go.dev/log/slog)) + +This complete program logs the URI and status as JSON. From the `echox` checkout, run `cd reference/request-logger && go run .`, then request `http://localhost:1323/`. + + + +The page imports the same file that documentation CI compiles. `v.Error` is set when the next handler returns an error; there is no extra configuration field for it. With `HandleError: true`, Echo's global error handler writes the response before `LogValuesFunc` runs, so earlier middleware cannot then change its status or body. + +### fmt.Printf + +For a small application, `LogValuesFunc` can call `fmt.Printf` instead of `slog`. Keep the same callback signature and enable each value you read with its matching `Log*` field. + +### Zerolog ([rs/zerolog](https://github.com/rs/zerolog)) + +Pass `v.URI`, `v.Status`, and other selected values to your Zerolog logger inside `LogValuesFunc`. + +### Zap ([uber-go/zap](https://github.com/uber-go/zap)) + +Pass the selected values to Zap inside the same callback. Echo does not require a particular logging library. + +### Logrus ([sirupsen/logrus](https://github.com/sirupsen/logrus)) + +Build Logrus fields from `RequestLoggerValues` inside `LogValuesFunc`. + +## Configuration + +The table is generated from the exported fields in the recorded Echo revision. It does not claim runtime defaults. + + + +## Troubleshooting + +### panic: missing LogValuesFunc callback function for request logger middleware + +`LogValuesFunc` is required. Set it before calling `RequestLoggerWithConfig`; the runnable example above includes the callback. + +### Parameters in logs are empty + +Enable the corresponding extraction field, such as `LogURI: true` for `v.URI` or `LogStatus: true` for `v.Status`. Values not selected in the config remain empty. diff --git a/site/src/content/docs/middleware/static.md b/site/src/content/docs/middleware/static.md deleted file mode 100644 index 9b822549..00000000 --- a/site/src/content/docs/middleware/static.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -title: Static -description: Serve static files from a root directory. -sidebar: - order: 24 ---- - -Static middleware serves static files from the provided root directory. - -All core middleware lives in the `middleware` package: - -```go -import "github.com/labstack/echo/v5/middleware" -``` - -## Usage - -```go -e := echo.New() -e.Use(middleware.Static("/static")) -``` - -This serves static files from the `static` directory. For example, a request to `/js/main.js` -fetches and serves the `static/js/main.js` file. - -## Custom configuration - -```go -e := echo.New() -e.Use(middleware.StaticWithConfig(middleware.StaticConfig{ - Root: "static", - Browse: true, -})) -``` - -This serves static files from the `static` directory and enables directory browsing. - -The default behavior when used with non-root URL paths is to append the URL path to the -filesystem path. - -#### Example 1 - -```go -group := root.Group("somepath") -group.Use(middleware.Static(filepath.Join("filesystempath"))) -// When an incoming request comes for `/somepath`, the actual filesystem request goes to -// `filesystempath/somepath` instead of only `filesystempath`. -group.GET("/*", func(c *echo.Context) error { return echo.ErrNotFound }) -``` - -:::note -Group-level middleware is tied to the route and works only if the group has at least one route. -::: - -:::tip -To turn off this behavior, set the `IgnoreBase` config parameter to `true`. -::: - -#### Example 2 - -Serve SPA assets from an embedded filesystem: - -```go -package main - -import ( - "embed" - "net/http" - - "github.com/labstack/echo/v5" - "github.com/labstack/echo/v5/middleware" -) - -//go:embed assets -var webAssets embed.FS - -func main() { - e := echo.New() - - e.Use(middleware.StaticWithConfig(middleware.StaticConfig{ - HTML5: true, - Root: "assets", // files are located in the `assets` directory of the webAssets fs - Filesystem: webAssets, - })) - api := e.Group("/api") - api.GET("/users", func(c *echo.Context) error { - return c.String(http.StatusOK, "users") - }) - - if err := e.Start(":8080"); err != nil { - e.Logger.Error("failed to start server", "error", err) - } -} -``` - -## Configuration - -```go -type StaticConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Root directory from where the static content is served (relative to given Filesystem). - // `Root: "."` means root folder from Filesystem. - // Required. - Root string - - // Filesystem provides access to the static content. - // Optional. Defaults to echo.Filesystem (serves files from `.` folder where executable is started). - Filesystem fs.FS - - // Index file for serving a directory. - // Optional. Default value "index.html". - Index string - - // Enable HTML5 mode by forwarding all not-found requests to root so that - // a SPA (single-page application) can handle the routing. - // Optional. Default value false. - HTML5 bool - - // Enable directory browsing. - // Optional. Default value false. - Browse bool - - // Enable ignoring of the base of the URL path. - // Example: when assigning a static middleware to a non-root path group, - // the filesystem path is not doubled. - // Optional. Default value false. - IgnoreBase bool - - // DisablePathUnescaping disables path parameter (param: *) unescaping. This is useful when the router is set to - // unescape all parameters and doing it again in this middleware would corrupt the filename that is requested. - DisablePathUnescaping bool - - // DirectoryListTemplate is the template used to list directory contents. - // Optional. Defaults to the `directoryListHTMLTemplate` constant. - DirectoryListTemplate string -} -``` - -### Default configuration - -```go -DefaultStaticConfig = StaticConfig{ - Skipper: DefaultSkipper, - Index: "index.html", -} -``` diff --git a/site/src/content/docs/middleware/static.mdx b/site/src/content/docs/middleware/static.mdx new file mode 100644 index 00000000..006ea10f --- /dev/null +++ b/site/src/content/docs/middleware/static.mdx @@ -0,0 +1,44 @@ +--- +title: Static +description: Serve files from a directory and keep encoded paths safe by default. +sidebar: + order: 24 +--- + +import { Code } from '@astrojs/starlight/components'; +import ConfigReference from '../../../components/ConfigReference.astro'; +import example from '../../../generated/static.go.txt?raw'; + +Static middleware serves files from a root directory. The example below serves `public/index.html` at `/`. + +## Usage + +From the `echox` checkout, run `cd reference/static && go run .`, then open `http://localhost:1323/`. + + + +The page imports the same file that documentation CI compiles. The example keeps `EnablePathUnescaping` false, the safe default for encoded slashes. + +## Custom configuration + +Set `Root` to the directory to serve. `Browse` enables directory listings, `HTML5` forwards missing paths to the index file for a single-page application, and `Filesystem` lets you provide an `fs.FS`. Use `IgnoreBase` when a group's URL prefix should not become part of the file path. + +#### Example 1 + +When Static middleware is attached to a non-root group, Echo normally includes the group's URL prefix in the filesystem path. Set `IgnoreBase: true` if your filesystem root already points inside that prefix. A group needs a matching route for its middleware to run. + +#### Example 2 + +To serve an embedded filesystem, set `Filesystem` to your `embed.FS` and `Root` to the asset directory within it. See the [embed resources cookbook](/cookbook/embed-resources/) for a complete program. + +## Configuration + +The table comes from the exported fields in the recorded Echo revision. It identifies deprecated fields but does not infer defaults or security behavior. + + + +### Default configuration + +`Index` defaults to `index.html`. `EnablePathUnescaping` defaults to `false`, so encoded slashes in the wildcard path stay encoded. `DisablePathUnescaping` is deprecated and ignored by current Echo; use `EnablePathUnescaping` when you explicitly need unescaping. + +Enable unescaping only when you need URL-encoded characters in filenames and your route rules do not restrict access to subdirectories. Decoding an encoded slash after routing can bypass a route guard that matched the encoded path differently. Review the linked source in the table before enabling it. diff --git a/site/src/content/docs/pt-br/middleware/logger.md b/site/src/content/docs/pt-br/middleware/logger.md deleted file mode 100644 index a9e55a6d..00000000 --- a/site/src/content/docs/pt-br/middleware/logger.md +++ /dev/null @@ -1,242 +0,0 @@ ---- -title: Request Logger -description: Logging de requests totalmente customizável que se integra a bibliotecas de logging estruturado. -sidebar: - order: 12 ---- - -O middleware `RequestLogger` registra informações sobre cada request HTTP. Ele permite customizar -totalmente o que é registrado e como, tornando-o adequado para uso com bibliotecas de terceiros -(logging estruturado). - -Os valores que o logger pode extrair são controlados pelos campos booleanos e slices de -`RequestLoggerConfig`. Habilite um campo (por exemplo `LogStatus: true`) para ter seu valor -preenchido em `RequestLoggerValues`, que é passado para seu `LogValuesFunc`. - -```go -type RequestLoggerConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // BeforeNextFunc is called before the next middleware or handler in the chain. - BeforeNextFunc func(c *echo.Context) - - // LogValuesFunc is called with the values extracted by the logger from the - // request/response. - // Mandatory. - LogValuesFunc func(c *echo.Context, v RequestLoggerValues) error - - // HandleError instructs the logger to call the global error handler when the next - // middleware/handler returns an error. A side effect is that the response is then - // committed and sent, so middlewares up the chain can no longer change the status - // code or body. - HandleError bool - - // LogLatency records the duration of the rest of the handler chain (the next(c) call). - LogLatency bool - // LogProtocol extracts the request protocol (for example HTTP/1.1 or HTTP/2). - LogProtocol bool - // LogRemoteIP extracts the request remote IP. See echo.Context.RealIP() for details. - LogRemoteIP bool - // LogHost extracts the request host value (for example example.com). - LogHost bool - // LogMethod extracts the request method (for example GET). - LogMethod bool - // LogURI extracts the request URI (for example /list?lang=en&page=1). - LogURI bool - // LogURIPath extracts the request URI path part (for example /list). - LogURIPath bool - // LogRoutePath extracts the route path the request matched (for example /user/:id). - LogRoutePath bool - // LogRequestID extracts the request ID from the X-Request-ID request header, or the - // response if the request did not have a value. - LogRequestID bool - // LogReferer extracts the request referer value. - LogReferer bool - // LogUserAgent extracts the request user agent value. - LogUserAgent bool - // LogStatus extracts the response status code. If the chain returns an echo.HTTPError, - // the status code is taken from it. - LogStatus bool - // LogError extracts the error returned from the handler chain. - LogError bool - // LogContentLength extracts the Content-Length header value. Note: this can differ - // from the actual request body size as it may be spoofed. - LogContentLength bool - // LogResponseSize extracts the response content length. Note: when used with Gzip - // middleware this value may not always be correct. - LogResponseSize bool - // LogHeaders extracts the given list of request headers. A slice of values is logged - // per header since a request can contain more than one. Names are canonicalized with - // http.CanonicalHeaderKey (for example "accept-encoding" becomes "Accept-Encoding"). - LogHeaders []string - // LogQueryParams extracts the given list of query parameters from the request URI. A - // slice of values is logged per name since a request can repeat a parameter. - LogQueryParams []string - // LogFormValues extracts the given list of form values from the request body and URI. - // A slice of values is logged per name since a request can repeat a value. - LogFormValues []string -} -``` - -Todo o middleware principal fica no pacote `middleware`: - -```go -import "github.com/labstack/echo/v5/middleware" -``` - -## Exemplos - -### fmt.Printf - -```go -skipper := func(c *echo.Context) bool { - // Skip the health check endpoint. - return c.Request().URL.Path == "/health" -} -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogStatus: true, - LogURI: true, - Skipper: skipper, - BeforeNextFunc: func(c *echo.Context) { - c.Set("customValueFromContext", 42) - }, - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - value, _ := c.Get("customValueFromContext").(int) - fmt.Printf("REQUEST: uri: %v, status: %v, custom-value: %v\n", v.URI, v.Status, value) - return nil - }, -})) -``` - -Saída de exemplo: - -```text -REQUEST: uri: /hello, status: 200, custom-value: 42 -``` - -### slog ([log/slog](https://pkg.go.dev/log/slog)) - -```go -logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogStatus: true, - LogURI: true, - LogError: true, - HandleError: true, // forwards the error to the global error handler so it can pick the status code - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - if v.Error == nil { - logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST", - slog.String("uri", v.URI), - slog.Int("status", v.Status), - ) - } else { - logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR", - slog.String("uri", v.URI), - slog.Int("status", v.Status), - slog.String("err", v.Error.Error()), - ) - } - return nil - }, -})) -``` - -Saída de exemplo: - -```text -{"time":"2024-12-30T20:55:46.2399999+08:00","level":"INFO","msg":"REQUEST","uri":"/hello","status":200} -``` - -### Zerolog ([rs/zerolog](https://github.com/rs/zerolog)) - -```go -logger := zerolog.New(os.Stdout) -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogURI: true, - LogStatus: true, - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - logger.Info(). - Str("URI", v.URI). - Int("status", v.Status). - Msg("request") - return nil - }, -})) -``` - -Saída de exemplo: - -```text -{"level":"info","URI":"/hello","status":200,"message":"request"} -``` - -### Zap ([uber-go/zap](https://github.com/uber-go/zap)) - -```go -logger, _ := zap.NewProduction() -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogURI: true, - LogStatus: true, - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - logger.Info("request", - zap.String("URI", v.URI), - zap.Int("status", v.Status), - ) - return nil - }, -})) -``` - -Saída de exemplo: - -```text -{"level":"info","ts":1735564026.3197417,"caller":"cmd/main.go:20","msg":"request","URI":"/hello","status":200} -``` - -### Logrus ([sirupsen/logrus](https://github.com/sirupsen/logrus)) - -```go -log := logrus.New() -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogURI: true, - LogStatus: true, - LogValuesFunc: func(c *echo.Context, values middleware.RequestLoggerValues) error { - log.WithFields(logrus.Fields{ - "URI": values.URI, - "status": values.Status, - }).Info("request") - return nil - }, -})) -``` - -Saída de exemplo: - -```text -time="2024-12-30T21:08:49+08:00" level=info msg=request URI=/hello status=200 -``` - -## Solução de problemas - -### panic: missing LogValuesFunc callback function for request logger middleware - -Esse panic ocorre quando o callback obrigatório `LogValuesFunc` não é definido. Defina uma -função compatível com a assinatura `LogValuesFunc` e atribua-a na configuração: - -```go -func logValues(c *echo.Context, v middleware.RequestLoggerValues) error { - fmt.Printf("Request Method: %s, URI: %s\n", v.Method, v.URI) - return nil -} - -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogValuesFunc: logValues, -})) -``` - -### Parâmetros nos logs estão vazios - -Se valores como `v.URI` e `v.Status` estiverem vazios dentro de `LogValuesFunc`, verifique se as -flags de extração correspondentes (`LogStatus`, `LogURI` e assim por diante) estão definidas como `true` na -configuração. Cada valor só é preenchido quando sua flag está habilitada. diff --git a/site/src/content/docs/pt-br/middleware/logger.mdx b/site/src/content/docs/pt-br/middleware/logger.mdx new file mode 100644 index 00000000..70766c7b --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/logger.mdx @@ -0,0 +1,54 @@ +--- +title: Request Logger +description: Registre valores selecionados da requisição e da resposta com sua biblioteca de logs. +sidebar: + order: 12 +--- + +import { Code } from '@astrojs/starlight/components'; +import ConfigReference from '../../../../components/ConfigReference.astro'; +import example from '../../../../generated/request-logger.go.txt?raw'; + +`RequestLogger` envia valores selecionados da requisição e da resposta para a função obrigatória `LogValuesFunc`. Use `slog` ou outra biblioteca. Cada campo `Log*` ativa a coleta de um valor; por exemplo, `LogStatus: true` preenche `v.Status`. + +## Exemplos + +### slog ([log/slog](https://pkg.go.dev/log/slog)) + +Este programa completo registra a URI e o status em JSON. No repositório do `echox`, execute `cd reference/request-logger && go run .` e acesse `http://localhost:1323/`. + + + +A página importa o mesmo arquivo compilado pela CI da documentação. `v.Error` recebe o erro retornado pelo próximo handler. Com `HandleError: true`, o tratador global escreve a resposta antes de executar `LogValuesFunc`; middlewares anteriores já não podem alterar o status ou o corpo. + +### fmt.Printf + +Em uma aplicação pequena, `LogValuesFunc` pode usar `fmt.Printf`. Ative os campos `Log*` correspondentes aos valores lidos. + +### Zerolog ([rs/zerolog](https://github.com/rs/zerolog)) + +Envie os valores selecionados ao Zerolog dentro de `LogValuesFunc`. + +### Zap ([uber-go/zap](https://github.com/uber-go/zap)) + +Use a mesma função para enviar os valores ao Zap. + +### Logrus ([sirupsen/logrus](https://github.com/sirupsen/logrus)) + +Monte os campos do Logrus a partir de `RequestLoggerValues`. + +## Configuração + +A tabela é gerada dos campos exportados da revisão indicada do Echo. Ela não presume valores padrão em tempo de execução. + + + +## Solução de problemas + +### panic: missing LogValuesFunc callback function for request logger middleware + +`LogValuesFunc` é obrigatória. Defina a função antes de chamar `RequestLoggerWithConfig`; o exemplo executável inclui essa função. + +### Parâmetros nos logs estão vazios + +Ative o campo correspondente: `LogURI: true` para `v.URI` ou `LogStatus: true` para `v.Status`. diff --git a/site/src/content/docs/pt-br/middleware/static.md b/site/src/content/docs/pt-br/middleware/static.md deleted file mode 100644 index 474b61a8..00000000 --- a/site/src/content/docs/pt-br/middleware/static.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -title: Static -description: Sirva arquivos estáticos a partir de um diretório raiz. -sidebar: - order: 24 ---- - -O middleware Static serve arquivos estáticos a partir do diretório raiz fornecido. - -Todo o middleware principal fica no pacote `middleware`: - -```go -import "github.com/labstack/echo/v5/middleware" -``` - -## Uso - -```go -e := echo.New() -e.Use(middleware.Static("/static")) -``` - -Isso serve arquivos estáticos a partir do diretório `static`. Por exemplo, um request para `/js/main.js` -busca e serve o arquivo `static/js/main.js`. - -## Configuração customizada - -```go -e := echo.New() -e.Use(middleware.StaticWithConfig(middleware.StaticConfig{ - Root: "static", - Browse: true, -})) -``` - -Isso serve arquivos estáticos a partir do diretório `static` e habilita navegação de diretório. - -O comportamento padrão quando usado com caminhos de URL não raiz é anexar o caminho da URL ao -caminho do filesystem. - -#### Exemplo 1 - -```go -group := root.Group("somepath") -group.Use(middleware.Static(filepath.Join("filesystempath"))) -// When an incoming request comes for `/somepath`, the actual filesystem request goes to -// `filesystempath/somepath` instead of only `filesystempath`. -group.GET("/*", func(c *echo.Context) error { return echo.ErrNotFound }) -``` - -:::note -Middleware em nível de grupo é vinculado à rota e só funciona se o grupo tiver ao menos uma rota. -::: - -:::tip -Para desligar esse comportamento, defina o parâmetro de configuração `IgnoreBase` como `true`. -::: - -#### Exemplo 2 - -Sirva assets de SPA a partir de um filesystem incorporado: - -```go -package main - -import ( - "embed" - "net/http" - - "github.com/labstack/echo/v5" - "github.com/labstack/echo/v5/middleware" -) - -//go:embed assets -var webAssets embed.FS - -func main() { - e := echo.New() - - e.Use(middleware.StaticWithConfig(middleware.StaticConfig{ - HTML5: true, - Root: "assets", // files are located in the `assets` directory of the webAssets fs - Filesystem: webAssets, - })) - api := e.Group("/api") - api.GET("/users", func(c *echo.Context) error { - return c.String(http.StatusOK, "users") - }) - - if err := e.Start(":8080"); err != nil { - e.Logger.Error("failed to start server", "error", err) - } -} -``` - -## Configuração - -```go -type StaticConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Root directory from where the static content is served (relative to given Filesystem). - // `Root: "."` means root folder from Filesystem. - // Required. - Root string - - // Filesystem provides access to the static content. - // Optional. Defaults to echo.Filesystem (serves files from `.` folder where executable is started). - Filesystem fs.FS - - // Index file for serving a directory. - // Optional. Default value "index.html". - Index string - - // Enable HTML5 mode by forwarding all not-found requests to root so that - // a SPA (single-page application) can handle the routing. - // Optional. Default value false. - HTML5 bool - - // Enable directory browsing. - // Optional. Default value false. - Browse bool - - // Enable ignoring of the base of the URL path. - // Example: when assigning a static middleware to a non-root path group, - // the filesystem path is not doubled. - // Optional. Default value false. - IgnoreBase bool - - // DisablePathUnescaping disables path parameter (param: *) unescaping. This is useful when the router is set to - // unescape all parameters and doing it again in this middleware would corrupt the filename that is requested. - DisablePathUnescaping bool - - // DirectoryListTemplate is the template used to list directory contents. - // Optional. Defaults to the `directoryListHTMLTemplate` constant. - DirectoryListTemplate string -} -``` - -### Configuração padrão - -```go -DefaultStaticConfig = StaticConfig{ - Skipper: DefaultSkipper, - Index: "index.html", -} -``` diff --git a/site/src/content/docs/pt-br/middleware/static.mdx b/site/src/content/docs/pt-br/middleware/static.mdx new file mode 100644 index 00000000..57a9d329 --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/static.mdx @@ -0,0 +1,44 @@ +--- +title: Static +description: Sirva arquivos de um diretório mantendo caminhos codificados seguros por padrão. +sidebar: + order: 24 +--- + +import { Code } from '@astrojs/starlight/components'; +import ConfigReference from '../../../../components/ConfigReference.astro'; +import example from '../../../../generated/static.go.txt?raw'; + +O middleware Static serve arquivos de um diretório raiz. O exemplo serve `public/index.html` em `/`. + +## Uso + +No repositório do `echox`, execute `cd reference/static && go run .` e abra `http://localhost:1323/`. + + + +A página importa o mesmo arquivo compilado pela CI da documentação. O exemplo mantém `EnablePathUnescaping` em `false`, o padrão seguro para barras codificadas. + +## Configuração customizada + +`Root` define o diretório servido. `Browse` habilita a listagem de diretórios, `HTML5` encaminha caminhos não encontrados ao arquivo de índice e `Filesystem` aceita um `fs.FS`. Use `IgnoreBase` quando o prefixo da URL de um grupo não deve entrar no caminho do arquivo. + +#### Exemplo 1 + +Em um grupo fora da raiz, o Echo normalmente inclui o prefixo da URL do grupo no caminho do arquivo. Use `IgnoreBase: true` se o diretório raiz já incluir esse prefixo. O grupo precisa de uma rota correspondente para executar seu middleware. + +#### Exemplo 2 + +Para servir um sistema de arquivos incorporado, atribua seu `embed.FS` a `Filesystem` e o diretório dos recursos a `Root`. Veja o [cookbook de recursos incorporados](/pt-br/cookbook/embed-resources/). + +## Configuração + +A tabela vem dos campos exportados da revisão indicada do Echo. Ela marca campos obsoletos, mas não infere padrões nem regras de segurança. + + + +### Configuração padrão + +`Index` usa `index.html` por padrão. `EnablePathUnescaping` é `false` por padrão: barras codificadas no caminho curinga continuam codificadas. `DisablePathUnescaping` está obsoleto e é ignorado; use `EnablePathUnescaping` quando precisar habilitar a decodificação. + +Habilite-a somente se precisar de caracteres codificados em nomes de arquivos e se suas rotas não restringirem o acesso a subdiretórios. Decodificar uma barra após o roteamento pode contornar uma rota de proteção. diff --git a/site/src/content/docs/zh-cn/middleware/logger.md b/site/src/content/docs/zh-cn/middleware/logger.md deleted file mode 100644 index 4dc8ea98..00000000 --- a/site/src/content/docs/zh-cn/middleware/logger.md +++ /dev/null @@ -1,240 +0,0 @@ ---- -title: 请求日志 -description: 可完全自定义的请求日志记录,可与结构化日志库集成。 -sidebar: - order: 12 ---- - -`RequestLogger` 中间件会记录每个 HTTP 请求的信息。它让你完全自定义记录内容和方式, -非常适合搭配第三方(结构化日志)库使用。 - -logger 可以提取的值由 `RequestLoggerConfig` 的布尔字段和切片字段控制。启用某个字段 -(例如 `LogStatus: true`)后,该字段的值会填充到传给 `LogValuesFunc` 的 -`RequestLoggerValues` 上。 - -```go -type RequestLoggerConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // BeforeNextFunc is called before the next middleware or handler in the chain. - BeforeNextFunc func(c *echo.Context) - - // LogValuesFunc is called with the values extracted by the logger from the - // request/response. - // Mandatory. - LogValuesFunc func(c *echo.Context, v RequestLoggerValues) error - - // HandleError instructs the logger to call the global error handler when the next - // middleware/handler returns an error. A side effect is that the response is then - // committed and sent, so middlewares up the chain can no longer change the status - // code or body. - HandleError bool - - // LogLatency records the duration of the rest of the handler chain (the next(c) call). - LogLatency bool - // LogProtocol extracts the request protocol (for example HTTP/1.1 or HTTP/2). - LogProtocol bool - // LogRemoteIP extracts the request remote IP. See echo.Context.RealIP() for details. - LogRemoteIP bool - // LogHost extracts the request host value (for example example.com). - LogHost bool - // LogMethod extracts the request method (for example GET). - LogMethod bool - // LogURI extracts the request URI (for example /list?lang=en&page=1). - LogURI bool - // LogURIPath extracts the request URI path part (for example /list). - LogURIPath bool - // LogRoutePath extracts the route path the request matched (for example /user/:id). - LogRoutePath bool - // LogRequestID extracts the request ID from the X-Request-ID request header, or the - // response if the request did not have a value. - LogRequestID bool - // LogReferer extracts the request referer value. - LogReferer bool - // LogUserAgent extracts the request user agent value. - LogUserAgent bool - // LogStatus extracts the response status code. If the chain returns an echo.HTTPError, - // the status code is taken from it. - LogStatus bool - // LogError extracts the error returned from the handler chain. - LogError bool - // LogContentLength extracts the Content-Length header value. Note: this can differ - // from the actual request body size as it may be spoofed. - LogContentLength bool - // LogResponseSize extracts the response content length. Note: when used with Gzip - // middleware this value may not always be correct. - LogResponseSize bool - // LogHeaders extracts the given list of request headers. A slice of values is logged - // per header since a request can contain more than one. Names are canonicalized with - // http.CanonicalHeaderKey (for example "accept-encoding" becomes "Accept-Encoding"). - LogHeaders []string - // LogQueryParams extracts the given list of query parameters from the request URI. A - // slice of values is logged per name since a request can repeat a parameter. - LogQueryParams []string - // LogFormValues extracts the given list of form values from the request body and URI. - // A slice of values is logged per name since a request can repeat a value. - LogFormValues []string -} -``` - -所有核心中间件都位于 `middleware` 包中: - -```go -import "github.com/labstack/echo/v5/middleware" -``` - -## 示例 - -### fmt.Printf - -```go -skipper := func(c *echo.Context) bool { - // Skip the health check endpoint. - return c.Request().URL.Path == "/health" -} -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogStatus: true, - LogURI: true, - Skipper: skipper, - BeforeNextFunc: func(c *echo.Context) { - c.Set("customValueFromContext", 42) - }, - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - value, _ := c.Get("customValueFromContext").(int) - fmt.Printf("REQUEST: uri: %v, status: %v, custom-value: %v\n", v.URI, v.Status, value) - return nil - }, -})) -``` - -示例输出: - -```text -REQUEST: uri: /hello, status: 200, custom-value: 42 -``` - -### slog ([log/slog](https://pkg.go.dev/log/slog)) - -```go -logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogStatus: true, - LogURI: true, - LogError: true, - HandleError: true, // forwards the error to the global error handler so it can pick the status code - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - if v.Error == nil { - logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST", - slog.String("uri", v.URI), - slog.Int("status", v.Status), - ) - } else { - logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR", - slog.String("uri", v.URI), - slog.Int("status", v.Status), - slog.String("err", v.Error.Error()), - ) - } - return nil - }, -})) -``` - -示例输出: - -```text -{"time":"2024-12-30T20:55:46.2399999+08:00","level":"INFO","msg":"REQUEST","uri":"/hello","status":200} -``` - -### Zerolog ([rs/zerolog](https://github.com/rs/zerolog)) - -```go -logger := zerolog.New(os.Stdout) -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogURI: true, - LogStatus: true, - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - logger.Info(). - Str("URI", v.URI). - Int("status", v.Status). - Msg("request") - return nil - }, -})) -``` - -示例输出: - -```text -{"level":"info","URI":"/hello","status":200,"message":"request"} -``` - -### Zap ([uber-go/zap](https://github.com/uber-go/zap)) - -```go -logger, _ := zap.NewProduction() -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogURI: true, - LogStatus: true, - LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { - logger.Info("request", - zap.String("URI", v.URI), - zap.Int("status", v.Status), - ) - return nil - }, -})) -``` - -示例输出: - -```text -{"level":"info","ts":1735564026.3197417,"caller":"cmd/main.go:20","msg":"request","URI":"/hello","status":200} -``` - -### Logrus ([sirupsen/logrus](https://github.com/sirupsen/logrus)) - -```go -log := logrus.New() -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogURI: true, - LogStatus: true, - LogValuesFunc: func(c *echo.Context, values middleware.RequestLoggerValues) error { - log.WithFields(logrus.Fields{ - "URI": values.URI, - "status": values.Status, - }).Info("request") - return nil - }, -})) -``` - -示例输出: - -```text -time="2024-12-30T21:08:49+08:00" level=info msg=request URI=/hello status=200 -``` - -## 故障排除 - -### panic: missing LogValuesFunc callback function for request logger middleware - -当必需的 `LogValuesFunc` 回调未设置时,会发生这个 panic。定义一个匹配 -`LogValuesFunc` 签名的函数,并在配置中赋值: - -```go -func logValues(c *echo.Context, v middleware.RequestLoggerValues) error { - fmt.Printf("Request Method: %s, URI: %s\n", v.Method, v.URI) - return nil -} - -e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogValuesFunc: logValues, -})) -``` - -### 日志中的参数为空 - -如果 `LogValuesFunc` 内的 `v.URI`、`v.Status` 等值为空,请检查对应的提取标志 -(`LogStatus`、`LogURI` 等)是否在配置中设为 `true`。只有启用对应标志时,值才会被填充。 diff --git a/site/src/content/docs/zh-cn/middleware/logger.mdx b/site/src/content/docs/zh-cn/middleware/logger.mdx new file mode 100644 index 00000000..f18c7be6 --- /dev/null +++ b/site/src/content/docs/zh-cn/middleware/logger.mdx @@ -0,0 +1,54 @@ +--- +title: Request Logger +description: 使用任意日志库记录选定的请求和响应数据。 +sidebar: + order: 12 +--- + +import { Code } from '@astrojs/starlight/components'; +import ConfigReference from '../../../../components/ConfigReference.astro'; +import example from '../../../../generated/request-logger.go.txt?raw'; + +`RequestLogger` 将选定的请求和响应数据传给必填的 `LogValuesFunc` 回调。你可以在回调中使用 `slog` 或其他日志库。每个 `Log*` 字段控制对应数据的提取;例如,`LogStatus: true` 会填充 `v.Status`。 + +## 示例 + +### slog ([log/slog](https://pkg.go.dev/log/slog)) + +下面的完整程序以 JSON 格式记录 URI 和状态码。在 `echox` 源码目录运行 `cd reference/request-logger && go run .`,然后访问 `http://localhost:1323/`。 + + + +本页直接导入文档 CI 编译的同一个文件。下一个处理器返回错误时,`v.Error` 会包含该错误。设置 `HandleError: true` 后,全局错误处理器会先写入响应,再调用 `LogValuesFunc`;上游中间件此时无法更改状态码或响应体。 + +### fmt.Printf + +在小型应用中,`LogValuesFunc` 可以调用 `fmt.Printf`。记得启用所读取数据对应的 `Log*` 字段。 + +### Zerolog ([rs/zerolog](https://github.com/rs/zerolog)) + +在 `LogValuesFunc` 中将选定的数据传给 Zerolog。 + +### Zap ([uber-go/zap](https://github.com/uber-go/zap)) + +同一个回调也可以将数据传给 Zap。 + +### Logrus ([sirupsen/logrus](https://github.com/sirupsen/logrus)) + +从 `RequestLoggerValues` 构造 Logrus 字段。 + +## 配置 + +下表由所标注 Echo 修订版本的导出字段生成,不推断运行时默认值。 + + + +## 故障排除 + +### panic: missing LogValuesFunc callback function for request logger middleware + +`LogValuesFunc` 为必填项。调用 `RequestLoggerWithConfig` 前需设置它;上面的可运行示例已包含该回调。 + +### 日志中的参数为空 + +启用对应的提取字段:`v.URI` 需要 `LogURI: true`,`v.Status` 需要 `LogStatus: true`。 diff --git a/site/src/content/docs/zh-cn/middleware/static.md b/site/src/content/docs/zh-cn/middleware/static.md deleted file mode 100644 index d1346ade..00000000 --- a/site/src/content/docs/zh-cn/middleware/static.md +++ /dev/null @@ -1,147 +0,0 @@ ---- -title: 静态文件 -description: 从根目录提供静态文件。 -sidebar: - order: 24 ---- - -Static 中间件会从提供的根目录提供静态文件。 - -所有核心中间件都位于 `middleware` 包中: - -```go -import "github.com/labstack/echo/v5/middleware" -``` - -## 用法 - -```go -e := echo.New() -e.Use(middleware.Static("/static")) -``` - -这会从 `static` 目录提供静态文件。例如,对 `/js/main.js` 的请求会获取并提供 -`static/js/main.js` 文件。 - -## 自定义配置 - -```go -e := echo.New() -e.Use(middleware.StaticWithConfig(middleware.StaticConfig{ - Root: "static", - Browse: true, -})) -``` - -这会从 `static` 目录提供静态文件,并启用目录浏览。 - -与非根 URL 路径一起使用时,默认行为是把 URL 路径追加到文件系统路径。 - -#### 示例 1 - -```go -group := root.Group("somepath") -group.Use(middleware.Static(filepath.Join("filesystempath"))) -// When an incoming request comes for `/somepath`, the actual filesystem request goes to -// `filesystempath/somepath` instead of only `filesystempath`. -group.GET("/*", func(c *echo.Context) error { return echo.ErrNotFound }) -``` - -:::note -路由组级中间件与路由绑定,只有当路由组至少有一条路由时才会工作。 -::: - -:::tip -要关闭此行为,请把 `IgnoreBase` 配置参数设为 `true`。 -::: - -#### 示例 2 - -从嵌入式文件系统提供 SPA 资源: - -```go -package main - -import ( - "embed" - "net/http" - - "github.com/labstack/echo/v5" - "github.com/labstack/echo/v5/middleware" -) - -//go:embed assets -var webAssets embed.FS - -func main() { - e := echo.New() - - e.Use(middleware.StaticWithConfig(middleware.StaticConfig{ - HTML5: true, - Root: "assets", // files are located in the `assets` directory of the webAssets fs - Filesystem: webAssets, - })) - api := e.Group("/api") - api.GET("/users", func(c *echo.Context) error { - return c.String(http.StatusOK, "users") - }) - - if err := e.Start(":8080"); err != nil { - e.Logger.Error("failed to start server", "error", err) - } -} -``` - -## 配置 - -```go -type StaticConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Root directory from where the static content is served (relative to given Filesystem). - // `Root: "."` means root folder from Filesystem. - // Required. - Root string - - // Filesystem provides access to the static content. - // Optional. Defaults to echo.Filesystem (serves files from `.` folder where executable is started). - Filesystem fs.FS - - // Index file for serving a directory. - // Optional. Default value "index.html". - Index string - - // Enable HTML5 mode by forwarding all not-found requests to root so that - // a SPA (single-page application) can handle the routing. - // Optional. Default value false. - HTML5 bool - - // Enable directory browsing. - // Optional. Default value false. - Browse bool - - // Enable ignoring of the base of the URL path. - // Example: when assigning a static middleware to a non-root path group, - // the filesystem path is not doubled. - // Optional. Default value false. - IgnoreBase bool - - // DisablePathUnescaping disables path parameter (param: *) unescaping. This is useful when the router is set to - // unescape all parameters and doing it again in this middleware would corrupt the filename that is requested. - DisablePathUnescaping bool - - // DirectoryListTemplate is the template used to list directory contents. - // Optional. Defaults to the `directoryListHTMLTemplate` constant. - DirectoryListTemplate string -} -``` - -### 默认配置 - -```go -DefaultStaticConfig = StaticConfig{ - Skipper: DefaultSkipper, - Index: "index.html", -} -``` diff --git a/site/src/content/docs/zh-cn/middleware/static.mdx b/site/src/content/docs/zh-cn/middleware/static.mdx new file mode 100644 index 00000000..7d098897 --- /dev/null +++ b/site/src/content/docs/zh-cn/middleware/static.mdx @@ -0,0 +1,44 @@ +--- +title: Static +description: 从目录提供文件,并默认安全地处理编码路径。 +sidebar: + order: 24 +--- + +import { Code } from '@astrojs/starlight/components'; +import ConfigReference from '../../../../components/ConfigReference.astro'; +import example from '../../../../generated/static.go.txt?raw'; + +Static 中间件从根目录提供文件。下面的示例在 `/` 提供 `public/index.html`。 + +## 用法 + +在 `echox` 源码目录运行 `cd reference/static && go run .`,然后打开 `http://localhost:1323/`。 + + + +本页直接导入文档 CI 编译的同一个文件。示例将 `EnablePathUnescaping` 保持为 `false`,这是处理编码斜杠的安全默认值。 + +## 自定义配置 + +`Root` 指定要提供的目录。`Browse` 启用目录列表,`HTML5` 将未找到的路径转到索引文件,`Filesystem` 可接收 `fs.FS`。如果分组的 URL 前缀不应成为文件路径的一部分,请使用 `IgnoreBase`。 + +#### 示例 1 + +将 Static 中间件附加到非根路径的分组时,Echo 通常会将分组的 URL 前缀加入文件路径。如果文件系统根目录已经包含该前缀,请设置 `IgnoreBase: true`。分组需要有匹配的路由才能运行其中间件。 + +#### 示例 2 + +要提供嵌入式文件系统,请将 `embed.FS` 赋给 `Filesystem`,并将其中的资源目录设为 `Root`。完整程序见[嵌入资源示例](/zh-cn/cookbook/embed-resources/)。 + +## 配置 + +下表由所标注 Echo 修订版本的导出字段生成。它标记弃用字段,但不推断默认值或安全行为。 + + + +### 默认配置 + +`Index` 默认为 `index.html`。`EnablePathUnescaping` 默认为 `false`,因此通配符路径中的编码斜杠不会被解码。`DisablePathUnescaping` 已弃用,当前 Echo 会忽略它;确实需要解码时请使用 `EnablePathUnescaping`。 + +只有在文件名需要 URL 编码字符,且路由规则不限制子目录访问时才启用解码。在路由匹配后解码斜杠可能绕过保护性路由。 diff --git a/site/translation-baseline.json b/site/translation-baseline.json new file mode 100644 index 00000000..67f9fdf3 --- /dev/null +++ b/site/translation-baseline.json @@ -0,0 +1,18 @@ +{ + "es": { + "logger": "53b60faa99c9656e18cf4abad32e9716768586b71ba83a67f619bd596732a99f", + "static": "3992dc8bcbc8b8cac1ec8c89e9e9ea0e26b804053b7658b9a294e7e1926de21d" + }, + "ja": { + "logger": "53b60faa99c9656e18cf4abad32e9716768586b71ba83a67f619bd596732a99f", + "static": "3992dc8bcbc8b8cac1ec8c89e9e9ea0e26b804053b7658b9a294e7e1926de21d" + }, + "pt-br": { + "logger": "53b60faa99c9656e18cf4abad32e9716768586b71ba83a67f619bd596732a99f", + "static": "3992dc8bcbc8b8cac1ec8c89e9e9ea0e26b804053b7658b9a294e7e1926de21d" + }, + "zh-cn": { + "logger": "53b60faa99c9656e18cf4abad32e9716768586b71ba83a67f619bd596732a99f", + "static": "3992dc8bcbc8b8cac1ec8c89e9e9ea0e26b804053b7658b9a294e7e1926de21d" + } +} From 31af7432495cd216ca1f5417db79358d2bc7d0f6 Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Sun, 27 Sep 2026 11:28:12 -0700 Subject: [PATCH 2/5] docs: retain API reference tool trial in echox --- docs/api-reference-tool-trial.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 docs/api-reference-tool-trial.md diff --git a/docs/api-reference-tool-trial.md b/docs/api-reference-tool-trial.md new file mode 100644 index 00000000..c1edaf0e --- /dev/null +++ b/docs/api-reference-tool-trial.md @@ -0,0 +1,24 @@ +# API reference tool trial + +The first trial used `gomarkdoc` v1.1.0 against the pinned Echo checkout's +`middleware` package, which includes CORS, Request Logger, and Static, and +against the external `github.com/labstack/echo-jwt/v5` package from the current +`echox` module. The package output was 2,422 lines for core middleware and 389 +lines for JWT. `gomarkdoc --check --output ...` successfully detected that the +unmodified generated core file matched its source. The output included +`EnablePathUnescaping` and excluded the removed `RequestLoggerConfig.LogError`. + +`--embed` supports marked regions within authored Markdown. A check against an +unmarked, fully generated file failed because embed mode would append a second +generated block. This confirms that an existing page must add embed markers +before using that mode. Templates can change generated Markdown, but this +package-wide output is too broad for individual middleware pages and is not a +machine-readable field manifest. Those two requirements motivate the small +`config-fields` extractor in `reference/`. It extracts only fields, types, +deprecation markers, and source positions; it does not infer defaults or +rewrite authored pages. + +The site currently renders field tables from that extractor. A targeted +`gomarkdoc` template and marked embedding on a real page remain to be evaluated +for signatures and other reference sections. This trial does not justify +generating behavior explanations. From 8799207f577f56ba8ce01a6e1fe805ad69d541ae Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Sun, 27 Sep 2026 15:22:51 -0700 Subject: [PATCH 3/5] docs: build source-aligned stable and next references --- .gitignore | 1 + README.md | 41 +- reference/cmd/config-fields/main.go | 47 +- reference/cmd/config-fields/main_test.go | 5 + site/astro.config.mjs | 7 +- site/echo-source.json | 3 +- site/external-baseline.json | 218 ++++++ site/external-sources.json | 20 + site/next-source.json | 5 + site/package.json | 8 +- site/performance-baseline.json | 80 +++ site/reference-baseline.json | 61 ++ site/reference-pages.json | 22 + site/route-baseline.json | 680 ++++++++++++++++++ site/scripts/accept-source.mjs | 16 +- site/scripts/accept-translations.mjs | 31 +- site/scripts/build-site.mjs | 41 ++ site/scripts/check-performance.mjs | 52 ++ site/scripts/check-site.mjs | 96 +++ site/scripts/check-source.mjs | 58 +- site/scripts/prepare-echo-source.mjs | 18 +- site/scripts/translation-sections.mjs | 31 + site/scripts/translation-status.mjs | 37 +- site/src/components/ConfigReference.astro | 52 +- site/src/components/HomeHero.astro | 6 +- site/src/components/Search.astro | 13 +- site/src/components/VersionBanner.astro | 32 + .../docs/cookbook/http2-server-push.md | 2 +- .../docs/es/cookbook/http2-server-push.md | 2 +- site/src/content/docs/es/cookbook/http2.md | 2 + site/src/content/docs/es/guide/binding.md | 2 + .../content/docs/es/guide/customization.md | 2 + site/src/content/docs/es/guide/request.md | 2 + .../{basic-auth.md => basic-auth.mdx} | 24 +- .../{body-dump.md => body-dump.mdx} | 25 +- .../{body-limit.md => body-limit.mdx} | 12 +- ...context-timeout.md => context-timeout.mdx} | 15 +- site/src/content/docs/es/middleware/cors.md | 139 ---- site/src/content/docs/es/middleware/cors.mdx | 68 ++ .../docs/es/middleware/{csrf.md => csrf.mdx} | 72 +- .../{decompress.md => decompress.mdx} | 19 +- .../docs/es/middleware/{gzip.md => gzip.mdx} | 22 +- site/src/content/docs/es/middleware/index.md | 30 + site/src/content/docs/es/middleware/jwt.md | 126 ---- site/src/content/docs/es/middleware/jwt.mdx | 45 ++ .../content/docs/es/middleware/key-auth.md | 97 --- .../content/docs/es/middleware/key-auth.mdx | 59 ++ ...method-override.md => method-override.mdx} | 13 +- .../{open-telemetry.md => open-telemetry.mdx} | 6 + .../{prometheus.md => prometheus.mdx} | 10 + site/src/content/docs/es/middleware/proxy.md | 140 ---- site/src/content/docs/es/middleware/proxy.mdx | 79 ++ .../{rate-limiter.md => rate-limiter.mdx} | 19 +- .../es/middleware/{recover.md => recover.mdx} | 22 +- .../middleware/{redirect.md => redirect.mdx} | 13 +- .../{request-id.md => request-id.mdx} | 20 +- .../es/middleware/{rewrite.md => rewrite.mdx} | 26 +- site/src/content/docs/es/middleware/secure.md | 119 --- .../src/content/docs/es/middleware/secure.mdx | 57 ++ .../{trailing-slash.md => trailing-slash.mdx} | 25 +- site/src/content/docs/index.mdx | 2 +- .../docs/ja/cookbook/http2-server-push.md | 2 +- site/src/content/docs/ja/cookbook/http2.md | 2 + site/src/content/docs/ja/guide/binding.md | 2 + .../content/docs/ja/guide/customization.md | 2 + site/src/content/docs/ja/guide/request.md | 2 + .../{basic-auth.md => basic-auth.mdx} | 24 +- .../{body-dump.md => body-dump.mdx} | 25 +- .../{body-limit.md => body-limit.mdx} | 12 +- ...context-timeout.md => context-timeout.mdx} | 15 +- site/src/content/docs/ja/middleware/cors.md | 139 ---- site/src/content/docs/ja/middleware/cors.mdx | 68 ++ .../docs/ja/middleware/{csrf.md => csrf.mdx} | 72 +- .../{decompress.md => decompress.mdx} | 19 +- .../docs/ja/middleware/{gzip.md => gzip.mdx} | 22 +- site/src/content/docs/ja/middleware/index.md | 30 + site/src/content/docs/ja/middleware/jwt.md | 126 ---- site/src/content/docs/ja/middleware/jwt.mdx | 45 ++ .../content/docs/ja/middleware/key-auth.md | 97 --- .../content/docs/ja/middleware/key-auth.mdx | 59 ++ ...method-override.md => method-override.mdx} | 13 +- .../{open-telemetry.md => open-telemetry.mdx} | 6 + .../{prometheus.md => prometheus.mdx} | 10 + site/src/content/docs/ja/middleware/proxy.md | 140 ---- site/src/content/docs/ja/middleware/proxy.mdx | 79 ++ .../{rate-limiter.md => rate-limiter.mdx} | 19 +- .../ja/middleware/{recover.md => recover.mdx} | 22 +- .../middleware/{redirect.md => redirect.mdx} | 15 +- .../{request-id.md => request-id.mdx} | 20 +- .../ja/middleware/{rewrite.md => rewrite.mdx} | 26 +- site/src/content/docs/ja/middleware/secure.md | 119 --- .../src/content/docs/ja/middleware/secure.mdx | 57 ++ .../{trailing-slash.md => trailing-slash.mdx} | 25 +- .../{basic-auth.md => basic-auth.mdx} | 24 +- .../{body-dump.md => body-dump.mdx} | 25 +- .../{body-limit.md => body-limit.mdx} | 12 +- ...context-timeout.md => context-timeout.mdx} | 15 +- site/src/content/docs/middleware/cors.md | 139 ---- site/src/content/docs/middleware/cors.mdx | 68 ++ .../docs/middleware/{csrf.md => csrf.mdx} | 72 +- .../{decompress.md => decompress.mdx} | 19 +- .../docs/middleware/{gzip.md => gzip.mdx} | 22 +- site/src/content/docs/middleware/index.md | 30 + site/src/content/docs/middleware/jwt.md | 126 ---- site/src/content/docs/middleware/jwt.mdx | 45 ++ site/src/content/docs/middleware/key-auth.md | 97 --- site/src/content/docs/middleware/key-auth.mdx | 59 ++ ...method-override.md => method-override.mdx} | 13 +- .../{open-telemetry.md => open-telemetry.mdx} | 6 + .../{prometheus.md => prometheus.mdx} | 10 + site/src/content/docs/middleware/proxy.md | 140 ---- site/src/content/docs/middleware/proxy.mdx | 79 ++ .../{rate-limiter.md => rate-limiter.mdx} | 19 +- .../middleware/{recover.md => recover.mdx} | 22 +- .../middleware/{redirect.md => redirect.mdx} | 13 +- .../{request-id.md => request-id.mdx} | 20 +- .../middleware/{rewrite.md => rewrite.mdx} | 26 +- site/src/content/docs/middleware/secure.md | 119 --- site/src/content/docs/middleware/secure.mdx | 57 ++ .../{trailing-slash.md => trailing-slash.mdx} | 25 +- .../docs/pt-br/cookbook/http2-server-push.md | 2 +- site/src/content/docs/pt-br/cookbook/http2.md | 2 + site/src/content/docs/pt-br/guide/binding.md | 2 + .../content/docs/pt-br/guide/customization.md | 2 + site/src/content/docs/pt-br/guide/request.md | 2 + .../{basic-auth.md => basic-auth.mdx} | 24 +- .../{body-dump.md => body-dump.mdx} | 25 +- .../{body-limit.md => body-limit.mdx} | 12 +- ...context-timeout.md => context-timeout.mdx} | 15 +- .../src/content/docs/pt-br/middleware/cors.md | 139 ---- .../content/docs/pt-br/middleware/cors.mdx | 68 ++ .../pt-br/middleware/{csrf.md => csrf.mdx} | 72 +- .../{decompress.md => decompress.mdx} | 19 +- .../pt-br/middleware/{gzip.md => gzip.mdx} | 22 +- .../content/docs/pt-br/middleware/index.md | 30 + site/src/content/docs/pt-br/middleware/jwt.md | 126 ---- .../src/content/docs/pt-br/middleware/jwt.mdx | 45 ++ .../content/docs/pt-br/middleware/key-auth.md | 97 --- .../docs/pt-br/middleware/key-auth.mdx | 59 ++ ...method-override.md => method-override.mdx} | 13 +- .../{open-telemetry.md => open-telemetry.mdx} | 6 + .../{prometheus.md => prometheus.mdx} | 10 + .../content/docs/pt-br/middleware/proxy.md | 140 ---- .../content/docs/pt-br/middleware/proxy.mdx | 79 ++ .../{rate-limiter.md => rate-limiter.mdx} | 19 +- .../middleware/{recover.md => recover.mdx} | 22 +- .../middleware/{redirect.md => redirect.mdx} | 13 +- .../{request-id.md => request-id.mdx} | 20 +- .../middleware/{rewrite.md => rewrite.mdx} | 26 +- .../content/docs/pt-br/middleware/secure.md | 119 --- .../content/docs/pt-br/middleware/secure.mdx | 57 ++ .../{trailing-slash.md => trailing-slash.mdx} | 25 +- .../docs/zh-cn/cookbook/http2-server-push.md | 2 +- site/src/content/docs/zh-cn/cookbook/http2.md | 2 + site/src/content/docs/zh-cn/guide/binding.md | 2 + .../content/docs/zh-cn/guide/customization.md | 2 + site/src/content/docs/zh-cn/guide/request.md | 2 + .../{basic-auth.md => basic-auth.mdx} | 24 +- .../{body-dump.md => body-dump.mdx} | 25 +- .../{body-limit.md => body-limit.mdx} | 12 +- ...context-timeout.md => context-timeout.mdx} | 15 +- .../src/content/docs/zh-cn/middleware/cors.md | 137 ---- .../content/docs/zh-cn/middleware/cors.mdx | 66 ++ .../zh-cn/middleware/{csrf.md => csrf.mdx} | 72 +- .../{decompress.md => decompress.mdx} | 19 +- .../zh-cn/middleware/{gzip.md => gzip.mdx} | 22 +- .../content/docs/zh-cn/middleware/index.md | 30 + site/src/content/docs/zh-cn/middleware/jwt.md | 126 ---- .../src/content/docs/zh-cn/middleware/jwt.mdx | 45 ++ .../content/docs/zh-cn/middleware/key-auth.md | 97 --- .../docs/zh-cn/middleware/key-auth.mdx | 59 ++ ...method-override.md => method-override.mdx} | 13 +- .../{open-telemetry.md => open-telemetry.mdx} | 6 + .../{prometheus.md => prometheus.mdx} | 10 + .../content/docs/zh-cn/middleware/proxy.md | 138 ---- .../content/docs/zh-cn/middleware/proxy.mdx | 77 ++ .../{rate-limiter.md => rate-limiter.mdx} | 19 +- .../middleware/{recover.md => recover.mdx} | 22 +- .../middleware/{redirect.md => redirect.mdx} | 15 +- .../{request-id.md => request-id.mdx} | 20 +- .../middleware/{rewrite.md => rewrite.mdx} | 26 +- .../content/docs/zh-cn/middleware/secure.md | 118 --- .../content/docs/zh-cn/middleware/secure.mdx | 56 ++ .../{trailing-slash.md => trailing-slash.mdx} | 25 +- site/src/data/github.ts | 27 - site/src/redirects.mjs | 5 +- site/translation-baseline.json | 412 ++++++++++- 187 files changed, 4043 insertions(+), 4630 deletions(-) create mode 100644 site/external-baseline.json create mode 100644 site/external-sources.json create mode 100644 site/next-source.json create mode 100644 site/performance-baseline.json create mode 100644 site/reference-pages.json create mode 100644 site/route-baseline.json create mode 100644 site/scripts/build-site.mjs create mode 100644 site/scripts/check-performance.mjs create mode 100644 site/scripts/check-site.mjs create mode 100644 site/scripts/translation-sections.mjs create mode 100644 site/src/components/VersionBanner.astro rename site/src/content/docs/es/middleware/{basic-auth.md => basic-auth.mdx} (67%) rename site/src/content/docs/es/middleware/{body-dump.md => body-dump.mdx} (59%) rename site/src/content/docs/es/middleware/{body-limit.md => body-limit.mdx} (83%) rename site/src/content/docs/es/middleware/{context-timeout.md => context-timeout.mdx} (73%) delete mode 100644 site/src/content/docs/es/middleware/cors.md create mode 100644 site/src/content/docs/es/middleware/cors.mdx rename site/src/content/docs/es/middleware/{csrf.md => csrf.mdx} (61%) rename site/src/content/docs/es/middleware/{decompress.md => decompress.mdx} (61%) rename site/src/content/docs/es/middleware/{gzip.md => gzip.mdx} (62%) create mode 100644 site/src/content/docs/es/middleware/index.md delete mode 100644 site/src/content/docs/es/middleware/jwt.md create mode 100644 site/src/content/docs/es/middleware/jwt.mdx delete mode 100644 site/src/content/docs/es/middleware/key-auth.md create mode 100644 site/src/content/docs/es/middleware/key-auth.mdx rename site/src/content/docs/es/middleware/{method-override.md => method-override.mdx} (77%) rename site/src/content/docs/es/middleware/{open-telemetry.md => open-telemetry.mdx} (92%) rename site/src/content/docs/es/middleware/{prometheus.md => prometheus.mdx} (96%) delete mode 100644 site/src/content/docs/es/middleware/proxy.md create mode 100644 site/src/content/docs/es/middleware/proxy.mdx rename site/src/content/docs/es/middleware/{rate-limiter.md => rate-limiter.mdx} (83%) rename site/src/content/docs/es/middleware/{recover.md => recover.mdx} (67%) rename site/src/content/docs/es/middleware/{redirect.md => redirect.mdx} (88%) rename site/src/content/docs/es/middleware/{request-id.md => request-id.mdx} (75%) rename site/src/content/docs/es/middleware/{rewrite.md => rewrite.mdx} (71%) delete mode 100644 site/src/content/docs/es/middleware/secure.md create mode 100644 site/src/content/docs/es/middleware/secure.mdx rename site/src/content/docs/es/middleware/{trailing-slash.md => trailing-slash.mdx} (62%) rename site/src/content/docs/ja/middleware/{basic-auth.md => basic-auth.mdx} (69%) rename site/src/content/docs/ja/middleware/{body-dump.md => body-dump.mdx} (64%) rename site/src/content/docs/ja/middleware/{body-limit.md => body-limit.mdx} (85%) rename site/src/content/docs/ja/middleware/{context-timeout.md => context-timeout.mdx} (75%) delete mode 100644 site/src/content/docs/ja/middleware/cors.md create mode 100644 site/src/content/docs/ja/middleware/cors.mdx rename site/src/content/docs/ja/middleware/{csrf.md => csrf.mdx} (62%) rename site/src/content/docs/ja/middleware/{decompress.md => decompress.mdx} (63%) rename site/src/content/docs/ja/middleware/{gzip.md => gzip.mdx} (63%) create mode 100644 site/src/content/docs/ja/middleware/index.md delete mode 100644 site/src/content/docs/ja/middleware/jwt.md create mode 100644 site/src/content/docs/ja/middleware/jwt.mdx delete mode 100644 site/src/content/docs/ja/middleware/key-auth.md create mode 100644 site/src/content/docs/ja/middleware/key-auth.mdx rename site/src/content/docs/ja/middleware/{method-override.md => method-override.mdx} (79%) rename site/src/content/docs/ja/middleware/{open-telemetry.md => open-telemetry.mdx} (93%) rename site/src/content/docs/ja/middleware/{prometheus.md => prometheus.mdx} (96%) delete mode 100644 site/src/content/docs/ja/middleware/proxy.md create mode 100644 site/src/content/docs/ja/middleware/proxy.mdx rename site/src/content/docs/ja/middleware/{rate-limiter.md => rate-limiter.mdx} (84%) rename site/src/content/docs/ja/middleware/{recover.md => recover.mdx} (69%) rename site/src/content/docs/ja/middleware/{redirect.md => redirect.mdx} (90%) rename site/src/content/docs/ja/middleware/{request-id.md => request-id.mdx} (75%) rename site/src/content/docs/ja/middleware/{rewrite.md => rewrite.mdx} (73%) delete mode 100644 site/src/content/docs/ja/middleware/secure.md create mode 100644 site/src/content/docs/ja/middleware/secure.mdx rename site/src/content/docs/ja/middleware/{trailing-slash.md => trailing-slash.mdx} (65%) rename site/src/content/docs/middleware/{basic-auth.md => basic-auth.mdx} (66%) rename site/src/content/docs/middleware/{body-dump.md => body-dump.mdx} (59%) rename site/src/content/docs/middleware/{body-limit.md => body-limit.mdx} (82%) rename site/src/content/docs/middleware/{context-timeout.md => context-timeout.mdx} (71%) delete mode 100644 site/src/content/docs/middleware/cors.md create mode 100644 site/src/content/docs/middleware/cors.mdx rename site/src/content/docs/middleware/{csrf.md => csrf.mdx} (60%) rename site/src/content/docs/middleware/{decompress.md => decompress.mdx} (59%) rename site/src/content/docs/middleware/{gzip.md => gzip.mdx} (61%) create mode 100644 site/src/content/docs/middleware/index.md delete mode 100644 site/src/content/docs/middleware/jwt.md create mode 100644 site/src/content/docs/middleware/jwt.mdx delete mode 100644 site/src/content/docs/middleware/key-auth.md create mode 100644 site/src/content/docs/middleware/key-auth.mdx rename site/src/content/docs/middleware/{method-override.md => method-override.mdx} (76%) rename site/src/content/docs/middleware/{open-telemetry.md => open-telemetry.mdx} (93%) rename site/src/content/docs/middleware/{prometheus.md => prometheus.mdx} (96%) delete mode 100644 site/src/content/docs/middleware/proxy.md create mode 100644 site/src/content/docs/middleware/proxy.mdx rename site/src/content/docs/middleware/{rate-limiter.md => rate-limiter.mdx} (83%) rename site/src/content/docs/middleware/{recover.md => recover.mdx} (65%) rename site/src/content/docs/middleware/{redirect.md => redirect.mdx} (88%) rename site/src/content/docs/middleware/{request-id.md => request-id.mdx} (74%) rename site/src/content/docs/middleware/{rewrite.md => rewrite.mdx} (70%) delete mode 100644 site/src/content/docs/middleware/secure.md create mode 100644 site/src/content/docs/middleware/secure.mdx rename site/src/content/docs/middleware/{trailing-slash.md => trailing-slash.mdx} (61%) rename site/src/content/docs/pt-br/middleware/{basic-auth.md => basic-auth.mdx} (66%) rename site/src/content/docs/pt-br/middleware/{body-dump.md => body-dump.mdx} (59%) rename site/src/content/docs/pt-br/middleware/{body-limit.md => body-limit.mdx} (83%) rename site/src/content/docs/pt-br/middleware/{context-timeout.md => context-timeout.mdx} (72%) delete mode 100644 site/src/content/docs/pt-br/middleware/cors.md create mode 100644 site/src/content/docs/pt-br/middleware/cors.mdx rename site/src/content/docs/pt-br/middleware/{csrf.md => csrf.mdx} (61%) rename site/src/content/docs/pt-br/middleware/{decompress.md => decompress.mdx} (60%) rename site/src/content/docs/pt-br/middleware/{gzip.md => gzip.mdx} (61%) create mode 100644 site/src/content/docs/pt-br/middleware/index.md delete mode 100644 site/src/content/docs/pt-br/middleware/jwt.md create mode 100644 site/src/content/docs/pt-br/middleware/jwt.mdx delete mode 100644 site/src/content/docs/pt-br/middleware/key-auth.md create mode 100644 site/src/content/docs/pt-br/middleware/key-auth.mdx rename site/src/content/docs/pt-br/middleware/{method-override.md => method-override.mdx} (76%) rename site/src/content/docs/pt-br/middleware/{open-telemetry.md => open-telemetry.mdx} (92%) rename site/src/content/docs/pt-br/middleware/{prometheus.md => prometheus.mdx} (96%) delete mode 100644 site/src/content/docs/pt-br/middleware/proxy.md create mode 100644 site/src/content/docs/pt-br/middleware/proxy.mdx rename site/src/content/docs/pt-br/middleware/{rate-limiter.md => rate-limiter.mdx} (83%) rename site/src/content/docs/pt-br/middleware/{recover.md => recover.mdx} (66%) rename site/src/content/docs/pt-br/middleware/{redirect.md => redirect.mdx} (89%) rename site/src/content/docs/pt-br/middleware/{request-id.md => request-id.mdx} (74%) rename site/src/content/docs/pt-br/middleware/{rewrite.md => rewrite.mdx} (71%) delete mode 100644 site/src/content/docs/pt-br/middleware/secure.md create mode 100644 site/src/content/docs/pt-br/middleware/secure.mdx rename site/src/content/docs/pt-br/middleware/{trailing-slash.md => trailing-slash.mdx} (61%) rename site/src/content/docs/zh-cn/middleware/{basic-auth.md => basic-auth.mdx} (65%) rename site/src/content/docs/zh-cn/middleware/{body-dump.md => body-dump.mdx} (58%) rename site/src/content/docs/zh-cn/middleware/{body-limit.md => body-limit.mdx} (81%) rename site/src/content/docs/zh-cn/middleware/{context-timeout.md => context-timeout.mdx} (70%) delete mode 100644 site/src/content/docs/zh-cn/middleware/cors.md create mode 100644 site/src/content/docs/zh-cn/middleware/cors.mdx rename site/src/content/docs/zh-cn/middleware/{csrf.md => csrf.mdx} (58%) rename site/src/content/docs/zh-cn/middleware/{decompress.md => decompress.mdx} (58%) rename site/src/content/docs/zh-cn/middleware/{gzip.md => gzip.mdx} (59%) create mode 100644 site/src/content/docs/zh-cn/middleware/index.md delete mode 100644 site/src/content/docs/zh-cn/middleware/jwt.md create mode 100644 site/src/content/docs/zh-cn/middleware/jwt.mdx delete mode 100644 site/src/content/docs/zh-cn/middleware/key-auth.md create mode 100644 site/src/content/docs/zh-cn/middleware/key-auth.mdx rename site/src/content/docs/zh-cn/middleware/{method-override.md => method-override.mdx} (75%) rename site/src/content/docs/zh-cn/middleware/{open-telemetry.md => open-telemetry.mdx} (92%) rename site/src/content/docs/zh-cn/middleware/{prometheus.md => prometheus.mdx} (96%) delete mode 100644 site/src/content/docs/zh-cn/middleware/proxy.md create mode 100644 site/src/content/docs/zh-cn/middleware/proxy.mdx rename site/src/content/docs/zh-cn/middleware/{rate-limiter.md => rate-limiter.mdx} (82%) rename site/src/content/docs/zh-cn/middleware/{recover.md => recover.mdx} (65%) rename site/src/content/docs/zh-cn/middleware/{redirect.md => redirect.mdx} (88%) rename site/src/content/docs/zh-cn/middleware/{request-id.md => request-id.mdx} (74%) rename site/src/content/docs/zh-cn/middleware/{rewrite.md => rewrite.mdx} (68%) delete mode 100644 site/src/content/docs/zh-cn/middleware/secure.md create mode 100644 site/src/content/docs/zh-cn/middleware/secure.mdx rename site/src/content/docs/zh-cn/middleware/{trailing-slash.md => trailing-slash.mdx} (59%) diff --git a/.gitignore b/.gitignore index b73e8988..5f595aa0 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ vendor .cache/ site/src/generated/ site/dist/ +site/dist-next/ site/node_modules/ diff --git a/README.md b/README.md index 05c94025..198a930f 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,14 @@ the runnable cookbook recipes the docs reference. ## Documentation site -Requires [Node.js](https://nodejs.org) (LTS), Go 1.27, and Git. The site build -fetches the Echo commit recorded in `site/echo-source.json`, compiles the -`reference/` examples against it, extracts exported middleware fields, and -checks them against `site/reference-baseline.json` before building pages. +Requires [Node.js](https://nodejs.org) (LTS), Go 1.27, and Git. The build +publishes the stable docs at `/` and a next preview at `/next/`, each with its +own search index and source revision. Those revisions are pinned in +`site/echo-source.json` and `site/next-source.json`. The build compiles the +`reference/` examples against both, extracts middleware fields and function +signatures, and checks them against `site/reference-baseline.json`. JWT, +Prometheus, and OpenTelemetry are extracted from their own pinned modules in +`site/external-sources.json` and checked against `site/external-baseline.json`. ```bash cd site @@ -28,18 +32,23 @@ npm run build # production build to site/dist npm run preview # preview the production build ``` -The first build fetches the pinned Echo source into `.cache/`. To test a proposed -Echo checkout instead, set `ECHO_SOURCE_DIR` to its absolute path when running -`npm run build`. The build reports changed fields and stops; review the affected -pages, run `npm run source:prepare` and `npm run source:accept`, then review the -baseline diff before committing it. The generated files in `site/src/generated/` -are never edited or committed. - -`npm run translations:status` reports whether the reviewed Spanish, Japanese, -Portuguese, and Chinese Request Logger and Static pages still match the current -English source. After reviewing those translations, run -`npm run translations:accept` and review the hash changes. This report tracks -freshness; it does not validate translation quality. +The first build fetches pinned source into `.cache/`. To test a proposed next +Echo checkout, set `ECHO_SOURCE_DIR` to its absolute path when running +`npm run build`. The build reports changed API facts and stops. Review the +affected pages and behavior, run `npm run source:prepare` and +`npm run source:accept`, then review the baseline diff before committing it. +The generated files in `site/src/generated/` are never edited or committed. +`npm run site:check` checks routes, local links and fragments, image text, +search assets, and locale coverage. `npm run performance:check` catches large +HTML or first-load asset growth on representative stable and next pages. + +`npm run translations:status` identifies changed sections in the Spanish, +Japanese, Portuguese, and Chinese Request Logger, Static, and middleware task +pages. Translate and review the affected section, then record that page with +`npm run translations:accept -- es logger` (replace locale and page) and review +the baseline diff. This tracks edits to both English and localized text; it +does not judge translation quality. The API tables are generated from source, +so translators focus on explanations, task guidance, examples, and safety notes. Content is Markdown/MDX under `site/src/content/docs/` (`guide/`, `middleware/`, `cookbook/`). To add a page, drop a file in the right folder — the sidebar is diff --git a/reference/cmd/config-fields/main.go b/reference/cmd/config-fields/main.go index 1057f285..090ad5aa 100644 --- a/reference/cmd/config-fields/main.go +++ b/reference/cmd/config-fields/main.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -// config-fields emits source-backed middleware config fields for the website. +// config-fields emits source-backed middleware API facts for the website. // It does not claim to determine runtime defaults or behavior. package main @@ -33,22 +33,33 @@ type config struct { Fields []field `json:"fields"` } +type function struct { + Name string `json:"name"` + Signature string `json:"signature"` + File string `json:"file"` + Line int `json:"line"` +} + type manifest struct { - Module string `json:"module"` - Revision string `json:"revision,omitempty"` - Configs []config `json:"configs"` + Module string `json:"module"` + Revision string `json:"revision,omitempty"` + Configs []config `json:"configs"` + Functions []function `json:"functions"` } func main() { root := flag.String("root", "", "Echo repository root") revision := flag.String("revision", "", "exact Echo commit or tag used for this build") + module := flag.String("module", "github.com/labstack/echo/v5", "module path containing the source") + directory := flag.String("directory", "middleware", "package directory relative to the module root") + packageName := flag.String("package", "middleware", "Go package name") flag.Parse() if *root == "" { fmt.Fprintln(os.Stderr, "-root is required") os.Exit(2) } - result, err := extract(*root, *revision) + result, err := extractPackage(*root, *revision, *module, *directory, *packageName) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) @@ -62,26 +73,43 @@ func main() { } func extract(root, revision string) (manifest, error) { + return extractPackage(root, revision, "github.com/labstack/echo/v5", "middleware", "middleware") +} + +func extractPackage(root, revision, module, directory, packageName string) (manifest, error) { root, err := filepath.Abs(root) if err != nil { return manifest{}, err } fs := token.NewFileSet() - packageDir := filepath.Join(root, "middleware") + packageDir := filepath.Join(root, directory) packages, err := parser.ParseDir(fs, packageDir, func(info os.FileInfo) bool { return strings.HasSuffix(info.Name(), ".go") && !strings.HasSuffix(info.Name(), "_test.go") }, parser.ParseComments) if err != nil { return manifest{}, err } - pkg, ok := packages["middleware"] + pkg, ok := packages[packageName] if !ok { - return manifest{}, fmt.Errorf("middleware package not found in %s", packageDir) + return manifest{}, fmt.Errorf("package %s not found in %s", packageName, packageDir) } - result := manifest{Module: "github.com/labstack/echo/v5", Revision: revision, Configs: []config{}} + result := manifest{Module: module, Revision: revision, Configs: []config{}, Functions: []function{}} for filename, source := range pkg.Files { for _, declaration := range source.Decls { + if fn, ok := declaration.(*ast.FuncDecl); ok && fn.Recv == nil && ast.IsExported(fn.Name.Name) { + var rendered bytes.Buffer + if err := format.Node(&rendered, fs, fn.Type); err != nil { + return manifest{}, err + } + result.Functions = append(result.Functions, function{ + Name: fn.Name.Name, + Signature: strings.Replace(rendered.String(), "func(", "func "+fn.Name.Name+"(", 1), + File: filepath.ToSlash(strings.TrimPrefix(filename, root+string(filepath.Separator))), + Line: fs.Position(fn.Pos()).Line, + }) + continue + } group, ok := declaration.(*ast.GenDecl) if !ok || group.Tok != token.TYPE { continue @@ -121,6 +149,7 @@ func extract(root, revision string) (manifest, error) { } } sort.Slice(result.Configs, func(i, j int) bool { return result.Configs[i].Name < result.Configs[j].Name }) + sort.Slice(result.Functions, func(i, j int) bool { return result.Functions[i].Name < result.Functions[j].Name }) return result, nil } diff --git a/reference/cmd/config-fields/main_test.go b/reference/cmd/config-fields/main_test.go index 33a40296..b5496b36 100644 --- a/reference/cmd/config-fields/main_test.go +++ b/reference/cmd/config-fields/main_test.go @@ -23,6 +23,8 @@ type ExampleConfig struct { } type hiddenConfig struct { Visible bool } type OtherType struct { Visible bool } +func ExampleWithConfig(c ExampleConfig) bool { return c.New != "" } +func privateFunction() {} ` if err := os.WriteFile(filepath.Join(dir, "example.go"), []byte(source), 0644); err != nil { t.Fatal(err) @@ -42,4 +44,7 @@ type OtherType struct { Visible bool } if fields[0].Name != "Old" || fields[0].Type != "bool" || !fields[0].Deprecated || fields[1].Name != "New" { t.Fatalf("unexpected fields: %#v", fields) } + if len(got.Functions) != 1 || got.Functions[0].Name != "ExampleWithConfig" || got.Functions[0].Signature != "func ExampleWithConfig(c ExampleConfig) bool" || got.Functions[0].File != "middleware/example.go" { + t.Fatalf("unexpected functions: %#v", got.Functions) + } } diff --git a/site/astro.config.mjs b/site/astro.config.mjs index 5603037f..84fa145a 100644 --- a/site/astro.config.mjs +++ b/site/astro.config.mjs @@ -2,11 +2,15 @@ import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; import { redirects } from './src/redirects.mjs'; +const next = process.env.DOCS_CHANNEL === 'next'; + // https://astro.build/config export default defineConfig({ site: 'https://echo.labstack.com', + base: next ? '/next/' : '/', + outDir: next ? './dist-next' : './dist', // Preserve every live Docusaurus /docs/* URL at cutover (generated — see ./src/redirects.mjs). - redirects, + redirects: next ? {} : redirects, integrations: [ starlight({ title: 'Echo', @@ -36,6 +40,7 @@ export default defineConfig({ // Keep Starlight's built-in Pagefind ⌘K search; Search override adds the // empty-state launchpad. "Ask AI" is the kapa.ai widget (see head). components: { + Banner: './src/components/VersionBanner.astro', Footer: './src/components/Footer.astro', Search: './src/components/Search.astro', }, diff --git a/site/echo-source.json b/site/echo-source.json index 81bb76d9..bb5affe7 100644 --- a/site/echo-source.json +++ b/site/echo-source.json @@ -1,4 +1,5 @@ { "repository": "https://github.com/labstack/echo.git", - "revision": "68ac4cf7e0b862001a9de7cdbc50d9447287a07c" + "revision": "caa018246b840091e71a1d61ea9ee1176ab81333", + "release": "v5.3.0" } diff --git a/site/external-baseline.json b/site/external-baseline.json new file mode 100644 index 00000000..a24133d5 --- /dev/null +++ b/site/external-baseline.json @@ -0,0 +1,218 @@ +{ + "jwt": { + "module": "github.com/labstack/echo-jwt/v5", + "version": "v5.0.2", + "configs": { + "Config": { + "Skipper": { + "type": "middleware.Skipper", + "deprecated": false + }, + "BeforeFunc": { + "type": "middleware.BeforeFunc", + "deprecated": false + }, + "SuccessHandler": { + "type": "func(c *echo.Context) error", + "deprecated": false + }, + "ErrorHandler": { + "type": "func(c *echo.Context, err error) error", + "deprecated": false + }, + "ContinueOnIgnoredError": { + "type": "bool", + "deprecated": false + }, + "ContextKey": { + "type": "string", + "deprecated": false + }, + "SigningKey": { + "type": "interface{}", + "deprecated": false + }, + "SigningKeys": { + "type": "map[string]interface{}", + "deprecated": false + }, + "SigningMethod": { + "type": "string", + "deprecated": false + }, + "KeyFunc": { + "type": "jwt.Keyfunc", + "deprecated": false + }, + "TokenLookup": { + "type": "string", + "deprecated": false + }, + "TokenLookupFuncs": { + "type": "[]middleware.ValuesExtractor", + "deprecated": false + }, + "ParseTokenFunc": { + "type": "func(c *echo.Context, auth string) (interface{}, error)", + "deprecated": false + }, + "NewClaimsFunc": { + "type": "func(c *echo.Context) jwt.Claims", + "deprecated": false + } + } + }, + "functions": { + "JWT": "func JWT(signingKey interface{}) echo.MiddlewareFunc", + "WithConfig": "func WithConfig(config Config) echo.MiddlewareFunc" + } + }, + "prometheus": { + "module": "github.com/labstack/echo-prometheus", + "version": "v0.0.1", + "configs": { + "HandlerConfig": { + "Gatherer": { + "type": "prometheus.Gatherer", + "deprecated": false + } + }, + "MiddlewareConfig": { + "Skipper": { + "type": "middleware.Skipper", + "deprecated": false + }, + "Namespace": { + "type": "string", + "deprecated": false + }, + "Subsystem": { + "type": "string", + "deprecated": false + }, + "LabelFuncs": { + "type": "map[string]LabelValueFunc", + "deprecated": false + }, + "HistogramOptsFunc": { + "type": "func(opts prometheus.HistogramOpts) prometheus.HistogramOpts", + "deprecated": false + }, + "CounterOptsFunc": { + "type": "func(opts prometheus.CounterOpts) prometheus.CounterOpts", + "deprecated": false + }, + "Registerer": { + "type": "prometheus.Registerer", + "deprecated": false + }, + "BeforeNext": { + "type": "func(c *echo.Context)", + "deprecated": false + }, + "AfterNext": { + "type": "func(c *echo.Context, err error)", + "deprecated": false + }, + "DoNotUseRequestPathFor404": { + "type": "bool", + "deprecated": false + } + }, + "PushGatewayConfig": { + "PushGatewayURL": { + "type": "string", + "deprecated": false + }, + "PushInterval": { + "type": "time.Duration", + "deprecated": false + }, + "Gatherer": { + "type": "prometheus.Gatherer", + "deprecated": false + }, + "ErrorHandler": { + "type": "func(err error) error", + "deprecated": false + }, + "ClientTransport": { + "type": "http.RoundTripper", + "deprecated": false + } + } + }, + "functions": { + "NewHandler": "func NewHandler() echo.HandlerFunc", + "NewHandlerWithConfig": "func NewHandlerWithConfig(config HandlerConfig) echo.HandlerFunc", + "NewMiddleware": "func NewMiddleware(subsystem string) echo.MiddlewareFunc", + "NewMiddlewareWithConfig": "func NewMiddlewareWithConfig(config MiddlewareConfig) echo.MiddlewareFunc", + "RunPushGatewayGatherer": "func RunPushGatewayGatherer(ctx context.Context, config PushGatewayConfig) error", + "WriteGatheredMetrics": "func WriteGatheredMetrics(writer io.Writer, gatherer prometheus.Gatherer) error" + } + }, + "open-telemetry": { + "module": "github.com/labstack/echo-opentelemetry", + "version": "v0.0.3", + "configs": { + "Config": { + "ServerName": { + "type": "string", + "deprecated": false + }, + "Skipper": { + "type": "middleware.Skipper", + "deprecated": false + }, + "OnNextError": { + "type": "OnErrorFunc", + "deprecated": false + }, + "OnExtractionError": { + "type": "OnErrorFunc", + "deprecated": false + }, + "TracerProvider": { + "type": "oteltrace.TracerProvider", + "deprecated": false + }, + "MeterProvider": { + "type": "metric.MeterProvider", + "deprecated": false + }, + "Propagators": { + "type": "propagation.TextMapPropagator", + "deprecated": false + }, + "SpanStartOptions": { + "type": "[]oteltrace.SpanStartOption", + "deprecated": false + }, + "SpanStartAttributes": { + "type": "AttributesFunc", + "deprecated": false + }, + "SpanEndAttributes": { + "type": "AttributesFunc", + "deprecated": false + }, + "MetricAttributes": { + "type": "MetricAttributesFunc", + "deprecated": false + }, + "Metrics": { + "type": "MetricsRecorder", + "deprecated": false + } + } + }, + "functions": { + "NewMetrics": "func NewMetrics(meter metric.Meter) (*Metrics, error)", + "NewMiddleware": "func NewMiddleware(serverName string) echo.MiddlewareFunc", + "NewMiddlewareWithConfig": "func NewMiddlewareWithConfig(config Config) echo.MiddlewareFunc", + "SpanNameFormatter": "func SpanNameFormatter(v Values) string", + "SpanStatus": "func SpanStatus(code int, err error) (codes.Code, string)", + "SplitAddress": "func SplitAddress(address string) (host string, port int, err error)" + } + } +} diff --git a/site/external-sources.json b/site/external-sources.json new file mode 100644 index 00000000..735a60ce --- /dev/null +++ b/site/external-sources.json @@ -0,0 +1,20 @@ +{ + "jwt": { + "module": "github.com/labstack/echo-jwt/v5", + "version": "v5.0.2", + "repository": "https://github.com/labstack/echo-jwt", + "package": "echojwt" + }, + "prometheus": { + "module": "github.com/labstack/echo-prometheus", + "version": "v0.0.1", + "repository": "https://github.com/labstack/echo-prometheus", + "package": "echoprometheus" + }, + "open-telemetry": { + "module": "github.com/labstack/echo-opentelemetry", + "version": "v0.0.3", + "repository": "https://github.com/labstack/echo-opentelemetry", + "package": "echootel" + } +} diff --git a/site/next-source.json b/site/next-source.json new file mode 100644 index 00000000..d90df8ac --- /dev/null +++ b/site/next-source.json @@ -0,0 +1,5 @@ +{ + "repository": "https://github.com/labstack/echo.git", + "revision": "e804f422d2b0546df620b7f73b7563f509368972", + "release": "next" +} diff --git a/site/package.json b/site/package.json index b80c9e9e..085a1414 100644 --- a/site/package.json +++ b/site/package.json @@ -4,14 +4,18 @@ "version": "0.1.0", "scripts": { "dev": "npm run source:prepare && astro dev", - "build": "npm run source:check && npm run translations:status && astro build", + "build": "node scripts/build-site.mjs", "preview": "astro preview", "astro": "astro", "source:prepare": "node scripts/prepare-echo-source.mjs", "source:check": "npm run source:prepare && node scripts/check-source.mjs", "source:accept": "node scripts/accept-source.mjs", "translations:status": "node scripts/translation-status.mjs", - "translations:accept": "node scripts/accept-translations.mjs" + "translations:accept": "node scripts/accept-translations.mjs", + "site:check": "node scripts/check-site.mjs", + "site:accept": "node scripts/check-site.mjs --accept", + "performance:check": "node scripts/check-performance.mjs", + "performance:accept": "node scripts/check-performance.mjs --accept" }, "dependencies": { "@astrojs/starlight": "^0.40.0", diff --git a/site/performance-baseline.json b/site/performance-baseline.json new file mode 100644 index 00000000..06dc8866 --- /dev/null +++ b/site/performance-baseline.json @@ -0,0 +1,80 @@ +{ + "/": { + "html": { + "raw": 27523, + "gzip": 8601 + }, + "firstLoadAssets": { + "raw": 94854, + "gzip": 19546 + }, + "externalDomains": [ + "encore.dev", + "fonts.googleapis.com", + "fonts.gstatic.com", + "github.com", + "pkg.go.dev", + "unpkg.com", + "widget.kapa.ai", + "x.com" + ] + }, + "/middleware/cors/": { + "html": { + "raw": 60522, + "gzip": 11390 + }, + "firstLoadAssets": { + "raw": 118355, + "gzip": 26052 + }, + "externalDomains": [ + "blog.portswigger.net", + "fonts.googleapis.com", + "fonts.gstatic.com", + "github.com", + "unpkg.com", + "widget.kapa.ai", + "x.com" + ] + }, + "/next/": { + "html": { + "raw": 27645, + "gzip": 8615 + }, + "firstLoadAssets": { + "raw": 94869, + "gzip": 19549 + }, + "externalDomains": [ + "encore.dev", + "fonts.googleapis.com", + "fonts.gstatic.com", + "github.com", + "pkg.go.dev", + "unpkg.com", + "widget.kapa.ai", + "x.com" + ] + }, + "/next/middleware/cors/": { + "html": { + "raw": 60976, + "gzip": 11405 + }, + "firstLoadAssets": { + "raw": 118370, + "gzip": 26055 + }, + "externalDomains": [ + "blog.portswigger.net", + "fonts.googleapis.com", + "fonts.gstatic.com", + "github.com", + "unpkg.com", + "widget.kapa.ai", + "x.com" + ] + } +} diff --git a/site/reference-baseline.json b/site/reference-baseline.json index 9672f031..3230fc2d 100644 --- a/site/reference-baseline.json +++ b/site/reference-baseline.json @@ -548,5 +548,66 @@ "deprecated": false } } + }, + "functions": { + "AddTrailingSlash": "func AddTrailingSlash() echo.MiddlewareFunc", + "AddTrailingSlashWithConfig": "func AddTrailingSlashWithConfig(config AddTrailingSlashConfig) echo.MiddlewareFunc", + "BasicAuth": "func BasicAuth(fn BasicAuthValidator) echo.MiddlewareFunc", + "BasicAuthWithConfig": "func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc", + "BodyDump": "func BodyDump(handler BodyDumpHandler) echo.MiddlewareFunc", + "BodyDumpWithConfig": "func BodyDumpWithConfig(config BodyDumpConfig) echo.MiddlewareFunc", + "BodyLimit": "func BodyLimit(limitBytes int64) echo.MiddlewareFunc", + "BodyLimitWithConfig": "func BodyLimitWithConfig(config BodyLimitConfig) echo.MiddlewareFunc", + "CORS": "func CORS(allowOrigins ...string) echo.MiddlewareFunc", + "CORSWithConfig": "func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc", + "CSRF": "func CSRF() echo.MiddlewareFunc", + "CSRFWithConfig": "func CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc", + "ContextTimeout": "func ContextTimeout(timeout time.Duration) echo.MiddlewareFunc", + "ContextTimeoutWithConfig": "func ContextTimeoutWithConfig(config ContextTimeoutConfig) echo.MiddlewareFunc", + "CreateExtractors": "func CreateExtractors(lookups string, limit uint) ([]ValuesExtractor, error)", + "Decompress": "func Decompress() echo.MiddlewareFunc", + "DecompressWithConfig": "func DecompressWithConfig(config DecompressConfig) echo.MiddlewareFunc", + "DefaultSkipper": "func DefaultSkipper(c *echo.Context) bool", + "Gzip": "func Gzip() echo.MiddlewareFunc", + "GzipWithConfig": "func GzipWithConfig(config GzipConfig) echo.MiddlewareFunc", + "HTTPSNonWWWRedirect": "func HTTPSNonWWWRedirect() echo.MiddlewareFunc", + "HTTPSNonWWWRedirectWithConfig": "func HTTPSNonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc", + "HTTPSRedirect": "func HTTPSRedirect() echo.MiddlewareFunc", + "HTTPSRedirectWithConfig": "func HTTPSRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc", + "HTTPSWWWRedirect": "func HTTPSWWWRedirect() echo.MiddlewareFunc", + "HTTPSWWWRedirectWithConfig": "func HTTPSWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc", + "KeyAuth": "func KeyAuth(fn KeyAuthValidator) echo.MiddlewareFunc", + "KeyAuthWithConfig": "func KeyAuthWithConfig(config KeyAuthConfig) echo.MiddlewareFunc", + "MethodFromForm": "func MethodFromForm(param string) MethodOverrideGetter", + "MethodFromHeader": "func MethodFromHeader(header string) MethodOverrideGetter", + "MethodFromQuery": "func MethodFromQuery(param string) MethodOverrideGetter", + "MethodOverride": "func MethodOverride() echo.MiddlewareFunc", + "MethodOverrideWithConfig": "func MethodOverrideWithConfig(config MethodOverrideConfig) echo.MiddlewareFunc", + "NewRandomBalancer": "func NewRandomBalancer(targets []*ProxyTarget) ProxyBalancer", + "NewRateLimiterMemoryStore": "func NewRateLimiterMemoryStore(rateLimit float64) (store *RateLimiterMemoryStore)", + "NewRateLimiterMemoryStoreWithConfig": "func NewRateLimiterMemoryStoreWithConfig(config RateLimiterMemoryStoreConfig) (store *RateLimiterMemoryStore)", + "NewRoundRobinBalancer": "func NewRoundRobinBalancer(targets []*ProxyTarget) ProxyBalancer", + "NonWWWRedirect": "func NonWWWRedirect() echo.MiddlewareFunc", + "NonWWWRedirectWithConfig": "func NonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc", + "Proxy": "func Proxy(balancer ProxyBalancer) echo.MiddlewareFunc", + "ProxyWithConfig": "func ProxyWithConfig(config ProxyConfig) echo.MiddlewareFunc", + "RateLimiter": "func RateLimiter(store RateLimiterStore) echo.MiddlewareFunc", + "RateLimiterWithConfig": "func RateLimiterWithConfig(config RateLimiterConfig) echo.MiddlewareFunc", + "Recover": "func Recover() echo.MiddlewareFunc", + "RecoverWithConfig": "func RecoverWithConfig(config RecoverConfig) echo.MiddlewareFunc", + "RemoveTrailingSlash": "func RemoveTrailingSlash() echo.MiddlewareFunc", + "RemoveTrailingSlashWithConfig": "func RemoveTrailingSlashWithConfig(config RemoveTrailingSlashConfig) echo.MiddlewareFunc", + "RequestID": "func RequestID() echo.MiddlewareFunc", + "RequestIDWithConfig": "func RequestIDWithConfig(config RequestIDConfig) echo.MiddlewareFunc", + "RequestLogger": "func RequestLogger() echo.MiddlewareFunc", + "RequestLoggerWithConfig": "func RequestLoggerWithConfig(config RequestLoggerConfig) echo.MiddlewareFunc", + "Rewrite": "func Rewrite(rules map[string]string) echo.MiddlewareFunc", + "RewriteWithConfig": "func RewriteWithConfig(config RewriteConfig) echo.MiddlewareFunc", + "Secure": "func Secure() echo.MiddlewareFunc", + "SecureWithConfig": "func SecureWithConfig(config SecureConfig) echo.MiddlewareFunc", + "Static": "func Static(root string) echo.MiddlewareFunc", + "StaticWithConfig": "func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc", + "WWWRedirect": "func WWWRedirect() echo.MiddlewareFunc", + "WWWRedirectWithConfig": "func WWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc" } } diff --git a/site/reference-pages.json b/site/reference-pages.json new file mode 100644 index 00000000..5e6380dc --- /dev/null +++ b/site/reference-pages.json @@ -0,0 +1,22 @@ +{ + "basic-auth": ["BasicAuthConfig"], + "body-dump": ["BodyDumpConfig"], + "body-limit": ["BodyLimitConfig"], + "context-timeout": ["ContextTimeoutConfig"], + "cors": ["CORSConfig"], + "csrf": ["CSRFConfig"], + "decompress": ["DecompressConfig"], + "gzip": ["GzipConfig"], + "key-auth": ["KeyAuthConfig"], + "logger": ["RequestLoggerConfig"], + "method-override": ["MethodOverrideConfig"], + "proxy": ["ProxyConfig"], + "rate-limiter": ["RateLimiterConfig", "RateLimiterMemoryStoreConfig"], + "recover": ["RecoverConfig"], + "redirect": ["RedirectConfig"], + "request-id": ["RequestIDConfig"], + "rewrite": ["RewriteConfig"], + "secure": ["SecureConfig"], + "static": ["StaticConfig"], + "trailing-slash": ["AddTrailingSlashConfig", "RemoveTrailingSlashConfig"] +} diff --git a/site/route-baseline.json b/site/route-baseline.json new file mode 100644 index 00000000..6e48a8e9 --- /dev/null +++ b/site/route-baseline.json @@ -0,0 +1,680 @@ +{ + "routes": [ + "/", + "/404.html", + "/cookbook/auto-tls/", + "/cookbook/cors/", + "/cookbook/crud/", + "/cookbook/embed-resources/", + "/cookbook/file-download/", + "/cookbook/file-upload/", + "/cookbook/graceful-shutdown/", + "/cookbook/hello-world/", + "/cookbook/http2-server-push/", + "/cookbook/http2/", + "/cookbook/jsonp/", + "/cookbook/jwt/", + "/cookbook/load-balancing/", + "/cookbook/middleware/", + "/cookbook/reverse-proxy/", + "/cookbook/sse/", + "/cookbook/streaming-response/", + "/cookbook/subdomain/", + "/cookbook/timeout/", + "/cookbook/websocket/", + "/docs/", + "/docs/binding/", + "/docs/category/cookbook/", + "/docs/category/guide/", + "/docs/category/middleware/", + "/docs/cookbook/auto-tls/", + "/docs/cookbook/cors/", + "/docs/cookbook/crud/", + "/docs/cookbook/embed-resources/", + "/docs/cookbook/file-download/", + "/docs/cookbook/file-upload/", + "/docs/cookbook/graceful-shutdown/", + "/docs/cookbook/hello-world/", + "/docs/cookbook/http2-server-push/", + "/docs/cookbook/http2/", + "/docs/cookbook/jsonp/", + "/docs/cookbook/jwt/", + "/docs/cookbook/load-balancing/", + "/docs/cookbook/middleware/", + "/docs/cookbook/reverse-proxy/", + "/docs/cookbook/sse/", + "/docs/cookbook/streaming-response/", + "/docs/cookbook/subdomain/", + "/docs/cookbook/timeout/", + "/docs/cookbook/websocket/", + "/docs/cookies/", + "/docs/customization/", + "/docs/error-handling/", + "/docs/ip-address/", + "/docs/middleware/basic-auth/", + "/docs/middleware/body-dump/", + "/docs/middleware/body-limit/", + "/docs/middleware/casbin-auth/", + "/docs/middleware/context-timeout/", + "/docs/middleware/cors/", + "/docs/middleware/csrf/", + "/docs/middleware/decompress/", + "/docs/middleware/gzip/", + "/docs/middleware/index/", + "/docs/middleware/jwt/", + "/docs/middleware/key-auth/", + "/docs/middleware/logger/", + "/docs/middleware/method-override/", + "/docs/middleware/open-telemetry/", + "/docs/middleware/prometheus/", + "/docs/middleware/proxy/", + "/docs/middleware/rate-limiter/", + "/docs/middleware/recover/", + "/docs/middleware/redirect/", + "/docs/middleware/request-id/", + "/docs/middleware/rewrite/", + "/docs/middleware/secure/", + "/docs/middleware/session/", + "/docs/middleware/static/", + "/docs/middleware/trailing-slash/", + "/docs/quick-start/", + "/docs/request/", + "/docs/response/", + "/docs/routing/", + "/docs/start-server/", + "/docs/static-files/", + "/docs/templates/", + "/docs/testing/", + "/es/", + "/es/cookbook/auto-tls/", + "/es/cookbook/cors/", + "/es/cookbook/crud/", + "/es/cookbook/embed-resources/", + "/es/cookbook/file-download/", + "/es/cookbook/file-upload/", + "/es/cookbook/graceful-shutdown/", + "/es/cookbook/hello-world/", + "/es/cookbook/http2-server-push/", + "/es/cookbook/http2/", + "/es/cookbook/jsonp/", + "/es/cookbook/jwt/", + "/es/cookbook/load-balancing/", + "/es/cookbook/middleware/", + "/es/cookbook/reverse-proxy/", + "/es/cookbook/sse/", + "/es/cookbook/streaming-response/", + "/es/cookbook/subdomain/", + "/es/cookbook/timeout/", + "/es/cookbook/websocket/", + "/es/guide/binding/", + "/es/guide/context/", + "/es/guide/cookies/", + "/es/guide/customization/", + "/es/guide/error-handling/", + "/es/guide/installation/", + "/es/guide/ip-address/", + "/es/guide/quickstart/", + "/es/guide/request/", + "/es/guide/response/", + "/es/guide/routing/", + "/es/guide/static-files/", + "/es/guide/templates/", + "/es/guide/testing/", + "/es/middleware/", + "/es/middleware/basic-auth/", + "/es/middleware/body-dump/", + "/es/middleware/body-limit/", + "/es/middleware/casbin-auth/", + "/es/middleware/context-timeout/", + "/es/middleware/cors/", + "/es/middleware/csrf/", + "/es/middleware/decompress/", + "/es/middleware/gzip/", + "/es/middleware/jwt/", + "/es/middleware/key-auth/", + "/es/middleware/logger/", + "/es/middleware/method-override/", + "/es/middleware/open-telemetry/", + "/es/middleware/prometheus/", + "/es/middleware/proxy/", + "/es/middleware/rate-limiter/", + "/es/middleware/recover/", + "/es/middleware/redirect/", + "/es/middleware/request-id/", + "/es/middleware/rewrite/", + "/es/middleware/secure/", + "/es/middleware/session/", + "/es/middleware/static/", + "/es/middleware/trailing-slash/", + "/guide/binding/", + "/guide/context/", + "/guide/cookies/", + "/guide/customization/", + "/guide/error-handling/", + "/guide/installation/", + "/guide/ip-address/", + "/guide/quickstart/", + "/guide/request/", + "/guide/response/", + "/guide/routing/", + "/guide/static-files/", + "/guide/templates/", + "/guide/testing/", + "/ja/", + "/ja/cookbook/auto-tls/", + "/ja/cookbook/cors/", + "/ja/cookbook/crud/", + "/ja/cookbook/embed-resources/", + "/ja/cookbook/file-download/", + "/ja/cookbook/file-upload/", + "/ja/cookbook/graceful-shutdown/", + "/ja/cookbook/hello-world/", + "/ja/cookbook/http2-server-push/", + "/ja/cookbook/http2/", + "/ja/cookbook/jsonp/", + "/ja/cookbook/jwt/", + "/ja/cookbook/load-balancing/", + "/ja/cookbook/middleware/", + "/ja/cookbook/reverse-proxy/", + "/ja/cookbook/sse/", + "/ja/cookbook/streaming-response/", + "/ja/cookbook/subdomain/", + "/ja/cookbook/timeout/", + "/ja/cookbook/websocket/", + "/ja/guide/binding/", + "/ja/guide/context/", + "/ja/guide/cookies/", + "/ja/guide/customization/", + "/ja/guide/error-handling/", + "/ja/guide/installation/", + "/ja/guide/ip-address/", + "/ja/guide/quickstart/", + "/ja/guide/request/", + "/ja/guide/response/", + "/ja/guide/routing/", + "/ja/guide/static-files/", + "/ja/guide/templates/", + "/ja/guide/testing/", + "/ja/middleware/", + "/ja/middleware/basic-auth/", + "/ja/middleware/body-dump/", + "/ja/middleware/body-limit/", + "/ja/middleware/casbin-auth/", + "/ja/middleware/context-timeout/", + "/ja/middleware/cors/", + "/ja/middleware/csrf/", + "/ja/middleware/decompress/", + "/ja/middleware/gzip/", + "/ja/middleware/jwt/", + "/ja/middleware/key-auth/", + "/ja/middleware/logger/", + "/ja/middleware/method-override/", + "/ja/middleware/open-telemetry/", + "/ja/middleware/prometheus/", + "/ja/middleware/proxy/", + "/ja/middleware/rate-limiter/", + "/ja/middleware/recover/", + "/ja/middleware/redirect/", + "/ja/middleware/request-id/", + "/ja/middleware/rewrite/", + "/ja/middleware/secure/", + "/ja/middleware/session/", + "/ja/middleware/static/", + "/ja/middleware/trailing-slash/", + "/middleware/", + "/middleware/basic-auth/", + "/middleware/body-dump/", + "/middleware/body-limit/", + "/middleware/casbin-auth/", + "/middleware/context-timeout/", + "/middleware/cors/", + "/middleware/csrf/", + "/middleware/decompress/", + "/middleware/gzip/", + "/middleware/jwt/", + "/middleware/key-auth/", + "/middleware/logger/", + "/middleware/method-override/", + "/middleware/open-telemetry/", + "/middleware/prometheus/", + "/middleware/proxy/", + "/middleware/rate-limiter/", + "/middleware/recover/", + "/middleware/redirect/", + "/middleware/request-id/", + "/middleware/rewrite/", + "/middleware/secure/", + "/middleware/session/", + "/middleware/static/", + "/middleware/trailing-slash/", + "/next/", + "/next/404.html", + "/next/cookbook/auto-tls/", + "/next/cookbook/cors/", + "/next/cookbook/crud/", + "/next/cookbook/embed-resources/", + "/next/cookbook/file-download/", + "/next/cookbook/file-upload/", + "/next/cookbook/graceful-shutdown/", + "/next/cookbook/hello-world/", + "/next/cookbook/http2-server-push/", + "/next/cookbook/http2/", + "/next/cookbook/jsonp/", + "/next/cookbook/jwt/", + "/next/cookbook/load-balancing/", + "/next/cookbook/middleware/", + "/next/cookbook/reverse-proxy/", + "/next/cookbook/sse/", + "/next/cookbook/streaming-response/", + "/next/cookbook/subdomain/", + "/next/cookbook/timeout/", + "/next/cookbook/websocket/", + "/next/es/", + "/next/es/cookbook/auto-tls/", + "/next/es/cookbook/cors/", + "/next/es/cookbook/crud/", + "/next/es/cookbook/embed-resources/", + "/next/es/cookbook/file-download/", + "/next/es/cookbook/file-upload/", + "/next/es/cookbook/graceful-shutdown/", + "/next/es/cookbook/hello-world/", + "/next/es/cookbook/http2-server-push/", + "/next/es/cookbook/http2/", + "/next/es/cookbook/jsonp/", + "/next/es/cookbook/jwt/", + "/next/es/cookbook/load-balancing/", + "/next/es/cookbook/middleware/", + "/next/es/cookbook/reverse-proxy/", + "/next/es/cookbook/sse/", + "/next/es/cookbook/streaming-response/", + "/next/es/cookbook/subdomain/", + "/next/es/cookbook/timeout/", + "/next/es/cookbook/websocket/", + "/next/es/guide/binding/", + "/next/es/guide/context/", + "/next/es/guide/cookies/", + "/next/es/guide/customization/", + "/next/es/guide/error-handling/", + "/next/es/guide/installation/", + "/next/es/guide/ip-address/", + "/next/es/guide/quickstart/", + "/next/es/guide/request/", + "/next/es/guide/response/", + "/next/es/guide/routing/", + "/next/es/guide/static-files/", + "/next/es/guide/templates/", + "/next/es/guide/testing/", + "/next/es/middleware/", + "/next/es/middleware/basic-auth/", + "/next/es/middleware/body-dump/", + "/next/es/middleware/body-limit/", + "/next/es/middleware/casbin-auth/", + "/next/es/middleware/context-timeout/", + "/next/es/middleware/cors/", + "/next/es/middleware/csrf/", + "/next/es/middleware/decompress/", + "/next/es/middleware/gzip/", + "/next/es/middleware/jwt/", + "/next/es/middleware/key-auth/", + "/next/es/middleware/logger/", + "/next/es/middleware/method-override/", + "/next/es/middleware/open-telemetry/", + "/next/es/middleware/prometheus/", + "/next/es/middleware/proxy/", + "/next/es/middleware/rate-limiter/", + "/next/es/middleware/recover/", + "/next/es/middleware/redirect/", + "/next/es/middleware/request-id/", + "/next/es/middleware/rewrite/", + "/next/es/middleware/secure/", + "/next/es/middleware/session/", + "/next/es/middleware/static/", + "/next/es/middleware/trailing-slash/", + "/next/guide/binding/", + "/next/guide/context/", + "/next/guide/cookies/", + "/next/guide/customization/", + "/next/guide/error-handling/", + "/next/guide/installation/", + "/next/guide/ip-address/", + "/next/guide/quickstart/", + "/next/guide/request/", + "/next/guide/response/", + "/next/guide/routing/", + "/next/guide/static-files/", + "/next/guide/templates/", + "/next/guide/testing/", + "/next/ja/", + "/next/ja/cookbook/auto-tls/", + "/next/ja/cookbook/cors/", + "/next/ja/cookbook/crud/", + "/next/ja/cookbook/embed-resources/", + "/next/ja/cookbook/file-download/", + "/next/ja/cookbook/file-upload/", + "/next/ja/cookbook/graceful-shutdown/", + "/next/ja/cookbook/hello-world/", + "/next/ja/cookbook/http2-server-push/", + "/next/ja/cookbook/http2/", + "/next/ja/cookbook/jsonp/", + "/next/ja/cookbook/jwt/", + "/next/ja/cookbook/load-balancing/", + "/next/ja/cookbook/middleware/", + "/next/ja/cookbook/reverse-proxy/", + "/next/ja/cookbook/sse/", + "/next/ja/cookbook/streaming-response/", + "/next/ja/cookbook/subdomain/", + "/next/ja/cookbook/timeout/", + "/next/ja/cookbook/websocket/", + "/next/ja/guide/binding/", + "/next/ja/guide/context/", + "/next/ja/guide/cookies/", + "/next/ja/guide/customization/", + "/next/ja/guide/error-handling/", + "/next/ja/guide/installation/", + "/next/ja/guide/ip-address/", + "/next/ja/guide/quickstart/", + "/next/ja/guide/request/", + "/next/ja/guide/response/", + "/next/ja/guide/routing/", + "/next/ja/guide/static-files/", + "/next/ja/guide/templates/", + "/next/ja/guide/testing/", + "/next/ja/middleware/", + "/next/ja/middleware/basic-auth/", + "/next/ja/middleware/body-dump/", + "/next/ja/middleware/body-limit/", + "/next/ja/middleware/casbin-auth/", + "/next/ja/middleware/context-timeout/", + "/next/ja/middleware/cors/", + "/next/ja/middleware/csrf/", + "/next/ja/middleware/decompress/", + "/next/ja/middleware/gzip/", + "/next/ja/middleware/jwt/", + "/next/ja/middleware/key-auth/", + "/next/ja/middleware/logger/", + "/next/ja/middleware/method-override/", + "/next/ja/middleware/open-telemetry/", + "/next/ja/middleware/prometheus/", + "/next/ja/middleware/proxy/", + "/next/ja/middleware/rate-limiter/", + "/next/ja/middleware/recover/", + "/next/ja/middleware/redirect/", + "/next/ja/middleware/request-id/", + "/next/ja/middleware/rewrite/", + "/next/ja/middleware/secure/", + "/next/ja/middleware/session/", + "/next/ja/middleware/static/", + "/next/ja/middleware/trailing-slash/", + "/next/middleware/", + "/next/middleware/basic-auth/", + "/next/middleware/body-dump/", + "/next/middleware/body-limit/", + "/next/middleware/casbin-auth/", + "/next/middleware/context-timeout/", + "/next/middleware/cors/", + "/next/middleware/csrf/", + "/next/middleware/decompress/", + "/next/middleware/gzip/", + "/next/middleware/jwt/", + "/next/middleware/key-auth/", + "/next/middleware/logger/", + "/next/middleware/method-override/", + "/next/middleware/open-telemetry/", + "/next/middleware/prometheus/", + "/next/middleware/proxy/", + "/next/middleware/rate-limiter/", + "/next/middleware/recover/", + "/next/middleware/redirect/", + "/next/middleware/request-id/", + "/next/middleware/rewrite/", + "/next/middleware/secure/", + "/next/middleware/session/", + "/next/middleware/static/", + "/next/middleware/trailing-slash/", + "/next/pt-br/", + "/next/pt-br/cookbook/auto-tls/", + "/next/pt-br/cookbook/cors/", + "/next/pt-br/cookbook/crud/", + "/next/pt-br/cookbook/embed-resources/", + "/next/pt-br/cookbook/file-download/", + "/next/pt-br/cookbook/file-upload/", + "/next/pt-br/cookbook/graceful-shutdown/", + "/next/pt-br/cookbook/hello-world/", + "/next/pt-br/cookbook/http2-server-push/", + "/next/pt-br/cookbook/http2/", + "/next/pt-br/cookbook/jsonp/", + "/next/pt-br/cookbook/jwt/", + "/next/pt-br/cookbook/load-balancing/", + "/next/pt-br/cookbook/middleware/", + "/next/pt-br/cookbook/reverse-proxy/", + "/next/pt-br/cookbook/sse/", + "/next/pt-br/cookbook/streaming-response/", + "/next/pt-br/cookbook/subdomain/", + "/next/pt-br/cookbook/timeout/", + "/next/pt-br/cookbook/websocket/", + "/next/pt-br/guide/binding/", + "/next/pt-br/guide/context/", + "/next/pt-br/guide/cookies/", + "/next/pt-br/guide/customization/", + "/next/pt-br/guide/error-handling/", + "/next/pt-br/guide/installation/", + "/next/pt-br/guide/ip-address/", + "/next/pt-br/guide/quickstart/", + "/next/pt-br/guide/request/", + "/next/pt-br/guide/response/", + "/next/pt-br/guide/routing/", + "/next/pt-br/guide/static-files/", + "/next/pt-br/guide/templates/", + "/next/pt-br/guide/testing/", + "/next/pt-br/middleware/", + "/next/pt-br/middleware/basic-auth/", + "/next/pt-br/middleware/body-dump/", + "/next/pt-br/middleware/body-limit/", + "/next/pt-br/middleware/casbin-auth/", + "/next/pt-br/middleware/context-timeout/", + "/next/pt-br/middleware/cors/", + "/next/pt-br/middleware/csrf/", + "/next/pt-br/middleware/decompress/", + "/next/pt-br/middleware/gzip/", + "/next/pt-br/middleware/jwt/", + "/next/pt-br/middleware/key-auth/", + "/next/pt-br/middleware/logger/", + "/next/pt-br/middleware/method-override/", + "/next/pt-br/middleware/open-telemetry/", + "/next/pt-br/middleware/prometheus/", + "/next/pt-br/middleware/proxy/", + "/next/pt-br/middleware/rate-limiter/", + "/next/pt-br/middleware/recover/", + "/next/pt-br/middleware/redirect/", + "/next/pt-br/middleware/request-id/", + "/next/pt-br/middleware/rewrite/", + "/next/pt-br/middleware/secure/", + "/next/pt-br/middleware/session/", + "/next/pt-br/middleware/static/", + "/next/pt-br/middleware/trailing-slash/", + "/next/zh-cn/", + "/next/zh-cn/cookbook/auto-tls/", + "/next/zh-cn/cookbook/cors/", + "/next/zh-cn/cookbook/crud/", + "/next/zh-cn/cookbook/embed-resources/", + "/next/zh-cn/cookbook/file-download/", + "/next/zh-cn/cookbook/file-upload/", + "/next/zh-cn/cookbook/graceful-shutdown/", + "/next/zh-cn/cookbook/hello-world/", + "/next/zh-cn/cookbook/http2-server-push/", + "/next/zh-cn/cookbook/http2/", + "/next/zh-cn/cookbook/jsonp/", + "/next/zh-cn/cookbook/jwt/", + "/next/zh-cn/cookbook/load-balancing/", + "/next/zh-cn/cookbook/middleware/", + "/next/zh-cn/cookbook/reverse-proxy/", + "/next/zh-cn/cookbook/sse/", + "/next/zh-cn/cookbook/streaming-response/", + "/next/zh-cn/cookbook/subdomain/", + "/next/zh-cn/cookbook/timeout/", + "/next/zh-cn/cookbook/websocket/", + "/next/zh-cn/guide/binding/", + "/next/zh-cn/guide/context/", + "/next/zh-cn/guide/cookies/", + "/next/zh-cn/guide/customization/", + "/next/zh-cn/guide/error-handling/", + "/next/zh-cn/guide/installation/", + "/next/zh-cn/guide/ip-address/", + "/next/zh-cn/guide/quickstart/", + "/next/zh-cn/guide/request/", + "/next/zh-cn/guide/response/", + "/next/zh-cn/guide/routing/", + "/next/zh-cn/guide/static-files/", + "/next/zh-cn/guide/templates/", + "/next/zh-cn/guide/testing/", + "/next/zh-cn/middleware/", + "/next/zh-cn/middleware/basic-auth/", + "/next/zh-cn/middleware/body-dump/", + "/next/zh-cn/middleware/body-limit/", + "/next/zh-cn/middleware/casbin-auth/", + "/next/zh-cn/middleware/context-timeout/", + "/next/zh-cn/middleware/cors/", + "/next/zh-cn/middleware/csrf/", + "/next/zh-cn/middleware/decompress/", + "/next/zh-cn/middleware/gzip/", + "/next/zh-cn/middleware/jwt/", + "/next/zh-cn/middleware/key-auth/", + "/next/zh-cn/middleware/logger/", + "/next/zh-cn/middleware/method-override/", + "/next/zh-cn/middleware/open-telemetry/", + "/next/zh-cn/middleware/prometheus/", + "/next/zh-cn/middleware/proxy/", + "/next/zh-cn/middleware/rate-limiter/", + "/next/zh-cn/middleware/recover/", + "/next/zh-cn/middleware/redirect/", + "/next/zh-cn/middleware/request-id/", + "/next/zh-cn/middleware/rewrite/", + "/next/zh-cn/middleware/secure/", + "/next/zh-cn/middleware/session/", + "/next/zh-cn/middleware/static/", + "/next/zh-cn/middleware/trailing-slash/", + "/pt-br/", + "/pt-br/cookbook/auto-tls/", + "/pt-br/cookbook/cors/", + "/pt-br/cookbook/crud/", + "/pt-br/cookbook/embed-resources/", + "/pt-br/cookbook/file-download/", + "/pt-br/cookbook/file-upload/", + "/pt-br/cookbook/graceful-shutdown/", + "/pt-br/cookbook/hello-world/", + "/pt-br/cookbook/http2-server-push/", + "/pt-br/cookbook/http2/", + "/pt-br/cookbook/jsonp/", + "/pt-br/cookbook/jwt/", + "/pt-br/cookbook/load-balancing/", + "/pt-br/cookbook/middleware/", + "/pt-br/cookbook/reverse-proxy/", + "/pt-br/cookbook/sse/", + "/pt-br/cookbook/streaming-response/", + "/pt-br/cookbook/subdomain/", + "/pt-br/cookbook/timeout/", + "/pt-br/cookbook/websocket/", + "/pt-br/guide/binding/", + "/pt-br/guide/context/", + "/pt-br/guide/cookies/", + "/pt-br/guide/customization/", + "/pt-br/guide/error-handling/", + "/pt-br/guide/installation/", + "/pt-br/guide/ip-address/", + "/pt-br/guide/quickstart/", + "/pt-br/guide/request/", + "/pt-br/guide/response/", + "/pt-br/guide/routing/", + "/pt-br/guide/static-files/", + "/pt-br/guide/templates/", + "/pt-br/guide/testing/", + "/pt-br/middleware/", + "/pt-br/middleware/basic-auth/", + "/pt-br/middleware/body-dump/", + "/pt-br/middleware/body-limit/", + "/pt-br/middleware/casbin-auth/", + "/pt-br/middleware/context-timeout/", + "/pt-br/middleware/cors/", + "/pt-br/middleware/csrf/", + "/pt-br/middleware/decompress/", + "/pt-br/middleware/gzip/", + "/pt-br/middleware/jwt/", + "/pt-br/middleware/key-auth/", + "/pt-br/middleware/logger/", + "/pt-br/middleware/method-override/", + "/pt-br/middleware/open-telemetry/", + "/pt-br/middleware/prometheus/", + "/pt-br/middleware/proxy/", + "/pt-br/middleware/rate-limiter/", + "/pt-br/middleware/recover/", + "/pt-br/middleware/redirect/", + "/pt-br/middleware/request-id/", + "/pt-br/middleware/rewrite/", + "/pt-br/middleware/secure/", + "/pt-br/middleware/session/", + "/pt-br/middleware/static/", + "/pt-br/middleware/trailing-slash/", + "/search/", + "/zh-cn/", + "/zh-cn/cookbook/auto-tls/", + "/zh-cn/cookbook/cors/", + "/zh-cn/cookbook/crud/", + "/zh-cn/cookbook/embed-resources/", + "/zh-cn/cookbook/file-download/", + "/zh-cn/cookbook/file-upload/", + "/zh-cn/cookbook/graceful-shutdown/", + "/zh-cn/cookbook/hello-world/", + "/zh-cn/cookbook/http2-server-push/", + "/zh-cn/cookbook/http2/", + "/zh-cn/cookbook/jsonp/", + "/zh-cn/cookbook/jwt/", + "/zh-cn/cookbook/load-balancing/", + "/zh-cn/cookbook/middleware/", + "/zh-cn/cookbook/reverse-proxy/", + "/zh-cn/cookbook/sse/", + "/zh-cn/cookbook/streaming-response/", + "/zh-cn/cookbook/subdomain/", + "/zh-cn/cookbook/timeout/", + "/zh-cn/cookbook/websocket/", + "/zh-cn/guide/binding/", + "/zh-cn/guide/context/", + "/zh-cn/guide/cookies/", + "/zh-cn/guide/customization/", + "/zh-cn/guide/error-handling/", + "/zh-cn/guide/installation/", + "/zh-cn/guide/ip-address/", + "/zh-cn/guide/quickstart/", + "/zh-cn/guide/request/", + "/zh-cn/guide/response/", + "/zh-cn/guide/routing/", + "/zh-cn/guide/static-files/", + "/zh-cn/guide/templates/", + "/zh-cn/guide/testing/", + "/zh-cn/middleware/", + "/zh-cn/middleware/basic-auth/", + "/zh-cn/middleware/body-dump/", + "/zh-cn/middleware/body-limit/", + "/zh-cn/middleware/casbin-auth/", + "/zh-cn/middleware/context-timeout/", + "/zh-cn/middleware/cors/", + "/zh-cn/middleware/csrf/", + "/zh-cn/middleware/decompress/", + "/zh-cn/middleware/gzip/", + "/zh-cn/middleware/jwt/", + "/zh-cn/middleware/key-auth/", + "/zh-cn/middleware/logger/", + "/zh-cn/middleware/method-override/", + "/zh-cn/middleware/open-telemetry/", + "/zh-cn/middleware/prometheus/", + "/zh-cn/middleware/proxy/", + "/zh-cn/middleware/rate-limiter/", + "/zh-cn/middleware/recover/", + "/zh-cn/middleware/redirect/", + "/zh-cn/middleware/request-id/", + "/zh-cn/middleware/rewrite/", + "/zh-cn/middleware/secure/", + "/zh-cn/middleware/session/", + "/zh-cn/middleware/static/", + "/zh-cn/middleware/trailing-slash/" + ] +} diff --git a/site/scripts/accept-source.mjs b/site/scripts/accept-source.mjs index 192fb708..54ebe044 100644 --- a/site/scripts/accept-source.mjs +++ b/site/scripts/accept-source.mjs @@ -5,5 +5,17 @@ import { fileURLToPath } from 'node:url'; const siteDir = fileURLToPath(new URL('..', import.meta.url)); const manifest = JSON.parse(readFileSync(join(siteDir, 'src/generated/config-fields.json'), 'utf8')); const configs = Object.fromEntries(manifest.configs.map((config) => [config.name, Object.fromEntries(config.fields.map((field) => [field.name, { type: field.type, deprecated: field.deprecated ?? false }]))])); -writeFileSync(join(siteDir, 'reference-baseline.json'), `${JSON.stringify({ configs }, null, 2)}\n`); -console.log(`Accepted ${manifest.configs.length} Echo config types for review`); +const functions = Object.fromEntries(manifest.functions.map((entry) => [entry.name, entry.signature])); +writeFileSync(join(siteDir, 'reference-baseline.json'), `${JSON.stringify({ configs, functions }, null, 2)}\n`); +const sources = JSON.parse(readFileSync(join(siteDir, 'external-sources.json'), 'utf8')); +const external = Object.fromEntries(Object.keys(sources).map((slug) => { + const data = JSON.parse(readFileSync(join(siteDir, `src/generated/${slug}.json`), 'utf8')); + return [slug, { + module: data.module, + version: data.revision, + configs: Object.fromEntries(data.configs.map((config) => [config.name, Object.fromEntries(config.fields.map((field) => [field.name, { type: field.type, deprecated: field.deprecated ?? false }]))])), + functions: Object.fromEntries(data.functions.map((entry) => [entry.name, entry.signature])), + }]; +})); +writeFileSync(join(siteDir, 'external-baseline.json'), `${JSON.stringify(external, null, 2)}\n`); +console.log(`Accepted ${manifest.configs.length} Echo config types, ${manifest.functions.length} functions, and ${Object.keys(external).length} external modules for review`); diff --git a/site/scripts/accept-translations.mjs b/site/scripts/accept-translations.mjs index d6319f3c..ad30fec4 100644 --- a/site/scripts/accept-translations.mjs +++ b/site/scripts/accept-translations.mjs @@ -1,11 +1,24 @@ -import { createHash } from 'node:crypto'; -import { readFileSync, writeFileSync } from 'node:fs'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { locales, pages, sections, siteDir } from './translation-sections.mjs'; -const siteDir = fileURLToPath(new URL('..', import.meta.url)); -const hash = (page) => createHash('sha256').update(readFileSync(join(siteDir, 'src/content/docs/middleware', `${page}.mdx`))).digest('hex'); -const pages = { logger: hash('logger'), static: hash('static') }; -const baseline = Object.fromEntries(['es', 'ja', 'pt-br', 'zh-cn'].map((locale) => [locale, pages])); -writeFileSync(join(siteDir, 'translation-baseline.json'), `${JSON.stringify(baseline, null, 2)}\n`); -console.log('Accepted Request Logger and Static translations for review'); +const [locale, page] = process.argv.slice(2); +if (!locales.includes(locale) || !pages.includes(page)) { + console.error('Usage: npm run translations:accept -- '); + process.exit(1); +} +const path = join(siteDir, 'translation-baseline.json'); +const baseline = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : {}; +const english = sections('', page); +const translated = sections(locale, page); +if (english.length !== translated.length) { + throw new Error(`${locale}/middleware/${page}: ${english.length} English sections and ${translated.length} translated sections`); +} +baseline[locale] ??= {}; +baseline[locale][page] = english.map((section, index) => ({ + heading: section.heading, + source: section.hash, + translation: translated[index].hash, +})); +writeFileSync(path, `${JSON.stringify(baseline, null, 2)}\n`); +console.log(`Recorded reviewed sections for ${locale}/middleware/${page}`); diff --git a/site/scripts/build-site.mjs b/site/scripts/build-site.mjs new file mode 100644 index 00000000..086a5009 --- /dev/null +++ b/site/scripts/build-site.mjs @@ -0,0 +1,41 @@ +import { execFileSync } from 'node:child_process'; +import { cpSync, existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const siteDir = fileURLToPath(new URL('..', import.meta.url)); +const proposedEcho = process.env.ECHO_SOURCE_DIR || ''; + +function run(args, channel, sourceDir = '') { + execFileSync('npm', args, { + cwd: siteDir, + env: { ...process.env, DOCS_CHANNEL: channel, ECHO_SOURCE_DIR: sourceDir }, + stdio: 'inherit', + }); +} + +run(['run', 'source:check'], 'stable'); +run(['run', 'translations:status'], 'stable'); +run(['run', 'astro', '--', 'build'], 'stable'); +run(['run', 'source:check'], 'next', proposedEcho); +run(['run', 'astro', '--', 'build'], 'next', proposedEcho); + +const nextDist = join(siteDir, 'dist-next'); +if (!existsSync(join(nextDist, 'index.html'))) throw new Error(`Next build missing: ${nextDist}`); +function rewriteNextLinks(dir) { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const path = join(dir, entry.name); + if (entry.isDirectory()) { rewriteNextLinks(path); continue; } + if (!path.endsWith('.html')) continue; + const html = readFileSync(path, 'utf8'); + const updated = html.replace(/]*>/g, (tag) => { + if (tag.includes('data-version-switch')) return tag; + return tag.replace(/\bhref="\/((?:(?:es|ja|pt-br|zh-cn)\/)?(?:guide|middleware|cookbook)\/[^\"]*)"/g, 'href="/next/$1"'); + }); + if (updated !== html) writeFileSync(path, updated); + } +} +rewriteNextLinks(nextDist); +cpSync(nextDist, join(siteDir, 'dist', 'next'), { recursive: true }); +run(['run', 'site:check'], 'stable'); +run(['run', 'performance:check'], 'stable'); diff --git a/site/scripts/check-performance.mjs b/site/scripts/check-performance.mjs new file mode 100644 index 00000000..d9cbda9a --- /dev/null +++ b/site/scripts/check-performance.mjs @@ -0,0 +1,52 @@ +import { readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { gzipSync } from 'node:zlib'; + +const siteDir = fileURLToPath(new URL('..', import.meta.url)); +const distDir = join(siteDir, 'dist'); +const baselineFile = join(siteDir, 'performance-baseline.json'); +const routes = ['/', '/middleware/cors/', '/next/', '/next/middleware/cors/']; + +function bytes(path) { + const data = readFileSync(path); + return { raw: data.length, gzip: gzipSync(data).length }; +} + +function metrics(route) { + const htmlFile = join(distDir, route, 'index.html'); + const html = readFileSync(htmlFile, 'utf8'); + const assetPaths = [...html.matchAll(/(?:src|href)="(\/(?:next\/)?_astro\/[^"?#]+)"/g)] + .map((match) => match[1]); + const assets = [...new Set(assetPaths)].filter((path) => /\.(?:js|css)$/.test(path)); + const assetBytes = assets.reduce((sum, path) => { + const size = bytes(join(distDir, path)); + return { raw: sum.raw + size.raw, gzip: sum.gzip + size.gzip }; + }, { raw: 0, gzip: 0 }); + const domains = [...new Set([...html.matchAll(/(?:src|href)="(https?:\/\/[^"?#]+)"/g)] + .map((match) => new URL(match[1]).hostname).filter((host) => host !== 'echo.labstack.com'))].sort(); + return { html: bytes(htmlFile), firstLoadAssets: assetBytes, externalDomains: domains }; +} + +const current = Object.fromEntries(routes.map((route) => [route, metrics(route)])); +if (process.argv.includes('--accept')) { + writeFileSync(baselineFile, `${JSON.stringify(current, null, 2)}\n`); + console.log('Accepted stable and next HTML/asset sizes for review'); + process.exit(0); +} +const baseline = JSON.parse(readFileSync(baselineFile, 'utf8')); +const problems = []; +for (const route of routes) { + const before = baseline[route]; + const after = current[route]; + for (const [part, metric] of [['html', 'gzip'], ['firstLoadAssets', 'gzip']]) { + if (after[part][metric] > before[part][metric] * 1.15 + 1024) { + problems.push(`${route} ${part} gzip grew from ${before[part][metric]} to ${after[part][metric]} bytes`); + } + } + for (const domain of after.externalDomains) { + if (!before.externalDomains.includes(domain)) problems.push(`${route} added external origin ${domain}`); + } +} +if (problems.length) throw new Error(`Review performance baseline changes:\n${problems.join('\n')}`); +console.log('Stable and next HTML/asset sizes are within the recorded baseline'); diff --git a/site/scripts/check-site.mjs b/site/scripts/check-site.mjs new file mode 100644 index 00000000..fa1a7b82 --- /dev/null +++ b/site/scripts/check-site.mjs @@ -0,0 +1,96 @@ +import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'; +import { extname, join, relative, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const siteDir = fileURLToPath(new URL('..', import.meta.url)); +const distDir = join(siteDir, 'dist'); +const baselineFile = join(siteDir, 'route-baseline.json'); +const accept = process.argv.includes('--accept'); +const origin = 'https://echo.labstack.com'; + +function walk(dir) { + return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const path = join(dir, entry.name); + return entry.isDirectory() ? walk(path) : [path]; + }); +} + +const files = walk(distDir); +const htmlFiles = files.filter((file) => file.endsWith('.html')); +const routes = htmlFiles.map((file) => { + const path = `/${relative(distDir, file).replaceAll('\\', '/')}`; + return path.endsWith('/index.html') ? path.slice(0, -10) : path; +}).sort(); + +if (accept) { + writeFileSync(baselineFile, `${JSON.stringify({ routes }, null, 2)}\n`); + console.log(`Accepted ${routes.length} routes for review`); + process.exit(0); +} + +const expected = JSON.parse(readFileSync(baselineFile, 'utf8')).routes; +const problems = []; +for (const route of expected.filter((item) => !routes.includes(item))) problems.push(`Removed route: ${route}`); +for (const route of routes.filter((item) => !expected.includes(item))) problems.push(`New route needs baseline review: ${route}`); +const ids = new Map(); +function idsIn(file) { + if (!ids.has(file)) { + const html = readFileSync(file, 'utf8'); + ids.set(file, new Set([...html.matchAll(/\bid="([^"]+)"/g)].map((match) => match[1]))); + } + return ids.get(file); +} + +let checked = 0; +for (const file of htmlFiles) { + const html = readFileSync(file, 'utf8'); + const markup = html.replace(/]*>[\s\S]*?<\/script>/gi, '').replace(/]*>[\s\S]*?<\/pre>/gi, ''); + const route = routes.find((item) => file === join(distDir, item, 'index.html') || file === join(distDir, item)); + const redirect = route.startsWith('/docs/') || route === '/docs/' || route === '/search/'; + if (!redirect && !/\]*\blang="[^"]+"/.test(html)) problems.push(`${route}: missing document language`); + if (route.endsWith('/404.html')) continue; + for (const [tag] of markup.matchAll(/]*>/g)) { + if (!/\balt(?:\s|=)/.test(tag)) problems.push(`${route}: image missing alt text`); + } + for (const [, names] of markup.matchAll(/\baria-labelledby="([^"]+)"/g)) { + for (const id of names.split(/\s+/)) { + if (id && !idsIn(file).has(id)) problems.push(`${route}: aria-labelledby references missing #${id}`); + } + } + if (route.startsWith('/next/')) { + for (const [tag] of markup.matchAll(/]*>/g)) { + if (tag.includes('data-version-switch')) continue; + const href = tag.match(/\bhref="(\/[^"]+)"/)?.[1]; + if (href && /^\/(?:es\/|ja\/|pt-br\/|zh-cn\/)?(?:guide|middleware|cookbook)\//.test(href)) { + problems.push(`${route}: next content links to stable route ${href}`); + } + } + } + for (const match of markup.matchAll(/\b(?:href|src)="([^"]+)"/g)) { + const value = match[1].replaceAll('&', '&'); + if (!value || value.startsWith('data:') || value.startsWith('mailto:') || value.startsWith('javascript:')) continue; + let target; + try { target = new URL(value, `${origin}${route}`); } catch { continue; } + if (target.origin !== origin) continue; + let pathname; + try { pathname = decodeURIComponent(target.pathname); } catch { problems.push(`${route}: malformed URL ${value}`); continue; } + const resolved = resolve(distDir, `.${pathname}`); + if (!resolved.startsWith(`${distDir}/`) && resolved !== distDir) { problems.push(`${route}: path escapes site ${value}`); continue; } + const targetFile = extname(resolved) ? resolved : join(resolved, 'index.html'); + if (!existsSync(targetFile) || !statSync(targetFile).isFile()) { problems.push(`${route}: missing ${value}`); continue; } + if (target.hash && targetFile.endsWith('.html') && !target.hash.startsWith('#:~:text=')) { + const fragment = decodeURIComponent(target.hash.slice(1)); + if (fragment && !idsIn(targetFile).has(fragment)) problems.push(`${route}: missing anchor ${value}`); + } + checked++; + } +} + +for (const locale of ['es', 'ja', 'pt-br', 'zh-cn']) { + if (!routes.includes(`/${locale}/middleware/`)) problems.push(`Missing ${locale} middleware task page`); +} +for (const path of ['sitemap-index.xml', 'pagefind/pagefind.js']) { + if (!existsSync(join(distDir, path))) problems.push(`Missing ${path}`); +} +if (problems.length) throw new Error(`${problems.length} site check failure(s):\n${problems.join('\n')}`); +console.log(`Checked ${routes.length} routes and ${checked} internal links/assets; all resolve`); diff --git a/site/scripts/check-source.mjs b/site/scripts/check-source.mjs index f43264aa..93f9a3d3 100644 --- a/site/scripts/check-source.mjs +++ b/site/scripts/check-source.mjs @@ -5,6 +5,9 @@ import { fileURLToPath } from 'node:url'; const siteDir = fileURLToPath(new URL('..', import.meta.url)); const manifest = JSON.parse(readFileSync(join(siteDir, 'src/generated/config-fields.json'), 'utf8')); const baseline = JSON.parse(readFileSync(join(siteDir, 'reference-baseline.json'), 'utf8')); +const pages = JSON.parse(readFileSync(join(siteDir, 'reference-pages.json'), 'utf8')); +const externalSources = JSON.parse(readFileSync(join(siteDir, 'external-sources.json'), 'utf8')); +const externalBaseline = JSON.parse(readFileSync(join(siteDir, 'external-baseline.json'), 'utf8')); function fieldsByConfig(configs) { return Object.fromEntries(configs.map((config) => [config.name, Object.fromEntries(config.fields.map((field) => [field.name, { type: field.type, deprecated: field.deprecated ?? false }]))])); @@ -21,16 +24,63 @@ for (const name of new Set([...Object.keys(baseline.configs), ...Object.keys(act } } } +const functions = Object.fromEntries(manifest.functions.map((entry) => [entry.name, entry.signature])); +for (const name of new Set([...Object.keys(baseline.functions), ...Object.keys(functions)])) { + if (baseline.functions[name] !== functions[name]) { + differences.push(`${name}: ${JSON.stringify(baseline.functions[name] ?? null)} -> ${JSON.stringify(functions[name] ?? null)}`); + } +} +for (const [slug, source] of Object.entries(externalSources)) { + const external = JSON.parse(readFileSync(join(siteDir, `src/generated/${slug}.json`), 'utf8')); + const before = externalBaseline[slug]; + if (!before || external.module !== source.module || external.revision !== source.version || before.version !== source.version) { + differences.push(`${slug}: source module/version differs from reviewed baseline`); + continue; + } + const fields = fieldsByConfig(external.configs); + for (const config of new Set([...Object.keys(before.configs), ...Object.keys(fields)])) { + const oldFields = before.configs[config] || {}; + const newFields = fields[config] || {}; + for (const field of new Set([...Object.keys(oldFields), ...Object.keys(newFields)])) { + if (JSON.stringify(oldFields[field]) !== JSON.stringify(newFields[field])) differences.push(`${slug}.${config}.${field}: ${JSON.stringify(oldFields[field] ?? null)} -> ${JSON.stringify(newFields[field] ?? null)}`); + } + } + const api = Object.fromEntries(external.functions.map((entry) => [entry.name, entry.signature])); + for (const name of new Set([...Object.keys(before.functions), ...Object.keys(api)])) { + if (before.functions[name] !== api[name]) differences.push(`${slug}.${name}: ${JSON.stringify(before.functions[name] ?? null)} -> ${JSON.stringify(api[name] ?? null)}`); + } +} if (differences.length) { - throw new Error(`Echo config fields changed. Review the affected pages, then update reference-baseline.json:\n${differences.join('\n')}`); + throw new Error(`Middleware API changed. Review the affected pages, then update the baseline:\n${differences.join('\n')}`); } +const documented = Object.values(pages).flat(); +for (const name of manifest.configs.map((entry) => entry.name)) { + if (documented.filter((entry) => entry === name).length !== 1) { + throw new Error(`${name} must be mapped to exactly one middleware page`); + } +} for (const locale of ['', 'es/', 'ja/', 'pt-br/', 'zh-cn/']) { - for (const [page, config, example] of [['logger', 'RequestLoggerConfig', 'request-logger.go'], ['static', 'StaticConfig', 'static.go']]) { + for (const [page, configs] of Object.entries(pages)) { + const file = join(siteDir, 'src/content/docs', locale, 'middleware', `${page}.mdx`); + const content = readFileSync(file, 'utf8'); + for (const config of configs) { + if (!content.includes(` config. run('go', ['test', './...'], referenceDir, goEnvironment); mkdirSync(generatedDir, { recursive: true }); writeFileSync(join(generatedDir, 'config-fields.json'), `${JSON.stringify(manifest, null, 2)}\n`); +for (const [slug, source] of Object.entries(externalSources)) { + const download = JSON.parse(run('go', ['mod', 'download', '-json', `${source.module}@${source.version}`], repoDir, { GOWORK: 'off' })); + if (download.Error || !download.Dir) throw new Error(`${source.module}@${source.version}: ${download.Error || 'source directory missing'}`); + const extracted = JSON.parse(run('go', [ + 'run', './cmd/config-fields', '-root', download.Dir, '-revision', source.version, + '-module', source.module, '-directory', '.', '-package', source.package, + ], referenceDir, goEnvironment)); + if (extracted.module !== source.module || extracted.revision !== source.version || !extracted.configs.length || !extracted.functions.length) { + throw new Error(`${source.module}@${source.version}: incomplete API manifest`); + } + writeFileSync(join(generatedDir, `${slug}.json`), `${JSON.stringify(extracted, null, 2)}\n`); +} copyFileSync(join(referenceDir, 'request-logger', 'main.go'), join(generatedDir, 'request-logger.go.txt')); copyFileSync(join(referenceDir, 'static', 'main.go'), join(generatedDir, 'static.go.txt')); console.log(`Prepared Echo source ${revision}`); diff --git a/site/scripts/translation-sections.mjs b/site/scripts/translation-sections.mjs new file mode 100644 index 00000000..a2bcdc50 --- /dev/null +++ b/site/scripts/translation-sections.mjs @@ -0,0 +1,31 @@ +import { createHash } from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export const siteDir = fileURLToPath(new URL('..', import.meta.url)); +export const locales = ['es', 'ja', 'pt-br', 'zh-cn']; +export const pages = ['logger', 'static', 'index']; + +export function sections(locale, page) { + const extension = page === 'index' ? 'md' : 'mdx'; + const path = join(siteDir, 'src/content/docs', locale ? `${locale}/middleware` : 'middleware', `${page}.${extension}`); + const content = readFileSync(path, 'utf8'); + const blocks = []; + let heading = 'Introduction'; + let lines = []; + for (const line of content.split('\n')) { + if (/^#{2,3} /.test(line)) { + blocks.push({ heading, text: lines.join('\n').trim() }); + heading = line.replace(/^#{2,3} /, ''); + lines = []; + } else { + lines.push(line); + } + } + blocks.push({ heading, text: lines.join('\n').trim() }); + return blocks.map(({ heading, text }) => ({ + heading, + hash: createHash('sha256').update(`${heading}\n${text}`).digest('hex'), + })); +} diff --git a/site/scripts/translation-status.mjs b/site/scripts/translation-status.mjs index 5848584f..bfb7c365 100644 --- a/site/scripts/translation-status.mjs +++ b/site/scripts/translation-status.mjs @@ -1,17 +1,34 @@ -import { createHash } from 'node:crypto'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { locales, pages, sections, siteDir } from './translation-sections.mjs'; -const siteDir = fileURLToPath(new URL('..', import.meta.url)); const baseline = JSON.parse(readFileSync(join(siteDir, 'translation-baseline.json'), 'utf8')); -const hash = (page) => createHash('sha256').update(readFileSync(join(siteDir, 'src/content/docs/middleware', `${page}.mdx`))).digest('hex'); let stale = 0; -for (const [locale, pages] of Object.entries(baseline)) { - for (const [page, reviewedHash] of Object.entries(pages)) { - const status = reviewedHash === hash(page) ? 'current' : 'needs review'; - if (status !== 'current') stale++; - console.log(`${locale}/middleware/${page}: ${status}`); +for (const locale of locales) { + for (const page of pages) { + const english = sections('', page); + const translated = sections(locale, page); + const accepted = baseline[locale]?.[page]; + if (!Array.isArray(accepted)) { + console.log(`${locale}/middleware/${page}: needs review (no section baseline)`); + stale++; + continue; + } + const count = Math.max(english.length, translated.length, accepted.length); + for (let index = 0; index < count; index++) { + const source = english[index]; + const translation = translated[index]; + const recorded = accepted[index]; + const changes = []; + if (!source || !translation || !recorded) changes.push('section count changed'); + if (source && recorded && source.hash !== recorded.source) changes.push('English changed'); + if (translation && recorded && translation.hash !== recorded.translation) changes.push('translation changed'); + if (changes.length) { + console.log(`${locale}/middleware/${page} §${index + 1} ${source?.heading ?? recorded?.heading ?? 'missing'}: ${changes.join(', ')}`); + stale++; + } + } } } -console.log(`${stale} translation(s) need review; this report does not validate the prose`); +console.log(`${stale} section(s) need review; matching hashes track edits, not translation quality`); +if (stale) process.exitCode = 1; diff --git a/site/src/components/ConfigReference.astro b/site/src/components/ConfigReference.astro index 98cf4ef3..0c7e72f9 100644 --- a/site/src/components/ConfigReference.astro +++ b/site/src/components/ConfigReference.astro @@ -1,26 +1,40 @@ --- -import manifest from '../generated/config-fields.json'; +import echoManifest from '../generated/config-fields.json'; +import jwtManifest from '../generated/jwt.json'; +import prometheusManifest from '../generated/prometheus.json'; +import telemetryManifest from '../generated/open-telemetry.json'; +import externalSources from '../../external-sources.json'; interface Props { name: string; locale?: 'en' | 'es' | 'ja' | 'pt-br' | 'zh-cn'; + source?: 'echo' | 'jwt' | 'prometheus' | 'open-telemetry'; } -const { name, locale = 'en' } = Astro.props; +const { name, locale = 'en', source = 'echo' } = Astro.props; +const manifest = { echo: echoManifest, jwt: jwtManifest, prometheus: prometheusManifest, 'open-telemetry': telemetryManifest }[source]; +const repository = source === 'echo' ? 'https://github.com/labstack/echo' : externalSources[source].repository; const config = manifest.configs.find((entry) => entry.name === name); -if (!config) throw new Error(`Missing Echo source reference for ${name}`); +if (!config) throw new Error(`Missing ${source} source reference for ${name}`); +const stem = name.replace(/Config$/, ''); +const functions = manifest.functions.filter((entry) => + entry.signature.includes(name) || entry.name === stem || entry.name === `New${stem}` || + (name === 'RedirectConfig' && /Redirect$/.test(entry.name)) || + (source === 'jwt' && entry.name === 'JWT') || + (source === 'open-telemetry' && name === 'Config' && entry.name === 'NewMiddleware') +); const labels = { - en: { field: 'Field', type: 'Type', source: 'Source', deprecated: 'Deprecated', caption: 'Fields from Echo source' }, - es: { field: 'Campo', type: 'Tipo', source: 'Fuente', deprecated: 'Obsoleto', caption: 'Campos del código de Echo' }, - ja: { field: 'フィールド', type: '型', source: 'ソース', deprecated: '非推奨', caption: 'Echo のソースコードのフィールド' }, - 'pt-br': { field: 'Campo', type: 'Tipo', source: 'Código', deprecated: 'Obsoleto', caption: 'Campos do código do Echo' }, - 'zh-cn': { field: '字段', type: '类型', source: '源码', deprecated: '已弃用', caption: 'Echo 源码中的字段' }, + en: { field: 'Field', type: 'Type', source: 'Source', deprecated: 'Deprecated', caption: 'Fields from package source', functions: 'Functions from package source' }, + es: { field: 'Campo', type: 'Tipo', source: 'Fuente', deprecated: 'Obsoleto', caption: 'Campos del código fuente', functions: 'Funciones del código fuente' }, + ja: { field: 'フィールド', type: '型', source: 'ソース', deprecated: '非推奨', caption: 'パッケージのソースコードのフィールド', functions: 'パッケージのソースコードの関数' }, + 'pt-br': { field: 'Campo', type: 'Tipo', source: 'Código', deprecated: 'Obsoleto', caption: 'Campos do código-fonte', functions: 'Funções do código-fonte' }, + 'zh-cn': { field: '字段', type: '类型', source: '源码', deprecated: '已弃用', caption: '包源码中的字段', functions: '包源码中的函数' }, }[locale]; -const sourceUrl = (line: number) => `https://github.com/labstack/echo/blob/${manifest.revision}/${config.file}#L${line}`; +const sourceUrl = (file: string, line: number) => `${repository}/blob/${manifest.revision}/${file}#L${line}`; ---
-

{config.name} · Echo {manifest.revision.slice(0, 7)}

+

{config.name} · {manifest.module}@{source === 'echo' ? manifest.revision.slice(0, 7) : manifest.revision}

@@ -29,22 +43,34 @@ const sourceUrl = (line: number) => `https://github.com/labstack/echo/blob/${man {config.fields.map((field) => - + )}
{labels.caption}
{field.name}{field.deprecated && {labels.deprecated}} {field.type}L{field.line}L{field.line}
+ {functions.length > 0 &&
+ + + + + {functions.map((entry) => + + + )} + +
{labels.functions}
{labels.type}{labels.source}
{entry.signature}L{entry.line}
+
}
diff --git a/site/src/components/HomeHero.astro b/site/src/components/HomeHero.astro index abffe3a9..a78a3e00 100644 --- a/site/src/components/HomeHero.astro +++ b/site/src/components/HomeHero.astro @@ -2,12 +2,14 @@ // Living Terminal hero: code window + a terminal pane that types `curl` // and streams Echo's JSON response. Animation is client-side, with a // static final-state fallback for reduced-motion / no-JS. -import { starsLabel, echoVersion } from '../data/github.ts'; +import { starsLabel } from '../data/github.ts'; +import stable from '../../echo-source.json'; +const next = process.env.DOCS_CHANNEL === 'next'; ---
- Echo {echoVersion} — now released + {next ? 'Echo next — preview documentation' : `Echo ${stable.release} — release documentation`}

Build fast Go APIs.
Without the bloat.

A high-performance, minimalist Go web framework — a zero-allocation diff --git a/site/src/components/Search.astro b/site/src/components/Search.astro index 729344a6..2c6888e1 100644 --- a/site/src/components/Search.astro +++ b/site/src/components/Search.astro @@ -11,6 +11,7 @@ import Default from '@astrojs/starlight/components/Search.astro';

- + Echo

The following static files are served via HTTP/2 server push

  • /app.css
  • diff --git a/site/src/content/docs/es/cookbook/http2-server-push.md b/site/src/content/docs/es/cookbook/http2-server-push.md index fd71df5f..c45c60a7 100644 --- a/site/src/content/docs/es/cookbook/http2-server-push.md +++ b/site/src/content/docs/es/cookbook/http2-server-push.md @@ -77,7 +77,7 @@ if err := sc.StartTLS(context.Background(), e, "cert.pem", "key.pem"); err != ni - + Echo

    The following static files are served via HTTP/2 server push

    • /app.css
    • diff --git a/site/src/content/docs/es/cookbook/http2.md b/site/src/content/docs/es/cookbook/http2.md index 2ba77367..4566bfb3 100644 --- a/site/src/content/docs/es/cookbook/http2.md +++ b/site/src/content/docs/es/cookbook/http2.md @@ -9,6 +9,8 @@ HTTP/2 mejora la latencia mediante multiplexing de requests, compresión de head server push. El servidor HTTP de Go negocia HTTP/2 automáticamente sobre TLS, por lo que servir HTTP/2 con Echo consiste en iniciar el servidor con un certificado. + + ## 1. Generar un certificado TLS X.509 autofirmado Ejecuta el siguiente comando para generar `cert.pem` y `key.pem`: diff --git a/site/src/content/docs/es/guide/binding.md b/site/src/content/docs/es/guide/binding.md index 135b49ed..0e338050 100644 --- a/site/src/content/docs/es/guide/binding.md +++ b/site/src/content/docs/es/guide/binding.md @@ -148,6 +148,8 @@ Cada tipo soportado ofrece métodos `Type(...)`, `MustType(...)`, `Types(...)` ( `MustTypes(...)`, por ejemplo `Int64`, `MustInt64`, `Int64s`. Usa `BindWithDelimiter("id", &dest, ",")` para separar valores unidos por comas. + + ## Binder personalizado Registra un binder personalizado mediante `Echo#Binder`: diff --git a/site/src/content/docs/es/guide/customization.md b/site/src/content/docs/es/guide/customization.md index e9759c52..2762f6f8 100644 --- a/site/src/content/docs/es/guide/customization.md +++ b/site/src/content/docs/es/guide/customization.md @@ -43,6 +43,8 @@ en [json.go](https://github.com/labstack/echo/blob/master/json.go). [Aprende más](/es/guide/templates/) + + ## Handler de errores HTTP `Echo#HTTPErrorHandler` registra un handler de errores HTTP personalizado. diff --git a/site/src/content/docs/es/guide/request.md b/site/src/content/docs/es/guide/request.md index 2e17ed2d..21264784 100644 --- a/site/src/content/docs/es/guide/request.md +++ b/site/src/content/docs/es/guide/request.md @@ -106,6 +106,8 @@ curl http://localhost:1323/users/123 Echo también puede vincular datos de request a structs y variables nativas de Go. Consulta [Binding](/es/guide/binding/). + + ## Validar datos Echo no incluye validación de datos integrada. Puedes registrar un validator personalizado mediante diff --git a/site/src/content/docs/es/middleware/basic-auth.md b/site/src/content/docs/es/middleware/basic-auth.mdx similarity index 67% rename from site/src/content/docs/es/middleware/basic-auth.md rename to site/src/content/docs/es/middleware/basic-auth.mdx index 0ce5538e..be22c7d2 100644 --- a/site/src/content/docs/es/middleware/basic-auth.md +++ b/site/src/content/docs/es/middleware/basic-auth.mdx @@ -5,6 +5,8 @@ sidebar: order: 1 --- +import ConfigReference from '../../../../components/ConfigReference.astro'; + El middleware Basic Auth proporciona autenticación HTTP basic. - Para credenciales válidas llama al siguiente handler. @@ -37,27 +39,7 @@ e.Use(middleware.BasicAuthWithConfig(middleware.BasicAuthConfig{})) ## Configuración -```go -type BasicAuthConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Validator validates the credentials. If the request contains multiple basic - // auth headers, it is called once for each header until the first valid result. - // Required. - Validator BasicAuthValidator - - // Realm is the realm attribute of the WWW-Authenticate header. - // Default value "Restricted". - Realm string - - // AllowedCheckLimit sets how many headers are allowed to be checked. This is - // useful in environments such as corporate test setups with application proxies - // restricting access with their own auth scheme. - // Default value 1. - AllowedCheckLimit uint -} -``` + `Validator` tiene esta firma: diff --git a/site/src/content/docs/es/middleware/body-dump.md b/site/src/content/docs/es/middleware/body-dump.mdx similarity index 59% rename from site/src/content/docs/es/middleware/body-dump.md rename to site/src/content/docs/es/middleware/body-dump.mdx index 5de4d32e..64c92cf9 100644 --- a/site/src/content/docs/es/middleware/body-dump.md +++ b/site/src/content/docs/es/middleware/body-dump.mdx @@ -5,6 +5,8 @@ sidebar: order: 2 --- +import ConfigReference from '../../../../components/ConfigReference.astro'; + El middleware Body Dump captura los payloads de request y response y los pasa a un handler registrado. Generalmente se usa para debugging o logging. @@ -37,28 +39,7 @@ e.Use(middleware.BodyDumpWithConfig(middleware.BodyDumpConfig{})) ## Configuración -```go -type BodyDumpConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Handler receives the request and response payloads and the handler error, if any. - // Required. - Handler BodyDumpHandler - - // MaxRequestBytes limits how much of the request body to dump. If the request body - // exceeds this limit, only the first MaxRequestBytes are dumped and the handler - // receives truncated data. - // Default: 5 * MB (5,242,880 bytes). Set to -1 to disable limits (not recommended). - MaxRequestBytes int64 - - // MaxResponseBytes limits how much of the response body to dump. If the response body - // exceeds this limit, only the first MaxResponseBytes are dumped and the handler - // receives truncated data. - // Default: 5 * MB (5,242,880 bytes). Set to -1 to disable limits (not recommended). - MaxResponseBytes int64 -} -``` + `Handler` tiene esta firma: diff --git a/site/src/content/docs/es/middleware/body-limit.md b/site/src/content/docs/es/middleware/body-limit.mdx similarity index 83% rename from site/src/content/docs/es/middleware/body-limit.md rename to site/src/content/docs/es/middleware/body-limit.mdx index 2a19b210..8364609d 100644 --- a/site/src/content/docs/es/middleware/body-limit.md +++ b/site/src/content/docs/es/middleware/body-limit.mdx @@ -5,6 +5,8 @@ sidebar: order: 3 --- +import ConfigReference from '../../../../components/ConfigReference.astro'; + El middleware Body Limit establece el tamaño máximo permitido para un body de request. Si el tamaño supera el límite configurado, envía una response `413 Request Entity Too Large`. @@ -33,15 +35,7 @@ e.Use(middleware.BodyLimitWithConfig(middleware.BodyLimitConfig{})) ## Configuración -```go -type BodyLimitConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // LimitBytes is the maximum allowed size in bytes for a request body. - LimitBytes int64 -} -``` + ### Configuración por defecto diff --git a/site/src/content/docs/es/middleware/context-timeout.md b/site/src/content/docs/es/middleware/context-timeout.mdx similarity index 73% rename from site/src/content/docs/es/middleware/context-timeout.md rename to site/src/content/docs/es/middleware/context-timeout.mdx index e6754080..b0a5ef2c 100644 --- a/site/src/content/docs/es/middleware/context-timeout.md +++ b/site/src/content/docs/es/middleware/context-timeout.mdx @@ -5,6 +5,8 @@ sidebar: order: 5 --- +import ConfigReference from '../../../../components/ConfigReference.astro'; + El middleware Context Timeout aplica un timeout al contexto del request dentro de un periodo predefinido, para que los métodos conscientes del contexto puedan retornar antes cuando se supera el deadline. @@ -30,18 +32,7 @@ e.Use(middleware.ContextTimeoutWithConfig(middleware.ContextTimeoutConfig{ ## Configuración -```go -type ContextTimeoutConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // ErrorHandler is a function invoked when an error arises during middleware execution. - ErrorHandler func(c *echo.Context, err error) error - - // Timeout configures the timeout for the middleware. - Timeout time.Duration -} -``` + ### Configuración por defecto diff --git a/site/src/content/docs/es/middleware/cors.md b/site/src/content/docs/es/middleware/cors.md deleted file mode 100644 index 139e5514..00000000 --- a/site/src/content/docs/es/middleware/cors.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -title: CORS -description: Middleware Cross-Origin Resource Sharing para control de acceso seguro entre dominios. -sidebar: - order: 6 ---- - -El middleware CORS implementa la especificación [CORS](https://fetch.spec.whatwg.org/#http-cors-protocol). -CORS da a los servidores web controles de acceso entre dominios, lo que permite transferencias de datos seguras entre dominios. - -Todo el middleware principal reside en el paquete `middleware`: - -```go -import "github.com/labstack/echo/v5/middleware" -``` - -## Uso - -```go -e.Use(middleware.CORS("https://example.com", "https://subdomain.example.com")) -``` - -## Configuración personalizada - -```go -e := echo.New() -e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ - AllowOrigins: []string{"https://labstack.com", "https://labstack.net"}, - AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept}, -})) -``` - -## Configuración - -```go -type CORSConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // AllowOrigins determines the value of the Access-Control-Allow-Origin response - // header, defining the list of origins that may access the resource. - // - // An origin consists of: scheme + "://" + host + optional ":" + port. - // A wildcard may be used, but it must be set explicitly as []string{"*"}. - // Example: `https://example.com`, `http://example.com:8080`, `*`. - // - // Security: use extreme caution when handling the origin and carefully validate any - // logic. Attackers may register hostile domain names. See - // https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html - // - // Mandatory. - AllowOrigins []string - - // UnsafeAllowOriginFunc is an optional custom function to validate the origin. It - // takes the origin and returns the allowed origin, whether it is allowed, and an - // error (returned immediately by the handler). If set, AllowOrigins is ignored. - // - // Security: use extreme caution when handling the origin. Attackers may register - // hostile (sub)domain names. - // - // Sub-domain check example: - // UnsafeAllowOriginFunc: func(c *echo.Context, origin string) (string, bool, error) { - // if strings.HasSuffix(origin, ".example.com") { - // return origin, true, nil - // } - // return "", false, nil - // } - // - // Optional. - UnsafeAllowOriginFunc func(c *echo.Context, origin string) (allowedOrigin string, allowed bool, err error) - - // AllowMethods determines the value of the Access-Control-Allow-Methods response - // header, used in response to a preflight request. - // - // Optional. Defaults to GET, HEAD, PUT, PATCH, POST, DELETE. If left empty, the - // middleware fills the preflight Access-Control-Allow-Methods header from the - // `Allow` header that the router set into the context. - AllowMethods []string - - // AllowHeaders determines the value of the Access-Control-Allow-Headers response - // header, indicating which HTTP headers can be used in the actual request. - // - // Optional. Defaults to an empty list. - AllowHeaders []string - - // AllowCredentials determines the value of the Access-Control-Allow-Credentials - // response header, indicating whether the response can be exposed when the - // credentials mode is true. - // - // Optional. Default value false, in which case the header is not set. - // - // Security: avoid using AllowCredentials = true together with AllowOrigins = *. - AllowCredentials bool - - // ExposeHeaders determines the value of Access-Control-Expose-Headers, the list of - // headers clients are allowed to access. - // - // Optional. Default value []string{}, in which case the header is not set. - ExposeHeaders []string - - // MaxAge determines the value of the Access-Control-Max-Age response header, how long - // (in seconds) the results of a preflight request can be cached. The header is set - // only if MaxAge != 0; a negative value sends "0", instructing browsers not to cache. - // - // Optional. Default value 0 — the header is not sent. - MaxAge int -} -``` - -### Configuración por defecto - -```go -// Effective defaults applied when fields are left unset. -CORSConfig{ - Skipper: DefaultSkipper, - AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete}, -} -``` - -## Seguridad - -Un origin con wildcard (`AllowOrigins: []string{"*"}`) combinado con `AllowCredentials: true` -es peligroso: reflejaría el `Origin` de **cualquier** petición en -`Access-Control-Allow-Origin`, permitiendo que una página de cualquier sitio haga peticiones -cross-origin con credenciales a tu API (consulta [Exploiting CORS misconfigurations](https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html)). - -Echo rechaza esta combinación en lugar de construir un middleware inseguro: `CORS` y -`CORSWithConfig` hacen **panic**, y `CORSConfig.ToMiddleware()` devuelve un error. Para permitir -peticiones con credenciales, enumera explícitamente los orígenes de confianza: - -```go -e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ - AllowOrigins: []string{"https://example.com"}, - AllowCredentials: true, -})) -``` - -Para validación dinámica de origin, usa `UnsafeAllowOriginFunc` y valida cada origin con -cuidado: los atacantes pueden registrar nombres de (sub)dominio falsos u hostiles. diff --git a/site/src/content/docs/es/middleware/cors.mdx b/site/src/content/docs/es/middleware/cors.mdx new file mode 100644 index 00000000..31250f88 --- /dev/null +++ b/site/src/content/docs/es/middleware/cors.mdx @@ -0,0 +1,68 @@ +--- +title: CORS +description: Middleware Cross-Origin Resource Sharing para control de acceso seguro entre dominios. +sidebar: + order: 6 +--- + +import ConfigReference from '../../../../components/ConfigReference.astro'; + +El middleware CORS implementa la especificación [CORS](https://fetch.spec.whatwg.org/#http-cors-protocol). +CORS da a los servidores web controles de acceso entre dominios, lo que permite transferencias de datos seguras entre dominios. + +Todo el middleware principal reside en el paquete `middleware`: + +```go +import "github.com/labstack/echo/v5/middleware" +``` + +## Uso + +```go +e.Use(middleware.CORS("https://example.com", "https://subdomain.example.com")) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: []string{"https://labstack.com", "https://labstack.net"}, + AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept}, +})) +``` + +## Configuración + + + +### Configuración por defecto + +```go +// Effective defaults applied when fields are left unset. +CORSConfig{ + Skipper: DefaultSkipper, + AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete}, +} +``` + +## Seguridad + +Un origin con wildcard (`AllowOrigins: []string{"*"}`) combinado con `AllowCredentials: true` +es peligroso: reflejaría el `Origin` de **cualquier** petición en +`Access-Control-Allow-Origin`, permitiendo que una página de cualquier sitio haga peticiones +cross-origin con credenciales a tu API (consulta [Exploiting CORS misconfigurations](https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html)). + +Echo rechaza esta combinación en lugar de construir un middleware inseguro: `CORS` y +`CORSWithConfig` hacen **panic**, y `CORSConfig.ToMiddleware()` devuelve un error. Para permitir +peticiones con credenciales, enumera explícitamente los orígenes de confianza: + +```go +e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: []string{"https://example.com"}, + AllowCredentials: true, +})) +``` + +Para validación dinámica de origin, usa `UnsafeAllowOriginFunc` y valida cada origin con +cuidado: los atacantes pueden registrar nombres de (sub)dominio falsos u hostiles. diff --git a/site/src/content/docs/es/middleware/csrf.md b/site/src/content/docs/es/middleware/csrf.mdx similarity index 61% rename from site/src/content/docs/es/middleware/csrf.md rename to site/src/content/docs/es/middleware/csrf.mdx index 787796dc..1b4ae669 100644 --- a/site/src/content/docs/es/middleware/csrf.md +++ b/site/src/content/docs/es/middleware/csrf.mdx @@ -5,6 +5,8 @@ sidebar: order: 7 --- +import ConfigReference from '../../../../components/ConfigReference.astro'; + Cross-Site Request Forgery (CSRF, a veces pronunciado "sea-surf", o XSRF) es un tipo de exploit malicioso en el que se transmiten comandos no autorizados desde un usuario en el que un sitio web confía. @@ -93,75 +95,7 @@ middleware.CSRFWithConfig(middleware.CSRFConfig{ ## Configuración -```go -type CSRFConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // TrustedOrigins permits any request with a `Sec-Fetch-Site` header whose `Origin` - // header exactly matches one of the listed values. Values should be formatted as - // the Origin header: "scheme://host[:port]". - TrustedOrigins []string - - // AllowSecFetchSiteFunc allows custom behaviour for `Sec-Fetch-Site` requests that - // are about to fail with a CSRF error, to be allowed or replaced with a custom - // error. Applies to `same-site` and `cross-site` values. - AllowSecFetchSiteFunc func(c *echo.Context) (bool, error) - - // TokenLength is the length of the generated token. - // Optional. Default value 32. - TokenLength uint8 - - // TokenLookup is a string in the form ":" or - // ":,:" used to extract the token from the request. - // Optional. Default value "header:X-CSRF-Token". - // Possible values: - // - "header:" or "header::" - // - "query:" - // - "form:" - // Multiple sources example: "header:X-CSRF-Token,query:csrf". - TokenLookup string `yaml:"token_lookup"` - - // Generator defines a function to generate the token. - // Optional. Defaults to randomString(TokenLength). - Generator func() string - - // ContextKey is the key under which the generated CSRF token is stored in the context. - // Optional. Default value "csrf". - ContextKey string - - // CookieName is the name of the CSRF cookie that stores the token. - // Optional. Default value "_csrf". - CookieName string - - // CookieDomain is the domain of the CSRF cookie. - // Optional. Default value none. - CookieDomain string - - // CookiePath is the path of the CSRF cookie. - // Optional. Default value none. - CookiePath string - - // CookieMaxAge is the max age (in seconds) of the CSRF cookie. - // Optional. Default value 86400 (24h). - CookieMaxAge int - - // CookieSecure indicates whether the CSRF cookie is secure. - // Optional. Default value false. - CookieSecure bool - - // CookieHTTPOnly indicates whether the CSRF cookie is HTTP only. - // Optional. Default value false. - CookieHTTPOnly bool - - // CookieSameSite indicates the SameSite mode of the CSRF cookie. - // Optional. Default value SameSiteDefaultMode. - CookieSameSite http.SameSite - - // ErrorHandler defines a function that returns custom errors. - ErrorHandler func(c *echo.Context, err error) error -} -``` + ### Configuración por defecto diff --git a/site/src/content/docs/es/middleware/decompress.md b/site/src/content/docs/es/middleware/decompress.mdx similarity index 61% rename from site/src/content/docs/es/middleware/decompress.md rename to site/src/content/docs/es/middleware/decompress.mdx index 9381e08a..976baba0 100644 --- a/site/src/content/docs/es/middleware/decompress.md +++ b/site/src/content/docs/es/middleware/decompress.mdx @@ -5,6 +5,8 @@ sidebar: order: 8 --- +import ConfigReference from '../../../../components/ConfigReference.astro'; + El middleware Decompress descomprime el body del request HTTP cuando el header `Content-Encoding` está establecido en `gzip`. @@ -36,22 +38,7 @@ e.Use(middleware.DecompressWithConfig(middleware.DecompressConfig{ ## Configuración -```go -type DecompressConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // GzipDecompressPool provides the sync.Pool used to create and store gzip readers. - GzipDecompressPool Decompressor - - // MaxDecompressedSize limits the maximum size of the decompressed request body in - // bytes. If the decompressed body exceeds this limit, the middleware returns an - // HTTP 413 error. This prevents zip-bomb attacks where a small compressed payload - // decompresses to a huge size. - // Default: 100 * MB (104,857,600 bytes). Set to -1 to disable limits (not recommended). - MaxDecompressedSize int64 -} -``` + ### Configuración por defecto diff --git a/site/src/content/docs/es/middleware/gzip.md b/site/src/content/docs/es/middleware/gzip.mdx similarity index 62% rename from site/src/content/docs/es/middleware/gzip.md rename to site/src/content/docs/es/middleware/gzip.mdx index 25e40e07..9e2ea1dd 100644 --- a/site/src/content/docs/es/middleware/gzip.md +++ b/site/src/content/docs/es/middleware/gzip.mdx @@ -5,6 +5,8 @@ sidebar: order: 9 --- +import ConfigReference from '../../../../components/ConfigReference.astro'; + El middleware Gzip comprime la response HTTP usando el esquema de compresión gzip. Todo el middleware principal reside en el paquete `middleware`: @@ -43,25 +45,7 @@ e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ ## Configuración -```go -type GzipConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Level is the gzip compression level. - // Optional. Default value -1. - Level int - - // MinLength is the length threshold before gzip compression is applied. - // Optional. Default value 0. - // - // Most of the time the default is fine. Compressing a short response might increase - // the transmitted data because of gzip's format overhead, and compression consumes - // CPU and time on both server and client. Depending on your use case such a - // threshold can be useful. - MinLength int -} -``` + ### Configuración por defecto diff --git a/site/src/content/docs/es/middleware/index.md b/site/src/content/docs/es/middleware/index.md new file mode 100644 index 00000000..9ee5d940 --- /dev/null +++ b/site/src/content/docs/es/middleware/index.md @@ -0,0 +1,30 @@ +--- +title: Elige un middleware +description: Empieza por la tarea, sigue un ejemplo ejecutable y consulta la API exacta de Echo. +sidebar: + order: 0 +--- + +¿Nuevo en Echo? Crea primero un servidor con la [guía de inicio](../guide/quickstart/). Cada página explica cuándo usar el middleware, muestra cómo configurarlo y presenta campos y firmas tomados de una revisión concreta de Echo. + +## ¿Qué necesitas hacer? + +| Tarea | Empieza aquí | Pruébalo | +| --- | --- | --- | +| Ver peticiones y errores | [Request Logger](./logger/) y [Recover](./recover/) | Ejecuta el [ejemplo de logging](./logger/) | +| Permitir peticiones desde el navegador | [CORS](./cors/) | Usa orígenes de confianza explícitos | +| Proteger formularios | [CSRF](./csrf/) | Elige cómo guardar el token | +| Autenticar una API | [JWT](./jwt/) | Ejecuta el [recetario de JWT](../cookbook/jwt/) | +| Limitar peticiones abusivas | [Rate Limiter](./rate-limiter/) | Elige identificador y almacenamiento | +| Servir archivos | [Static](./static/) | Ejecuta el [ejemplo de archivos estáticos](./static/) | +| Reenviar peticiones | [Proxy](./proxy/) | Ejecuta el [recetario de proxy inverso](../cookbook/reverse-proxy/) | + +## Del código a una petición real + +1. Elige una tarea y copia el ejemplo de uso más pequeño. +2. Consulta la configuración para ver los campos y firmas de la revisión indicada de Echo. Lee los valores por defecto y las notas de seguridad antes de cambiar el comportamiento. +3. Ejecuta el ejemplo completo o la receta enlazada y envía una petición con `curl`. + +Para una primera API, sigue con [rutas](../guide/routing/), [binding](../guide/binding/), [gestión de errores](../guide/error-handling/) y [pruebas](../guide/testing/). Para tráfico de producción, añade [Recover](./recover/) y [Request Logger](./logger/) antes de elegir los middleware de seguridad. + +Integraciones como JWT se mantienen en módulos separados; sus páginas indican el paquete responsable. La API del middleware principal está en [`github.com/labstack/echo/v5/middleware`](https://pkg.go.dev/github.com/labstack/echo/v5/middleware). diff --git a/site/src/content/docs/es/middleware/jwt.md b/site/src/content/docs/es/middleware/jwt.md deleted file mode 100644 index 97cbbd8f..00000000 --- a/site/src/content/docs/es/middleware/jwt.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -title: JWT -description: Middleware de autenticación JSON Web Token provisto por el módulo echo-jwt. -sidebar: - order: 10 ---- - -El middleware JWT proporciona autenticación JSON Web Token (JWT). Vive en un módulo separado: -[github.com/labstack/echo-jwt](https://github.com/labstack/echo-jwt). - -Comportamiento: - -- Para un token válido, establece el usuario en el contexto y llama al siguiente handler. -- Para un token inválido, envía una response `401 Unauthorized`. -- Para un header `Authorization` ausente o inválido, envía una response `400 Bad Request`. - -## Dependencias - -```go -import "github.com/labstack/echo-jwt/v5" -``` - -## Uso - -```go -e.Use(echojwt.JWT([]byte("secret"))) -``` - -## Configuración personalizada - -```go -e.Use(echojwt.WithConfig(echojwt.Config{ - SigningKey: []byte("secret"), -})) -``` - -## Configuración - -```go -type Config struct { - // Skipper defines a function to skip middleware. - Skipper middleware.Skipper - - // BeforeFunc defines a function which is executed just before the middleware. - BeforeFunc middleware.BeforeFunc - - // SuccessHandler defines a function executed for a valid token. If it returns an - // error, the middleware stops the handler chain and returns that error. - SuccessHandler func(c *echo.Context) error - - // ErrorHandler defines a function executed when all lookups have been done and none - // passed the Validator. It runs with the last missing (ErrExtractionValueMissing) - // or invalid key, and may be used to define a custom JWT error. - // - // Note: when the error handler swallows the error (returns nil), the middleware - // continues the handler chain. This is useful when part of your site/api is public - // and offers extra features for authorized users; the handler can set a default - // public JWT token value in the request and continue. - ErrorHandler func(c *echo.Context, err error) error - - // ContinueOnIgnoredError allows the next middleware/handler to be called when the - // ErrorHandler ignores the error (returns nil). - ContinueOnIgnoredError bool - - // ContextKey is the key under which user information from the token is stored in the context. - // Optional. Default value "user". - ContextKey string - - // SigningKey is the signing key used to validate the token. One of the three options - // to provide a token validation key. Order of precedence: user-defined KeyFunc, - // SigningKeys, then SigningKey. - // Required if neither a user-defined KeyFunc nor SigningKeys is provided. - SigningKey any - - // SigningKeys is a map of signing keys to validate tokens using the kid field. One of - // the three options to provide a token validation key. - // Required if neither a user-defined KeyFunc nor SigningKey is provided. - SigningKeys map[string]any - - // SigningMethod is the signing method used to check the token's signing algorithm. - // Not checked when a user-defined KeyFunc is provided. - // Optional. Default value HS256. - SigningMethod string - - // KeyFunc supplies the public key for token validation. It must verify the signing - // algorithm and select the proper key. Useful when tokens are issued by an external - // party. When provided, SigningKey, SigningKeys and SigningMethod are ignored. - // One of the three options to provide a token validation key, and not used if a - // custom ParseTokenFunc is set. - KeyFunc jwt.Keyfunc - - // TokenLookup is a string in the form ":" or - // ":,:" used to extract the token from the request. - // Optional. Default value "header:Authorization". - // Possible values: - // - "header:" or "header::" - // trims a static prefix from the extracted value. For JWT tokens with - // `Authorization: Bearer `, the prefix to cut is `Bearer ` (note the space). - // If the prefix is empty, the whole value is returned. - // - "query:" - // - "param:" - // - "cookie:" - // - "form:" - // Multiple sources example: "header:Authorization:Bearer ,cookie:myowncookie". - TokenLookup string - - // TokenLookupFuncs is a list of user-defined functions that extract the JWT token - // from the context. One of two options to provide a token extractor. Order of - // precedence: TokenLookupFuncs, then TokenLookup. Both may be provided. - TokenLookupFuncs []middleware.ValuesExtractor - - // ParseTokenFunc parses the token from the given auth string, returning an error when - // parsing fails or the token is invalid. - // Defaults to an implementation using github.com/golang-jwt/jwt. - ParseTokenFunc func(c *echo.Context, auth string) (any, error) - - // NewClaimsFunc returns the extendable claims defining token content. Used by the - // default ParseTokenFunc; not used if a custom ParseTokenFunc is set. - // Optional. Defaults to a function returning jwt.MapClaims. - NewClaimsFunc func(c *echo.Context) jwt.Claims -} -``` - -## Ejemplo - -Consulta el [recetario de JWT](/es/cookbook/jwt/) para ver un ejemplo completo. diff --git a/site/src/content/docs/es/middleware/jwt.mdx b/site/src/content/docs/es/middleware/jwt.mdx new file mode 100644 index 00000000..6a404d5b --- /dev/null +++ b/site/src/content/docs/es/middleware/jwt.mdx @@ -0,0 +1,45 @@ +--- +title: JWT +description: Middleware de autenticación JSON Web Token provisto por el módulo echo-jwt. +sidebar: + order: 10 +--- + +import ConfigReference from '../../../../components/ConfigReference.astro'; + +El middleware JWT proporciona autenticación JSON Web Token (JWT). Vive en un módulo separado: +[github.com/labstack/echo-jwt](https://github.com/labstack/echo-jwt). + +Comportamiento: + +- Para un token válido, establece el usuario en el contexto y llama al siguiente handler. +- Para un token inválido, envía una response `401 Unauthorized`. +- Para un header `Authorization` ausente o inválido, envía una response `400 Bad Request`. + +## Dependencias + +```go +import "github.com/labstack/echo-jwt/v5" +``` + +## Uso + +```go +e.Use(echojwt.JWT([]byte("secret"))) +``` + +## Configuración personalizada + +```go +e.Use(echojwt.WithConfig(echojwt.Config{ + SigningKey: []byte("secret"), +})) +``` + +## Configuración + + + +## Ejemplo + +Consulta el [recetario de JWT](/es/cookbook/jwt/) para ver un ejemplo completo. diff --git a/site/src/content/docs/es/middleware/key-auth.md b/site/src/content/docs/es/middleware/key-auth.md deleted file mode 100644 index 6d2da984..00000000 --- a/site/src/content/docs/es/middleware/key-auth.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -title: Key Auth -description: Middleware de autenticación basada en clave que valida una API key desde header, query, form o cookie. -sidebar: - order: 11 ---- - -El middleware Key Auth proporciona autenticación basada en clave. - -- Para una clave válida llama al siguiente handler. -- Para una clave inválida, envía una response `401 Unauthorized`. -- Para una clave ausente, envía una response `400 Bad Request`. - -Todo el middleware principal reside en el paquete `middleware`: - -```go -import "github.com/labstack/echo/v5/middleware" -``` - -## Uso - -```go -e.Use(middleware.KeyAuth(func(c *echo.Context, key string, source middleware.ExtractorSource) (bool, error) { - return key == "valid-key", nil -})) -``` - -## Configuración personalizada - -```go -e := echo.New() -e.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{ - KeyLookup: "query:api-key", - Validator: func(c *echo.Context, key string, source middleware.ExtractorSource) (bool, error) { - return key == "valid-key", nil - }, -})) -``` - -## Configuración - -```go -type KeyAuthConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // KeyLookup is a string in the form ":" or - // ":,:" used to extract the key from the request. - // Optional. Default value "header:Authorization:Bearer ". - // Possible values: - // - "header:" or "header::" - // trims a static prefix from the extracted value. For - // `Authorization: Basic `, the prefix to remove is `Basic `. - // - "query:" - // - "form:" - // - "cookie:" - // Multiple sources example: "header:Authorization,header:X-Api-Key". - KeyLookup string - - // AllowedCheckLimit sets how many KeyLookup values are allowed to be checked. This is - // useful in environments such as corporate test setups with application proxies - // restricting access with their own auth scheme. - AllowedCheckLimit uint - - // Validator validates the key. - // Required. - Validator KeyAuthValidator - - // ErrorHandler defines a function executed when all lookups have been done and none - // passed the Validator. It runs with the last missing (ErrExtractionValueMissing) or - // invalid key, and may be used to define a custom error. - // - // Note: when the error handler swallows the error (returns nil), the middleware - // continues the handler chain. This is useful when part of your site/api is public - // and offers extra features for authorized users. - ErrorHandler KeyAuthErrorHandler - - // ContinueOnIgnoredError allows the next middleware/handler to be called when the - // ErrorHandler ignores the error (returns nil). - ContinueOnIgnoredError bool -} -``` - -`Validator` tiene esta firma: - -```go -type KeyAuthValidator func(c *echo.Context, key string, source ExtractorSource) (bool, error) -``` - -### Configuración por defecto - -```go -DefaultKeyAuthConfig = KeyAuthConfig{ - Skipper: DefaultSkipper, - KeyLookup: "header:" + echo.HeaderAuthorization + ":Bearer ", -} -``` diff --git a/site/src/content/docs/es/middleware/key-auth.mdx b/site/src/content/docs/es/middleware/key-auth.mdx new file mode 100644 index 00000000..ae3adb58 --- /dev/null +++ b/site/src/content/docs/es/middleware/key-auth.mdx @@ -0,0 +1,59 @@ +--- +title: Key Auth +description: Middleware de autenticación basada en clave que valida una API key desde header, query, form o cookie. +sidebar: + order: 11 +--- + +import ConfigReference from '../../../../components/ConfigReference.astro'; + +El middleware Key Auth proporciona autenticación basada en clave. + +- Para una clave válida llama al siguiente handler. +- Para una clave inválida, envía una response `401 Unauthorized`. +- Para una clave ausente, envía una response `400 Bad Request`. + +Todo el middleware principal reside en el paquete `middleware`: + +```go +import "github.com/labstack/echo/v5/middleware" +``` + +## Uso + +```go +e.Use(middleware.KeyAuth(func(c *echo.Context, key string, source middleware.ExtractorSource) (bool, error) { + return key == "valid-key", nil +})) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{ + KeyLookup: "query:api-key", + Validator: func(c *echo.Context, key string, source middleware.ExtractorSource) (bool, error) { + return key == "valid-key", nil + }, +})) +``` + +## Configuración + + + +`Validator` tiene esta firma: + +```go +type KeyAuthValidator func(c *echo.Context, key string, source ExtractorSource) (bool, error) +``` + +### Configuración por defecto + +```go +DefaultKeyAuthConfig = KeyAuthConfig{ + Skipper: DefaultSkipper, + KeyLookup: "header:" + echo.HeaderAuthorization + ":Bearer ", +} +``` diff --git a/site/src/content/docs/es/middleware/method-override.md b/site/src/content/docs/es/middleware/method-override.mdx similarity index 77% rename from site/src/content/docs/es/middleware/method-override.md rename to site/src/content/docs/es/middleware/method-override.mdx index aac8c37c..2bf06393 100644 --- a/site/src/content/docs/es/middleware/method-override.md +++ b/site/src/content/docs/es/middleware/method-override.mdx @@ -5,6 +5,8 @@ sidebar: order: 13 --- +import ConfigReference from '../../../../components/ConfigReference.astro'; + El middleware Method Override lee el método sobrescrito desde el request y lo usa en lugar del método original. @@ -37,16 +39,7 @@ El método puede obtenerse con `MethodFromHeader`, `MethodFromForm` o `MethodFro ## Configuración -```go -type MethodOverrideConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Getter is a function that gets the overridden method from the request. - // Optional. Default value MethodFromHeader(echo.HeaderXHTTPMethodOverride). - Getter MethodOverrideGetter -} -``` + ### Configuración por defecto diff --git a/site/src/content/docs/es/middleware/open-telemetry.md b/site/src/content/docs/es/middleware/open-telemetry.mdx similarity index 92% rename from site/src/content/docs/es/middleware/open-telemetry.md rename to site/src/content/docs/es/middleware/open-telemetry.mdx index 4981e981..10572870 100644 --- a/site/src/content/docs/es/middleware/open-telemetry.md +++ b/site/src/content/docs/es/middleware/open-telemetry.mdx @@ -5,6 +5,8 @@ sidebar: order: 14 --- +import ConfigReference from '../../../../components/ConfigReference.astro'; + [Echo OpenTelemetry](https://github.com/labstack/echo-opentelemetry) es un middleware que proporciona instrumentación OpenTelemetry para requests HTTP. @@ -80,3 +82,7 @@ tracer, err := echo.ContextGet[trace.Tracer](c, echootel.TracerKey) El [ejemplo](https://github.com/labstack/echo-opentelemetry/blob/main/example/main.go) exporta metrics y spans a stdout, pero puedes usar cualquier exporter (OTLP, etc.). Consulta la documentación de [OpenTelemetry exporters](https://opentelemetry.io/docs/languages/go/exporters). + +## Referencia de la API + + diff --git a/site/src/content/docs/es/middleware/prometheus.md b/site/src/content/docs/es/middleware/prometheus.mdx similarity index 96% rename from site/src/content/docs/es/middleware/prometheus.md rename to site/src/content/docs/es/middleware/prometheus.mdx index 88398857..1f40da1f 100644 --- a/site/src/content/docs/es/middleware/prometheus.md +++ b/site/src/content/docs/es/middleware/prometheus.mdx @@ -5,6 +5,8 @@ sidebar: order: 15 --- +import ConfigReference from '../../../../components/ConfigReference.astro'; + El middleware [Echo Prometheus](https://github.com/labstack/echo-prometheus) genera metrics Prometheus para requests HTTP. @@ -282,3 +284,11 @@ func main() { } } ``` + +## Referencia de la API + + + + + + diff --git a/site/src/content/docs/es/middleware/proxy.md b/site/src/content/docs/es/middleware/proxy.md deleted file mode 100644 index 44ec44b8..00000000 --- a/site/src/content/docs/es/middleware/proxy.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: Proxy -description: Middleware de reverse proxy HTTP y WebSocket con load balancing. -sidebar: - order: 16 ---- - -Proxy proporciona un middleware de reverse proxy HTTP/WebSocket. Reenvía un request a un -servidor upstream usando una técnica de load balancing configurada. - -Todo el middleware principal reside en el paquete `middleware`: - -```go -import "github.com/labstack/echo/v5/middleware" -``` - -## Uso - -```go -url1, err := url.Parse("http://localhost:8081") -if err != nil { - e.Logger.Error("failed to parse url", "error", err) -} -url2, err := url.Parse("http://localhost:8082") -if err != nil { - e.Logger.Error("failed to parse url", "error", err) -} -e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer([]*middleware.ProxyTarget{ - { - URL: url1, - }, - { - URL: url2, - }, -}))) -``` - -## Configuración personalizada - -```go -e := echo.New() -e.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{})) -``` - -## Configuración - -```go -type ProxyConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Balancer defines a load balancing technique. - // Required. - Balancer ProxyBalancer - - // RetryCount defines the number of times a failed proxied request should be retried - // using the next available ProxyTarget. Defaults to 0, meaning requests are never retried. - RetryCount int - - // RetryFilter defines a function used to determine if a failed request to a - // ProxyTarget should be retried. The RetryFilter will only be called when the number - // of previous retries is less than RetryCount. If the function returns true, the - // request will be retried. The provided error indicates the reason for the request - // failure. When the ProxyTarget is unavailable, the error will be an instance of - // echo.HTTPError with a code of http.StatusBadGateway. In all other cases, the error - // will indicate an internal error in the Proxy middleware. When a RetryFilter is not - // specified, all requests that fail with http.StatusBadGateway will be retried. A custom - // RetryFilter can be provided to only retry specific requests. Note that RetryFilter is - // only called when the request to the target fails, or an internal error in the Proxy - // middleware has occurred. Successful requests that return a non-200 response code cannot - // be retried. - RetryFilter func(c *echo.Context, e error) bool - - // ErrorHandler defines a function which can be used to return custom errors from - // the Proxy middleware. ErrorHandler is only invoked when there has been - // either an internal error in the Proxy middleware or the ProxyTarget is - // unavailable. Due to the way requests are proxied, ErrorHandler is not invoked - // when a ProxyTarget returns a non-200 response. In these cases, the response - // is already written so errors cannot be modified. ErrorHandler is only - // invoked after all retry attempts have been exhausted. - ErrorHandler func(c *echo.Context, err error) error - - // Rewrite defines URL path rewrite rules. The values captured in asterisk can be - // retrieved by index e.g. $1, $2 and so on. - // Examples: - // "/old": "/new", - // "/api/*": "/$1", - // "/js/*": "/public/javascripts/$1", - // "/users/*/orders/*": "/user/$1/order/$2", - Rewrite map[string]string - - // RegexRewrite defines rewrite rules using regexp.Regexp with captures. - // Every capture group in the values can be retrieved by index e.g. $1, $2 and so on. - // Example: - // "^/old/[0.9]+/": "/new", - // "^/api/.+?/(.*)": "/v2/$1", - RegexRewrite map[*regexp.Regexp]string - - // Context key to store selected ProxyTarget into context. - // Optional. Default value "target". - ContextKey string - - // To customize the transport to remote. - // Examples: If custom TLS certificates are required. - Transport http.RoundTripper - - // ModifyResponse defines function to modify response from ProxyTarget. - ModifyResponse func(*http.Response) error -} -``` - -### Configuración por defecto - -| Nombre | Valor | -| ---------- | -------------- | -| Skipper | DefaultSkipper | -| ContextKey | `target` | - -### Reglas basadas en regex - -Para rewriting avanzado de requests proxied, también se pueden definir reglas usando -expresiones regulares. Los grupos de captura normales se pueden definir con `()` y referenciar -por índice (`$1`, `$2`, ...) en el path reescrito. - -Las reglas `RegexRewrite` y `Rewrite` normales se pueden combinar. - -```go -e.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{ - Balancer: rrb, - Rewrite: map[string]string{ - "^/v1/*": "/v2/$1", - }, - RegexRewrite: map[*regexp.Regexp]string{ - regexp.MustCompile("^/foo/([0-9].*)"): "/num/$1", - regexp.MustCompile("^/bar/(.+?)/(.*)"): "/baz/$2/$1", - }, -})) -``` - -Consulta el recetario de [reverse proxy](/es/cookbook/reverse-proxy/) para ver un ejemplo completo. diff --git a/site/src/content/docs/es/middleware/proxy.mdx b/site/src/content/docs/es/middleware/proxy.mdx new file mode 100644 index 00000000..3fd867cd --- /dev/null +++ b/site/src/content/docs/es/middleware/proxy.mdx @@ -0,0 +1,79 @@ +--- +title: Proxy +description: Middleware de reverse proxy HTTP y WebSocket con load balancing. +sidebar: + order: 16 +--- + +import ConfigReference from '../../../../components/ConfigReference.astro'; + +Proxy proporciona un middleware de reverse proxy HTTP/WebSocket. Reenvía un request a un +servidor upstream usando una técnica de load balancing configurada. + +Todo el middleware principal reside en el paquete `middleware`: + +```go +import "github.com/labstack/echo/v5/middleware" +``` + +## Uso + +```go +url1, err := url.Parse("http://localhost:8081") +if err != nil { + e.Logger.Error("failed to parse url", "error", err) +} +url2, err := url.Parse("http://localhost:8082") +if err != nil { + e.Logger.Error("failed to parse url", "error", err) +} +e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer([]*middleware.ProxyTarget{ + { + URL: url1, + }, + { + URL: url2, + }, +}))) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{})) +``` + +## Configuración + + + +### Configuración por defecto + +| Nombre | Valor | +| ---------- | -------------- | +| Skipper | DefaultSkipper | +| ContextKey | `target` | + +### Reglas basadas en regex + +Para rewriting avanzado de requests proxied, también se pueden definir reglas usando +expresiones regulares. Los grupos de captura normales se pueden definir con `()` y referenciar +por índice (`$1`, `$2`, ...) en el path reescrito. + +Las reglas `RegexRewrite` y `Rewrite` normales se pueden combinar. + +```go +e.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{ + Balancer: rrb, + Rewrite: map[string]string{ + "^/v1/*": "/v2/$1", + }, + RegexRewrite: map[*regexp.Regexp]string{ + regexp.MustCompile("^/foo/([0-9].*)"): "/num/$1", + regexp.MustCompile("^/bar/(.+?)/(.*)"): "/baz/$2/$1", + }, +})) +``` + +Consulta el recetario de [reverse proxy](/es/cookbook/reverse-proxy/) para ver un ejemplo completo. diff --git a/site/src/content/docs/es/middleware/rate-limiter.md b/site/src/content/docs/es/middleware/rate-limiter.mdx similarity index 83% rename from site/src/content/docs/es/middleware/rate-limiter.md rename to site/src/content/docs/es/middleware/rate-limiter.mdx index f815d41b..5c0dfd31 100644 --- a/site/src/content/docs/es/middleware/rate-limiter.md +++ b/site/src/content/docs/es/middleware/rate-limiter.mdx @@ -5,6 +5,8 @@ sidebar: order: 17 --- +import ConfigReference from '../../../../components/ConfigReference.astro'; + `RateLimiter` proporciona un middleware de rate limiter que limita el número de requests enviados al servidor desde una IP o identificador particular dentro de un periodo. @@ -72,20 +74,9 @@ Para implementar tu propio store, satisface la interfaz `RateLimiterStore` y pá ## Configuración -```go -type RateLimiterConfig struct { - Skipper Skipper - BeforeFunc BeforeFunc - // IdentifierExtractor uses echo.Context to extract the identifier for a visitor. - IdentifierExtractor Extractor - // Store defines a store for the rate limiter. - Store RateLimiterStore - // ErrorHandler provides a handler to be called when IdentifierExtractor returns a non-nil error. - ErrorHandler func(c *echo.Context, err error) error - // DenyHandler provides a handler to be called when RateLimiter denies access. - DenyHandler func(c *echo.Context, identifier string, err error) error -} -``` + + + ### Configuración por defecto diff --git a/site/src/content/docs/es/middleware/recover.md b/site/src/content/docs/es/middleware/recover.mdx similarity index 67% rename from site/src/content/docs/es/middleware/recover.md rename to site/src/content/docs/es/middleware/recover.mdx index fee2951c..29c53d89 100644 --- a/site/src/content/docs/es/middleware/recover.md +++ b/site/src/content/docs/es/middleware/recover.mdx @@ -5,6 +5,8 @@ sidebar: order: 18 --- +import ConfigReference from '../../../../components/ConfigReference.astro'; + El middleware Recover se recupera de panics en cualquier punto de la cadena, imprime el stack trace y pasa el control al [HTTPErrorHandler](/es/guide/customization/#http-error-handler) centralizado. @@ -35,25 +37,7 @@ El ejemplo anterior usa un `StackSize` de 1 KB y valores por defecto para `Disab ## Configuración -```go -type RecoverConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Size of the stack to be printed. - // Optional. Default value 4KB. - StackSize int - - // DisableStackAll disables formatting stack traces of all other goroutines - // into the buffer after the trace for the current goroutine. - // Optional. Default value false. - DisableStackAll bool - - // DisablePrintStack disables printing the stack trace. - // Optional. Default value false. - DisablePrintStack bool -} -``` + ### Configuración por defecto diff --git a/site/src/content/docs/es/middleware/redirect.md b/site/src/content/docs/es/middleware/redirect.mdx similarity index 88% rename from site/src/content/docs/es/middleware/redirect.md rename to site/src/content/docs/es/middleware/redirect.mdx index df641fc0..daab0e66 100644 --- a/site/src/content/docs/es/middleware/redirect.md +++ b/site/src/content/docs/es/middleware/redirect.mdx @@ -5,6 +5,8 @@ sidebar: order: 19 --- +import ConfigReference from '../../../../components/ConfigReference.astro'; + Todo el middleware principal reside en el paquete `middleware`: ```go @@ -85,16 +87,7 @@ El ejemplo anterior redirige requests HTTP a HTTPS con el código de estado ## Configuración -```go -type RedirectConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Status code to be used when redirecting the request. - // Optional. Default value http.StatusMovedPermanently. - Code int -} -``` + ### Configuración por defecto diff --git a/site/src/content/docs/es/middleware/request-id.md b/site/src/content/docs/es/middleware/request-id.mdx similarity index 75% rename from site/src/content/docs/es/middleware/request-id.md rename to site/src/content/docs/es/middleware/request-id.mdx index 64b41732..345a52f4 100644 --- a/site/src/content/docs/es/middleware/request-id.md +++ b/site/src/content/docs/es/middleware/request-id.mdx @@ -5,6 +5,8 @@ sidebar: order: 20 --- +import ConfigReference from '../../../../components/ConfigReference.astro'; + El middleware Request ID genera un ID único para un request. Todo el middleware principal reside en el paquete `middleware`: @@ -49,23 +51,7 @@ e.Use(middleware.RequestIDWithConfig(middleware.RequestIDConfig{ ## Configuración -```go -type RequestIDConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Generator defines a function to generate an ID. - // Optional. Default value random.String(32). - Generator func() string - - // RequestIDHandler defines a function which is executed for a request id. - RequestIDHandler func(c *echo.Context, requestID string) - - // TargetHeader defines what header to look for to populate the id. - // Optional. Default value is `X-Request-Id`. - TargetHeader string -} -``` + ### Configuración por defecto diff --git a/site/src/content/docs/es/middleware/rewrite.md b/site/src/content/docs/es/middleware/rewrite.mdx similarity index 71% rename from site/src/content/docs/es/middleware/rewrite.md rename to site/src/content/docs/es/middleware/rewrite.mdx index dc3d86b4..61d79b5e 100644 --- a/site/src/content/docs/es/middleware/rewrite.md +++ b/site/src/content/docs/es/middleware/rewrite.mdx @@ -5,6 +5,8 @@ sidebar: order: 21 --- +import ConfigReference from '../../../../components/ConfigReference.astro'; + El middleware Rewrite reescribe el path de la URL según las reglas proporcionadas. Es útil para compatibilidad hacia atrás o para crear enlaces más limpios y descriptivos. @@ -42,29 +44,7 @@ e.Pre(middleware.RewriteWithConfig(middleware.RewriteConfig{})) ## Configuración -```go -type RewriteConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // Rules defines the URL path rewrite rules. The values captured in asterisk can be - // retrieved by index e.g. $1, $2 and so on. - // Example: - // "/old": "/new", - // "/api/*": "/$1", - // "/js/*": "/public/javascripts/$1", - // "/users/*/orders/*": "/user/$1/order/$2", - // Required. - Rules map[string]string - - // RegexRules defines the URL path rewrite rules using regexp.Regexp with captures. - // Every capture group in the values can be retrieved by index e.g. $1, $2 and so on. - // Example: - // "^/old/[0.9]+/": "/new", - // "^/api/.+?/(.*)": "/v2/$1", - RegexRules map[*regexp.Regexp]string -} -``` + Configuración por defecto: diff --git a/site/src/content/docs/es/middleware/secure.md b/site/src/content/docs/es/middleware/secure.md deleted file mode 100644 index 53a1a1b8..00000000 --- a/site/src/content/docs/es/middleware/secure.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -title: Secure -description: Protege contra XSS, content sniffing, clickjacking y otros ataques de inyección. -sidebar: - order: 22 ---- - -El middleware Secure proporciona protección contra cross-site scripting (XSS), content type -sniffing, clickjacking, conexiones inseguras y otros ataques de inyección de código. - -Todo el middleware principal reside en el paquete `middleware`: - -```go -import "github.com/labstack/echo/v5/middleware" -``` - -## Uso - -```go -e.Use(middleware.Secure()) -``` - -## Configuración personalizada - -```go -e := echo.New() -e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ - XSSProtection: "", - ContentTypeNosniff: "", - XFrameOptions: "", - HSTSMaxAge: 3600, - ContentSecurityPolicy: "default-src 'self'", -})) -``` - -:::note -Pasar un `XSSProtection`, `ContentTypeNosniff`, `XFrameOptions` o -`ContentSecurityPolicy` vacío deshabilita esa protección. -::: - -## Configuración - -```go -type SecureConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // XSSProtection provides protection against cross-site scripting attack (XSS) - // by setting the `X-XSS-Protection` header. - // Optional. Default value "1; mode=block". - XSSProtection string - - // ContentTypeNosniff provides protection against overriding Content-Type - // header by setting the `X-Content-Type-Options` header. - // Optional. Default value "nosniff". - ContentTypeNosniff string - - // XFrameOptions can be used to indicate whether or not a browser should - // be allowed to render a page in a ,