slug

package module
v2.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 8, 2026 License: MIT Imports: 6 Imported by: 0

README

Go Report Card License License Stay with Ukraine

slug

slug generates URL-friendly slugs from Unicode strings in any language. It offers simple package-level functions and a configurable, immutable Slug type built from functional options.

Its guiding rule is that word boundaries are never silently lost: a punctuation mark separates words instead of gluing them together, and a handful of meaningful symbols become their own words (@at, &and). The result stays readable, not a run-together blob.

Features

  • Clean, URL-safe slug generation from any Unicode text.
  • Multi-language transliteration.
  • Punctuation and symbols handled predictably (never silently merges words).
  • Configurable separator, maximum length and empty-input fallback.
  • Uniqueness helper and slug validation.
  • Immutable and safe for concurrent use.

Installation

go get github.com/goloop/slug/v2
import "github.com/goloop/slug/v2"

Requires Go 1.24 or newer.

Quick start

package main

import (
    "fmt"

    "github.com/goloop/slug/v2"
)

func main() {
    fmt.Println(slug.Make("Hello World"))  // Hello-World
    fmt.Println(slug.Lower("Hello World")) // hello-world
    fmt.Println(slug.Upper("Hello World")) // HELLO-WORLD

    // Punctuation separates words; it is never dropped silently.
    fmt.Println(slug.Make("co-operate"))     // co-operate
    fmt.Println(slug.Make("email@site.com")) // email-at-site-com
    fmt.Println(slug.Make("R&D 100%"))       // R-and-D-100-pct
}

Build a configured, immutable maker for full control:

import "github.com/goloop/slug/v2/lang"

s := slug.New(
    slug.WithLang(lang.UK),    // regional transliteration
    slug.WithSeparator("_"),   // custom word separator (default "-")
    slug.WithMaxLength(60),    // cut on a word boundary
    slug.WithFallback("post"), // value for an empty result
)

s.Make("Привіт, світ!") // Pryvit_svit

Documentation

Contributing

Contributions are welcome. Please run go test ./..., go vet ./... and gofmt -l . before submitting a pull request.

License

slug is released under the MIT License. See LICENSE.

Documentation

Overview

Package slug generates URL-friendly slugs from Unicode strings in any language.

A slug is built in three steps: the input is transliterated to ASCII, split into words on every character that is not a letter or a digit, and the words are joined with a separator ("-" by default). Leading, trailing and repeated separators are removed, so the result contains only letters, digits and single separators.

Package-level helpers

The package functions use a default configuration (neutral transliteration, "-" separator, original letter case preserved):

slug.Make("Hello World")  // "Hello-World"
slug.Lower("Hello World") // "hello-world"
slug.Upper("Hello World") // "HELLO-WORLD"

Configurable slugs

New builds a Slug from functional options for full control:

s := slug.New(
	slug.WithLang(lang.UK),   // regional transliteration
	slug.WithSeparator("_"),  // custom separator
	slug.WithMaxLength(60),   // cut on a word boundary
	slug.WithFallback("post"),// value for an empty result
)
s.Make("Привіт, світ!") // "Pryvit_svit"

A few characters carry meaning and become words: "@" -> "at", "&" -> "and", "#" -> "sharp", "%" -> "pct" (e.g. "r&d" -> "r-and-d"). A true apostrophe between letters is dropped so the letters join ("it's" -> "its").

Concurrency

A Slug is immutable after New, so a single value may be shared and used by multiple goroutines concurrently; the package-level helpers are safe for concurrent use as well.

Index

Examples

Constants

View Source
const DefaultSeparator = "-"

DefaultSeparator is the word separator used when no other is configured.

Variables

This section is empty.

Functions

func IsValid

func IsValid(t string) bool

IsValid reports whether t is already a canonical slug under the default configuration (see (*Slug).IsValid).

func Lower

func Lower(t string) string

Lower is Make with the result lowercased.

Example
package main

