diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,50 @@
 # Changelog
 
+## 0.15.0.0 (2025-04-05)
+
+ *  Add [OSC8] support for hyperlinks (#185).  This makes hyperlinks clickable
+    in many terminal emulators (see [OSC8 adoption]).
+
+    There is currently no way to detect if a terminal supports this feature,
+    so for now this needs to be explicitly turned on in the configuration:
+
+    ```yaml
+    patat:
+      links:
+        osc8: true
+    ```
+
+ *  Enable `shortcut_reference_links` by default.  These are reference links
+    without the second pair of brackets, e.g.:
+
+    ```markdown
+    See [my website].
+
+    [my website]: http://example.com
+    ```
+
+ *  Validate settings on slide (#186).
+
+    This validation was present since the introduction of slide settings, but it
+    was accidentally removed in v0.14.
+
+ *  Improve reference link rendering.
+
+    This changes the way references are rendered from:
+
+        [example](http://example.com/)
+        [example with title](http://example.com/ "title")
+
+    To:
+
+        [example]: http://example.com/
+        [example with title]: http://example.com/ title
+
+    This easier to read and more coherent with Pandoc markdown.
+
+[OSC8]: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
+[OSC8 adoption]: https://github.com/Alhadis/OSC8-Adoption
+
 ## 0.14.2.0 (2024-03-10)
 
  *  Fix WezTerm image rendering when using panes (#182).
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -56,6 +56,7 @@
     -   [Evaluating code](#evaluating-code)
     -   [Speaker notes](#speaker-notes)
     -   [Transitions](#transitions)
+    -   [Links](#links)
 -   [Trivia](#trivia)
 
 Installation
@@ -884,6 +885,23 @@
 You can optionally set `items` to a non-empty list of transition effects to
 randomly sample from.  If `items` is not set, `patat` will simply sample from
 all transition effects using their respective default settings.
+
+### Links
+
+Version 0.15.0.0 and later support [OSC8] for hyperlinks.  This makes hyperlinks
+clickable in many terminal emulators (see [OSC8 adoption]).
+
+There is currently no way to detect if a terminal supports this feature, so you
+need to explicitly turn this on in the configuration:
+
+```yaml
+patat:
+  links:
+    osc8: true
+```
+
+[OSC8]: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
+[OSC8 adoption]: https://github.com/Alhadis/OSC8-Adoption
 
 Trivia
 ------
diff --git a/lib/Patat/Presentation/Display.hs b/lib/Patat/Presentation/Display.hs
--- a/lib/Patat/Presentation/Display.hs
+++ b/lib/Patat/Presentation/Display.hs
@@ -67,6 +67,7 @@
         , dsMargins       = margins settings
         , dsWrap          = fromMaybe NoWrap $ psWrap settings
         , dsTabStop       = maybe 4 A.unFlexibleNum $ psTabStop settings
+        , dsOSC8          = fromMaybe False (psLinks settings >>= lsOSC8)
         , dsTheme         = fromMaybe Theme.defaultTheme (psTheme settings)
         , dsSyntaxMap     = pSyntaxMap
         , dsResolve       = \var -> fromMaybe [] $ HMS.lookup var pVars
@@ -410,9 +411,10 @@
 
 prettyInline ds link@(Link _attrs _text (target, _title))
     | Just (text, _, _) <- toReferenceLink link =
-        "[" <> themed ds themeLinkText (prettyInlines ds text) <> "]"
+        let doc = Just $ prettyInlines ds text in
+        "[" <> themed ds themeLinkText (hyperlink ds target doc) <> "]"
     | otherwise =
-        "<" <> themed ds themeLinkTarget (PP.text target) <> ">"
+        "<" <> themed ds themeLinkTarget (hyperlink ds target Nothing) <> ">"
 
 prettyInline _ds SoftBreak = PP.softline
 
@@ -469,12 +471,11 @@
         "[" <>
         themed ds themeLinkText
             (prettyInlines ds $ newlineToSpace text) <>
-        "](" <>
+        "]: " <>
         themed ds themeLinkTarget (PP.text target) <>
         (if T.null title
             then mempty
-            else PP.space <> "\"" <> PP.text title <> "\"")
-        <> ")"
+            else PP.space <> PP.text title)
 
     newlineToSpace :: [Inline] -> [Inline]
     newlineToSpace = runIdentity . dftInlines (pure . pure) work
@@ -490,3 +491,13 @@
 toReferenceLink (Link _attrs text (target, title))
     | [Str target] /= text = Just (text, target, title)
 toReferenceLink _ = Nothing
+
+
+--------------------------------------------------------------------------------
+hyperlink :: DisplaySettings -> T.Text -> Maybe PP.Doc -> PP.Doc
+hyperlink ds url Nothing
+    | dsOSC8 ds = PP.hyperlink (T.unpack url) (PP.text url)
+    | otherwise = PP.text url
+hyperlink ds url (Just doc)
+    | dsOSC8 ds = PP.hyperlink (T.unpack url) doc
+    | otherwise = doc
diff --git a/lib/Patat/Presentation/Display/Internal.hs b/lib/Patat/Presentation/Display/Internal.hs
--- a/lib/Patat/Presentation/Display/Internal.hs
+++ b/lib/Patat/Presentation/Display/Internal.hs
@@ -21,6 +21,7 @@
     , dsWrap        :: !Wrap
     , dsTabStop     :: !Int
     , dsMargins     :: !Margins
+    , dsOSC8        :: !Bool
     , dsTheme       :: !Theme.Theme
     , dsSyntaxMap   :: !Skylighting.SyntaxMap
     , dsResolve     :: !(Var -> [Block])
diff --git a/lib/Patat/Presentation/Settings.hs b/lib/Patat/Presentation/Settings.hs
--- a/lib/Patat/Presentation/Settings.hs
+++ b/lib/Patat/Presentation/Settings.hs
@@ -24,6 +24,8 @@
 
     , TransitionSettings (..)
 
+    , LinkSettings (..)
+
     , parseSlideSettings
     ) where
 
@@ -65,6 +67,7 @@
     , psSyntaxDefinitions :: !(Maybe [FilePath])
     , psSpeakerNotes      :: !(Maybe SpeakerNotesSettings)
     , psTransition        :: !(Maybe TransitionSettings)
+    , psLinks             :: !(Maybe LinkSettings)
     } deriving (Eq, Show)
 
 
@@ -88,6 +91,7 @@
         , psSyntaxDefinitions = on (<>)  psSyntaxDefinitions l r
         , psSpeakerNotes      = on mplus psSpeakerNotes      l r
         , psTransition        = on mplus psTransition        l r
+        , psLinks             = on (<>)  psLinks             l r
         }
 
 
@@ -97,7 +101,7 @@
     mempty  = PresentationSettings
                 Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
                 Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
-                Nothing
+                Nothing Nothing
 
 
 --------------------------------------------------------------------------------
@@ -204,6 +208,7 @@
     , Pandoc.Ext_strikeout
     , Pandoc.Ext_superscript
     , Pandoc.Ext_subscript
+    , Pandoc.Ext_shortcut_reference_links
     ]
 
 
@@ -293,6 +298,25 @@
 instance A.FromJSON TransitionSettings where
     parseJSON = A.withObject "FromJSON TransitionSettings" $ \o ->
         TransitionSettings <$> o A..: "type" <*> pure o
+
+
+--------------------------------------------------------------------------------
+data LinkSettings = LinkSettings
+    { lsOSC8 :: !(Maybe Bool)
+    } deriving (Eq, Show)
+
+
+--------------------------------------------------------------------------------
+instance Semigroup LinkSettings where
+    l <> r = LinkSettings
+        { lsOSC8 = on mplus lsOSC8 l r
+        }
+
+
+--------------------------------------------------------------------------------
+instance A.FromJSON LinkSettings where
+    parseJSON = A.withObject "FromJSON LinkSettings" $ \o ->
+        LinkSettings <$> o A..:? "osc8"
 
 
 --------------------------------------------------------------------------------
diff --git a/lib/Patat/Presentation/Syntax.hs b/lib/Patat/Presentation/Syntax.hs
--- a/lib/Patat/Presentation/Syntax.hs
+++ b/lib/Patat/Presentation/Syntax.hs
@@ -48,7 +48,8 @@
 import qualified Data.Text.Encoding          as T
 import           Data.Traversable            (for)
 import qualified Data.Yaml                   as Yaml
-import           Patat.Presentation.Settings (PresentationSettings)
+import           Patat.Presentation.Settings (PresentationSettings,
+                                              parseSlideSettings)
 import           Patat.Unique
 import qualified Text.Pandoc                 as Pandoc
 import qualified Text.Pandoc.Writers.Shared  as Pandoc
@@ -201,7 +202,7 @@
     , Just t2 <- T.stripSuffix "-->" t1 = pure $ Config $
         case Yaml.decodeEither' (T.encodeUtf8 t2) of
             Left err  -> Left (show err)
-            Right obj -> Right obj
+            Right obj -> parseSlideSettings obj
     -- Parse other comments.
     | Just t1 <- T.stripPrefix "<!--" body
     , Just t2 <- T.stripSuffix "-->" t1 = pure $ SpeakerNote $ T.strip t2
@@ -296,13 +297,13 @@
 -- items become visible.
 data RevealSequence a = RevealSequence
     { -- The ID used for this sequence.
-      rsID :: RevealID
+      rsID      :: RevealID
     , -- These reveals should be advanced in this order.
       -- Reveal IDs will be included multiple times if needed.
       --
       -- This should (only) contain the ID of this counter, and IDs of counters
       -- nested inside the children fields.
-      rsOrder :: [RevealID]
+      rsOrder   :: [RevealID]
     , -- For each piece of content in this sequence, we store a set of ints.
       -- When the current counter state is included in this set, the item is
       -- visible.
diff --git a/lib/Patat/PrettyPrint.hs b/lib/Patat/PrettyPrint.hs
--- a/lib/Patat/PrettyPrint.hs
+++ b/lib/Patat/PrettyPrint.hs
@@ -29,6 +29,7 @@
     , deindent
 
     , ansi
+    , hyperlink
 
     , (<+>)
     , (<$$>)
@@ -126,6 +127,11 @@
 --------------------------------------------------------------------------------
 ansi :: [Ansi.SGR] -> Doc -> Doc
 ansi codes =  mkDoc . Ansi (codes ++)
+
+
+--------------------------------------------------------------------------------
+hyperlink :: String -> Doc -> Doc
+hyperlink url =  mkDoc . Hyperlink url
 
 
 --------------------------------------------------------------------------------
diff --git a/lib/Patat/PrettyPrint/Internal.hs b/lib/Patat/PrettyPrint/Internal.hs
--- a/lib/Patat/PrettyPrint/Internal.hs
+++ b/lib/Patat/PrettyPrint/Internal.hs
@@ -34,7 +34,7 @@
 --------------------------------------------------------------------------------
 import           Control.Monad.Reader       (asks, local)
 import           Control.Monad.RWS          (RWS, runRWS)
-import           Control.Monad.State        (get, modify)
+import           Control.Monad.State        (get, modify, state)
 import           Control.Monad.Writer       (tell)
 import           Data.Char.WCWidth.Extended (wcstrwidth)
 import qualified Data.List                  as L
@@ -53,9 +53,16 @@
 
 
 --------------------------------------------------------------------------------
+-- | Hyperlinks can be printed in multiple parts when formatting is used.  The
+-- ID is used to tell which ones belong to the same link.
+type HyperlinkID = Int
+
+
+--------------------------------------------------------------------------------
 -- | A simple chunk of text.  All ANSI codes are "reset" after printing.
 data Chunk
     = StringChunk [Ansi.SGR] String
+    | HyperlinkChunk HyperlinkID [Ansi.SGR] String String -- Codes, title, URL
     | NewlineChunk
     | ControlChunk Control
     deriving (Eq, Show)
@@ -72,6 +79,10 @@
     Ansi.hSetSGR h (reverse codes)
     IO.hPutStr h str
     Ansi.hSetSGR h [Ansi.Reset]
+hPutChunk h (HyperlinkChunk hid codes str url) = do
+    Ansi.hSetSGR h (reverse codes)
+    Ansi.hHyperlinkWithId h (show hid) url str
+    Ansi.hSetSGR h [Ansi.Reset]
 hPutChunk h (ControlChunk ctrl) = case ctrl of
     ClearScreenControl -> Ansi.hClearScreen h
     GoToLineControl l  -> Ansi.hSetCursorPosition h l 0
@@ -79,21 +90,29 @@
 
 --------------------------------------------------------------------------------
 chunkToString :: Chunk -> String
-chunkToString NewlineChunk        = "\n"
-chunkToString (StringChunk _ str) = str
-chunkToString (ControlChunk _)    = ""
+chunkToString NewlineChunk               = "\n"
+chunkToString (StringChunk _ str)        = str
+chunkToString (HyperlinkChunk _ _ str _) = str
+chunkToString (ControlChunk _)           = ""
 
 
 --------------------------------------------------------------------------------
 -- | If two neighboring chunks have the same set of ANSI codes, we can group
 -- them together.
 optimizeChunks :: Chunks -> Chunks
-optimizeChunks (StringChunk c1 s1 : StringChunk c2 s2 : chunks)
-    | c1 == c2  = optimizeChunks (StringChunk c1 (s1 <> s2) : chunks)
-    | otherwise =
-        StringChunk c1 s1 : optimizeChunks (StringChunk c2 s2 : chunks)
-optimizeChunks (x : chunks) = x : optimizeChunks chunks
-optimizeChunks [] = []
+optimizeChunks chunks = case chunks of
+    (StringChunk c1 s1 : StringChunk c2 s2 : t)
+        | c1 == c2 -> optimizeChunks (StringChunk c1 (s1 <> s2) : t)
+        | otherwise ->
+            StringChunk c1 s1 : optimizeChunks (StringChunk c2 s2 : t)
+    (HyperlinkChunk i1 c1 s1 u1 : HyperlinkChunk i2 c2 s2 u2 : t)
+        | i1 == i2 && c1 == c2 && u1 == u2 -> optimizeChunks $
+            HyperlinkChunk i1 c1 (s1 <> s2) u1 : t
+        | otherwise ->
+            HyperlinkChunk i1 c1 s1 u1 :
+            optimizeChunks (HyperlinkChunk i2 c2 s2 u2 : t)
+    x : t -> x : optimizeChunks t
+    [] -> []
 
 
 --------------------------------------------------------------------------------
@@ -123,15 +142,19 @@
         , indentOtherLines :: Indentation [Chunk]
         , indentDoc        :: d
         }
+    | Hyperlink String d
     | Control Control
     deriving (Functor)
 
 
 --------------------------------------------------------------------------------
 chunkToDocE :: Chunk -> DocE Doc
-chunkToDocE NewlineChunk         = Hardline
-chunkToDocE (StringChunk c1 str) = Ansi (\c0 -> c1 ++ c0) (Doc [String str])
-chunkToDocE (ControlChunk ctrl)  = Control ctrl
+chunkToDocE chunk = case chunk of
+    NewlineChunk              -> Hardline
+    ControlChunk ctrl         -> Control ctrl
+    StringChunk c1 s          -> Ansi (\c0 -> c1 ++ c0) $ Doc [String s]
+    HyperlinkChunk _ c1 s url -> Ansi (\c0 -> c1 ++ c0) $ Doc
+        [Hyperlink url $ Doc [String s]]
 
 
 --------------------------------------------------------------------------------
@@ -151,17 +174,29 @@
 
 --------------------------------------------------------------------------------
 data DocEnv = DocEnv
-    { deCodes  :: [Ansi.SGR]             -- ^ Most recent ones first in the list
-    , deIndent :: [Indentation [Chunk]]  -- ^ No need to store first-line indent
-    , deWrap   :: Maybe Int              -- ^ Wrap at columns
+    { -- | Most recent ones first
+      deCodes     :: [Ansi.SGR]
+    , -- | First-line indent not included
+      deIndent    :: [Indentation [Chunk]]
+    , -- | Wrap at columns
+      deWrap      :: Maybe Int
+    , -- | Hyperlink context
+      deHyperlink :: Maybe (HyperlinkID, String)
     }
 
 
 --------------------------------------------------------------------------------
-type DocM = RWS DocEnv Chunks LineBuffer
+data DocState = DocState
+    { dsLineBuffer  :: LineBuffer
+    , dsHyperlinkID :: Int
+    }
 
 
 --------------------------------------------------------------------------------
+type DocM = RWS DocEnv Chunks DocState
+
+
+--------------------------------------------------------------------------------
 -- | Note that the lists here are reversed so we have fast append.
 -- We also store the current length to avoid having to recompute it.
 data LineBuffer = LineBuffer Int [Indentation [Chunk]] [Chunk]
@@ -203,9 +238,10 @@
 --------------------------------------------------------------------------------
 docToChunks :: Doc -> Chunks
 docToChunks doc0 =
-    let env0        = DocEnv [] [] Nothing
-        ((), b, cs) = runRWS (go $ unDoc doc0) env0 emptyLineBuffer in
-    optimizeChunks (cs <> bufferToChunks b)
+    let env0        = DocEnv [] [] Nothing Nothing
+        state0      = DocState emptyLineBuffer 0
+        ((), finalState, cs) = runRWS (go $ unDoc doc0) env0 state0 in
+    optimizeChunks (cs <> bufferToChunks (dsLineBuffer finalState))
   where
     go :: [DocE Doc] -> DocM ()
 
@@ -216,6 +252,12 @@
         appendChunk chunk
         go docs
 
+    go (Hyperlink url doc : docs) = do
+        hid <- state $ \s ->
+            let hid = dsHyperlinkID s in (hid, s {dsHyperlinkID = hid + 1})
+        local (\env -> env {deHyperlink = Just (hid, url)}) (go $ unDoc doc)
+        go docs
+
     go (Softspace : docs) = do
         hard <- softConversion Softspace docs
         go (hard : docs)
@@ -230,12 +272,13 @@
         go (hard : docs)
 
     go (Hardline : docs) = do
-        buffer <- get
+        buffer <- dsLineBuffer <$> get
         tell $ bufferToChunks buffer <> [NewlineChunk]
         ind <- asks deIndent
-        modify $ \_ -> case docs of
-            []    -> emptyLineBuffer
-            _ : _ -> LineBuffer (sum $ map indentationWidth ind) ind []
+        let indw = sum $ map indentationWidth ind
+        modify $ \s -> case docs of
+            []    -> s {dsLineBuffer = emptyLineBuffer}
+            _ : _ -> s {dsLineBuffer = LineBuffer indw ind []}
         go docs
 
     go (WrapAt {..} : docs) = do
@@ -250,8 +293,10 @@
 
     go (Indent {..} : docs) = do
         local (\e -> e {deIndent = indentOtherLines : deIndent e}) $ do
-            modify $ \(LineBuffer w i c) -> LineBuffer
-                (w + indentationWidth indentFirstLine) (indentFirstLine : i) c
+            modify $ \s ->
+                let LineBuffer w i c = dsLineBuffer s
+                    w' = w + indentationWidth indentFirstLine in
+                s {dsLineBuffer = LineBuffer w' (indentFirstLine : i) c}
             go (unDoc indentDoc)
         go docs
 
@@ -259,15 +304,19 @@
         tell [ControlChunk ctrl]
         go docs
 
-
     makeChunk :: String -> DocM Chunk
     makeChunk str = do
         codes <- asks deCodes
-        return $ StringChunk codes str
+        mbHyperlink <- asks deHyperlink
+        return $ case mbHyperlink of
+            Just (hid, url) -> HyperlinkChunk hid codes str url
+            Nothing         -> StringChunk codes str
 
     appendChunk :: Chunk -> DocM ()
-    appendChunk c = modify $ \(LineBuffer w i cs) ->
-        LineBuffer (w + wcstrwidth (chunkToString c)) i (c : cs)
+    appendChunk c = modify $ \s ->
+        let LineBuffer w i cs = dsLineBuffer s
+            w' = w + wcstrwidth (chunkToString c) in
+        s {dsLineBuffer = LineBuffer w' i (c : cs)}
 
     -- Convert 'Softspace' or 'Softline' to 'Hardspace' or 'Hardline'
     softConversion :: DocE Doc -> [DocE Doc] -> DocM (DocE Doc)
@@ -276,7 +325,7 @@
         case mbWrapCol of
             Nothing     -> return hard
             Just maxCol -> do
-                LineBuffer currentCol _ _ <- get
+                LineBuffer currentCol _ _ <- dsLineBuffer <$> get
                 case nextWordLength docs of
                     Nothing                            -> return hard
                     Just l
@@ -289,18 +338,19 @@
             _         -> soft
 
     nextWordLength :: [DocE Doc] -> Maybe Int
-    nextWordLength []                 = Nothing
+    nextWordLength []                  = Nothing
     nextWordLength (String x : xs)
-        | L.null x                    = nextWordLength xs
-        | otherwise                   = Just (wcstrwidth x)
-    nextWordLength (Softspace : xs)   = nextWordLength xs
-    nextWordLength (Hardspace : xs)   = nextWordLength xs
-    nextWordLength (Softline : xs)    = nextWordLength xs
-    nextWordLength (Hardline : _)     = Nothing
-    nextWordLength (WrapAt {..} : xs) = nextWordLength (unDoc wrapDoc   ++ xs)
-    nextWordLength (Ansi   {..} : xs) = nextWordLength (unDoc ansiDoc   ++ xs)
-    nextWordLength (Indent {..} : xs) = nextWordLength (unDoc indentDoc ++ xs)
-    nextWordLength (Control _ : _)    = Nothing
+        | L.null x                     = nextWordLength xs
+        | otherwise                    = Just (wcstrwidth x)
+    nextWordLength (Hyperlink t _ : _) = Just (wcstrwidth t)
+    nextWordLength (Softspace : xs)    = nextWordLength xs
+    nextWordLength (Hardspace : xs)    = nextWordLength xs
+    nextWordLength (Softline : xs)     = nextWordLength xs
+    nextWordLength (Hardline : _)      = Nothing
+    nextWordLength (WrapAt {..} : xs)  = nextWordLength (unDoc wrapDoc   ++ xs)
+    nextWordLength (Ansi   {..} : xs)  = nextWordLength (unDoc ansiDoc   ++ xs)
+    nextWordLength (Indent {..} : xs)  = nextWordLength (unDoc indentDoc ++ xs)
+    nextWordLength (Control _ : _)     = Nothing
 
 
 --------------------------------------------------------------------------------
diff --git a/lib/Patat/PrettyPrint/Matrix.hs b/lib/Patat/PrettyPrint/Matrix.hs
--- a/lib/Patat/PrettyPrint/Matrix.hs
+++ b/lib/Patat/PrettyPrint/Matrix.hs
@@ -46,6 +46,8 @@
     go r y _ (NewlineChunk : cs)                     = go r (y + 1) 0 cs
     go r y x (ControlChunk ClearScreenControl  : cs) = go r y x cs  -- ?
     go r _ x (ControlChunk (GoToLineControl y) : cs) = go r y x cs
+    go r y x (HyperlinkChunk _ codes str _ : cs)       =
+        go r y x (StringChunk codes str : cs)  --  Links are erased
     go r y x chunks@(StringChunk codes (z : zs) : cs)
         | x + w > cols = go r (y + 1) 0 chunks
         | otherwise    = do
diff --git a/patat.cabal b/patat.cabal
--- a/patat.cabal
+++ b/patat.cabal
@@ -1,5 +1,5 @@
 Name:          patat
-Version:       0.14.2.0
+Version:       0.15.0.0
 Synopsis:      Terminal-based presentations using Pandoc
 Description:   Terminal-based presentations using Pandoc.
 License:       GPL-2
