diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Sridhar Ratnakumar
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# url-slug
+
diff --git a/src/Network/URI/Slug.hs b/src/Network/URI/Slug.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/URI/Slug.hs
@@ -0,0 +1,43 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE InstanceSigs #-}
+
+module Network.URI.Slug
+  ( Slug (unSlug),
+    decodeSlug,
+    encodeSlug,
+  )
+where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Data (Data)
+import Data.Text qualified as T
+import Data.Text.Normalize qualified as UT
+import Network.URI.Encode qualified as UE
+
+-- | An URL path is made of multiple slugs, separated by '/'
+newtype Slug = Slug {unSlug :: Text}
+  deriving stock (Eq, Show, Ord, Data, Generic)
+  deriving anyclass (ToJSON, FromJSON)
+
+-- | Decode an URL component into a `Slug` using `Network.URI.Encode`, as well
+-- as unicode normalize it.
+decodeSlug :: Text -> Slug
+decodeSlug =
+  fromString . UE.decode . toString
+
+-- | Encode a `Slug` into an URL component using `Network.URI.Encode`
+encodeSlug :: Slug -> Text
+encodeSlug =
+  UE.encodeText . unSlug
+
+instance IsString Slug where
+  fromString :: HasCallStack => String -> Slug
+  fromString (toText -> s) =
+    if "/" `T.isInfixOf` s
+      then error ("Slug cannot contain a slash: " <> s)
+      else Slug (unicodeNormalize s)
+
+-- Normalize varying non-ascii strings (in filepaths / slugs) to one
+-- representation, so that they can be reliably linked to.
+unicodeNormalize :: Text -> Text
+unicodeNormalize = UT.normalize UT.NFC . toText
diff --git a/url-slug.cabal b/url-slug.cabal
new file mode 100644
--- /dev/null
+++ b/url-slug.cabal
@@ -0,0 +1,86 @@
+cabal-version:      2.4
+name:               url-slug
+version:            0.1.0.0
+license:            MIT
+copyright:          2022 Sridhar Ratnakumar
+maintainer:         srid@srid.ca
+author:             Sridhar Ratnakumar
+category:           Network,Web
+
+-- TODO: Before hackage release.
+-- A short (one-line) description of the package.
+synopsis: Encoding and decoding of URL slugs
+
+-- A longer description of the package.
+description: Encoding and decoding of URL slugs
+
+-- A URL where users can report bugs.
+bug-reports: https://github.com/srid/url-slug
+
+extra-source-files:
+  LICENSE
+  README.md
+
+library
+  build-depends:
+    , aeson
+    , base                >=4.13.0.0 && <=4.18.0.0
+    , relude
+    , text
+    , unicode-transforms
+    , uri-encode
+
+  mixins:
+    base hiding (Prelude),
+    relude (Relude as Prelude, Relude.Container.One),
+    relude
+
+  ghc-options:
+    -Wall -Wincomplete-record-updates -Wincomplete-uni-patterns
+    -Wmissing-deriving-strategies -Wunused-foralls -Wunused-foralls
+    -fprint-explicit-foralls -fprint-explicit-kinds
+
+  default-extensions:
+    NoStarIsType
+    BangPatterns
+    ConstraintKinds
+    DataKinds
+    DeriveDataTypeable
+    DeriveFoldable
+    DeriveFunctor
+    DeriveGeneric
+    DeriveLift
+    DeriveTraversable
+    DerivingStrategies
+    DerivingVia
+    EmptyCase
+    EmptyDataDecls
+    EmptyDataDeriving
+    ExistentialQuantification
+    ExplicitForAll
+    FlexibleContexts
+    FlexibleInstances
+    GADTSyntax
+    GeneralisedNewtypeDeriving
+    ImportQualifiedPost
+    KindSignatures
+    LambdaCase
+    MultiParamTypeClasses
+    MultiWayIf
+    NumericUnderscores
+    OverloadedStrings
+    PolyKinds
+    PostfixOperators
+    RankNTypes
+    ScopedTypeVariables
+    StandaloneDeriving
+    StandaloneKindSignatures
+    TupleSections
+    TypeApplications
+    TypeFamilies
+    TypeOperators
+    ViewPatterns
+
+  exposed-modules:    Network.URI.Slug
+  hs-source-dirs:     src
+  default-language:   Haskell2010