import (
	"fmt"

	"github.com/goloop/slug/v2"
)

func main() {
	fmt.Println(slug.Lower("Hello World"))
}
Output:
hello-world

func Make

func Make(t string) string

Make returns the slug of t using the default configuration (neutral transliteration, "-" separator, original letter case preserved). It is shorthand for New().Make(t).

Example
package main

import (
	"fmt"

	"github.com/goloop/slug/v2"
)

func main() {
	fmt.Println(slug.Make("Hello World"))
}
Output:
Hello-World
Example (Hyphen)

A hyphen in the input is preserved as a separator, not dropped.

package main

import (
	"fmt"

	"github.com/goloop/slug/v2"
)

func main() {
	fmt.Println(slug.Make("co-operate with e-mail"))
}
Output:
co-operate-with-e-mail
Example (Symbols)

Meaningful symbols become words and punctuation becomes a separator.

package main

import (
	"fmt"

	"github.com/goloop/slug/v2"
)

func main() {
	fmt.Println(slug.Make("AT&T: 100% ready"))
}
Output:
AT-and-T-100-pct-ready

func MakeUnique

func MakeUnique(t string, exists func(string) bool) string

MakeUnique returns a slug of t made unique via exists (see (*Slug).MakeUnique), using the default configuration.

func TryMakeUnique added in v2.1.0

func TryMakeUnique(t string, exists func(string) bool, maxTries int) (string, bool)

TryMakeUnique returns a unique slug of t and reports success (see (*Slug).TryMakeUnique), using the default configuration.

func Upper

func Upper(t string) string

Upper is Make with the result uppercased.

Example
package main

import (
	"fmt"

	"github.com/goloop/slug/v2"
)

func main() {
	fmt.Println(slug.Upper("Hello World"))
}
Output:
HELLO-WORLD

Types

type Option

type Option func(*config)

An Option configures a Slug in New.

func WithFallback

func WithFallback(s string) Option

WithFallback sets the value returned when the input produces an empty slug (e.g. Make("!!!")). Without it such input yields an empty string.

The fallback is itself normalized through the slug pipeline at construction time, so it always obeys the canonical-slug and MaxLength invariants. A fallback that normalizes to nothing (for example "!!!") is treated as no fallback.

Example
package main

import (
	"fmt"

	"github.com/goloop/slug/v2"
)

func main() {
	s := slug.New(slug.WithFallback("post"))
	fmt.Println(s.Make("!!!"))
}
Output:
post

func WithLang

func WithLang(code string) Option

WithLang sets the transliteration language code. Use the constants of the lang subpackage, e.g. WithLang(lang.UK). An unknown code falls back to language-neutral transliteration (lang.None).

func WithMaxLength

func WithMaxLength(n int) Option

WithMaxLength limits the slug to at most n bytes, cutting on a word boundary so a word is never split. Values <= 0 are ignored (unlimited).

Example
package main

import (
	"fmt"

	"github.com/goloop/slug/v2"
)

func main() {
	s := slug.New(slug.WithMaxLength(11))
	fmt.Println(s.Make("hello world foo bar"))
}
Output:
hello-world

func WithSeparator

func WithSeparator(sep string) Option

WithSeparator sets the word separator (default "-"). An empty separator concatenates the words without any delimiter.

The separator is not validated. For predictable, URL-safe slugs it should contain only URL-safe, non-alphanumeric characters (for example "-" or "_"). A separator that contains letters or digits merges into the slug body and breaks IsValid; unsafe characters (spaces, ".", "/", "%", ...) produce slugs that are not URL-safe.

Example
package main

import (
	"fmt"

	"github.com/goloop/slug/v2"
)

func main() {
	s := slug.New(slug.WithSeparator("_"))
	fmt.Println(s.Make("Hello World"))
}
Output:
Hello_World

type Slug

type Slug struct {
	// contains filtered or unexported fields
}

Slug generates URL-friendly slugs according to its configuration.

A Slug is created with New and is immutable afterwards: all settings are resolved from the options at construction time. Because nothing is mutated during Make/Lower/Upper/IsValid/MakeUnique, a single *Slug is safe for concurrent use by multiple goroutines.

func New

func New(opts ...Option) *Slug

New returns a Slug configured by the given options. With no options it uses language-neutral transliteration, "-" as the separator, no length limit and an empty fallback.

s := slug.New(
	slug.WithLang(lang.UK),
	slug.WithMaxLength(60),
	slug.WithFallback("post"),
)
s.Make("Привіт, світ!") // "Pryvit-svit"
Example
package main

import (
	"fmt"

	"github.com/goloop/slug/v2"
	"github.com/goloop/slug/v2/lang"
)

func main() {
	s := slug.New(slug.WithLang(lang.UK))
	fmt.Println(s.Make("Привіт, світ!"))
}
Output:
Pryvit-svit

func (*Slug) IsValid

func (s *Slug) IsValid(t string) bool

IsValid reports whether t is already a canonical slug for this configuration: non-empty and left unchanged by Make. This means it contains only letters, digits and single separators, without leading or trailing separators, and (if a limit is set) is within MaxLength.

IsValid checks that t is itself canonical, not that t would produce a valid slug. In particular, with WithFallback an input that maps to the fallback is not canonical: IsValid("!!!") is false even though Make("!!!") returns the (valid) fallback.

func (*Slug) Lower

func (s *Slug) Lower(t string) string

Lower is Make with the result lowercased.

func (*Slug) Make

func (s *Slug) Make(t string) string

Make returns the slug of t. The letter case of the input is preserved; use Lower or Upper to force a case.

The input is transliterated to ASCII and then split into words on any character that is not a letter or a digit; the words are joined with the configured separator. Leading, trailing and repeated separators are removed. If the result is empty the configured fallback is returned (empty by default).

func (*Slug) MakeUnique

func (s *Slug) MakeUnique(t string, exists func(string) bool) string

MakeUnique returns a slug of t that is unique according to exists: if the base slug is already taken it appends the separator and an incrementing number ("-2", "-3", ...) until exists reports the candidate as free. When MaxLength is set the base is shortened on a word boundary to keep every candidate within the limit. A nil exists function disables the check.

MakeUnique is best-effort: it gives up after a bounded number of attempts (so a predicate that always reports "taken" cannot hang) and returns the base slug, which may then collide. Use TryMakeUnique when you need to detect that no unique slug could be produced.

Example
package main

import (
	"fmt"

	"github.com/goloop/slug/v2"
)

func main() {
	taken := map[string]bool{"hello-world": true, "hello-world-2": true}
	s := slug.New()
	fmt.Println(s.MakeUnique("hello world", func(x string) bool {
		return taken[x]
	}))
}
Output:
hello-world-3

func (*Slug) TryMakeUnique added in v2.1.0

func (s *Slug) TryMakeUnique(t string, exists func(string) bool, maxTries int) (string, bool)

TryMakeUnique returns a unique slug of t and reports whether it succeeded. It tests the base slug, then base+separator+2, base+separator+3, ... with exists (which must report whether a slug is already taken), trying at most maxTries candidates. Every candidate satisfies MaxLength; if the limit is so small that no numbered candidate fits, the search stops early. A non-positive maxTries uses a default bound; a nil exists succeeds with the base slug.

It returns (slug, true) on the first free candidate, or ("", false) if none is free within the attempts or no length-valid candidate can be formed.

Example
package main

import (
	"fmt"

	"github.com/goloop/slug/v2"
)

func main() {
	taken := map[string]bool{"post": true, "post-2": true}
	s := slug.New()
	if got, ok := s.TryMakeUnique("post", func(x string) bool { return taken[x] }, 100); ok {
		fmt.Println(got)
	}
}
Output:
post-3

func (*Slug) Upper

func (s *Slug) Upper(t string) string

Upper is Make with the result uppercased.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL