diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Revision history for heist-extra
 
+## 0.4.0.0 (2025-08-19)
+
+- **Backwards incompatible** changes
+  - Improved Header renderer to included heading ID (#6)
+- Handle simple inline raw html such as for `<kbd>` (#8)
+
 ## 0.3.0.0 (2023-08-09)
 
 - `treeSplice`: pass children to sortKey function (#2)
diff --git a/heist-extra.cabal b/heist-extra.cabal
--- a/heist-extra.cabal
+++ b/heist-extra.cabal
@@ -1,6 +1,6 @@
 cabal-version:      2.4
 name:               heist-extra
-version:            0.3.0.0
+version:            0.4.0.0
 license:            MIT
 copyright:          2022 Sridhar Ratnakumar
 maintainer:         srid@srid.ca
@@ -69,7 +69,7 @@
     ViewPatterns
 
   build-depends:
-    , base          >=4.13.0.0 && <4.18.0.0.0
+    , base          >=4.13.0.0 && <5
     , data-default
     , filepath
     , heist         >=1.1.1.0
diff --git a/src/Heist/Extra/Splices/Pandoc/Ctx.hs b/src/Heist/Extra/Splices/Pandoc/Ctx.hs
--- a/src/Heist/Extra/Splices/Pandoc/Ctx.hs
+++ b/src/Heist/Extra/Splices/Pandoc/Ctx.hs
@@ -103,10 +103,6 @@
   B.OrderedList {} -> childTagAttr node "OrderedList"
   B.CodeBlock {} -> childTagAttr node "CodeBlock"
   B.BlockQuote {} -> childTagAttr node "BlockQuote"
-  B.Header level _ _ ->
-    fromMaybe B.nullAttr $ do
-      header <- X.childElementTag "Header" node
-      pure $ childTagAttr header ("h" <> show level)
   _ -> B.nullAttr
 
 inlineLookupAttr :: X.Node -> B.Inline -> B.Attr
diff --git a/src/Heist/Extra/Splices/Pandoc/Render.hs b/src/Heist/Extra/Splices/Pandoc/Render.hs
--- a/src/Heist/Extra/Splices/Pandoc/Render.hs
+++ b/src/Heist/Extra/Splices/Pandoc/Render.hs
@@ -43,14 +43,14 @@
 rpBlock' :: RenderCtx -> B.Block -> HI.Splice Identity
 rpBlock' ctx@RenderCtx {..} b = case b of
   B.Plain is ->
-    rpInlineWithTasks ctx is
+    rpInlineWithTasks ctx (convertRawInline [] is)
   B.Para is -> do
-    let innerSplice = rpInlineWithTasks ctx is
+    let innerSplice = rpInlineWithTasks ctx (convertRawInline [] is)
     withTplTag ctx "Para" ("inlines" ## innerSplice) $
       one . X.Element "p" mempty <$> innerSplice
   B.LineBlock iss ->
     flip foldMapM iss $ \is ->
-      foldMapM (rpInline ctx) is >> pure [X.TextNode "\n"]
+      foldMapM (rpInline ctx) (convertRawInline [] is) >> pure [X.TextNode "\n"]
   B.CodeBlock (id', mkLangClass -> classes, attrs) s -> do
     pure $
       one . X.Element "div" (rpAttr $ bAttr b) $
@@ -81,14 +81,16 @@
     withTplTag ctx "DefinitionList" (definitionListSplices defs) $
       fmap (one . X.Element "dl" mempty) $
         flip foldMapM defs $ \(term, descList) -> do
-          a <- foldMapM (rpInline ctx) term
+          a <- foldMapM (rpInline ctx) (convertRawInline [] term)
           as <-
             flip foldMapM descList $
               fmap (one . X.Element "dd" mempty) . foldMapM (rpBlock ctx)
           pure $ a <> as
-  B.Header level attr is ->
-    one . X.Element (headerTag level) (rpAttr $ concatAttr attr $ bAttr b)
-      <$> foldMapM (rpInline ctx) is
+  B.Header level attr@(headerId, _, _) is -> do
+    let innerSplice = foldMapM (rpInline ctx) (convertRawInline [] is)
+    withTplTag ctx ("Header:" <> show level) (headerSplices headerId innerSplice) $
+      one . X.Element (headerTag level) (rpAttr $ concatAttr attr $ bAttr b)
+        <$> innerSplice
   B.HorizontalRule ->
     withTplTag ctx "HorizontalRule" mempty (pure $ one $ X.Element "hr" mempty mempty)
   B.Table attr _captions _colSpec (B.TableHead _ hrows) tbodys _tfoot -> do
@@ -136,6 +138,10 @@
         let lang = head classes
         pure $ lang : ("language-" <> lang) : tail classes
 
+    headerSplices headerId innerSplice = do
+      "header:id" ## HI.textSplice headerId
+      "inlines" ## innerSplice
+
     definitionListSplices :: [([B.Inline], [[B.Block]])] -> H.Splices (HI.Splice Identity)
     definitionListSplices defs = do
       "DefinitionList:Items" ## (HI.runChildrenWith . uncurry itemsSplices) `foldMapM` defs
@@ -246,6 +252,40 @@
       B.DoubleQuote ->
         w <&> \nodes ->
           [X.TextNode "“"] <> nodes <> [X.TextNode "”"]
+
+{- | Convert raw html attribute sequence into span
+
+ For example, this markdown: `<kbd>ctrl</kbd>`, which is in native pandoc:
+   RawInline (Format "html") "<kbd>" : Str "ctrl" : RawInline (Format "html") "</kbd>"
+ … is converted as:
+   <span xmlhtmlraw=""><kbd>ctrl</kbd></span>
+ … instead of the default behavior which is:
+   <span xmlhtmlraw=""><kbd></kbd></span>ctrl<span xmlhtmlraw=""></span>
+-}
+convertRawInline :: [B.Inline] -> [B.Inline] -> [B.Inline]
+convertRawInline acc = \case
+  [] -> reverse acc
+  (B.RawInline (B.Format "html") oTag : rest)
+    | -- This is a new raw tag, let's find a matching closing tag
+      Just (newElem, is) <- mkHtmlInline oTag rest ->
+        convertRawInline (newElem : acc) is
+  i : is -> convertRawInline (i : acc) is
+  where
+    mkHtmlInline :: Text -> [B.Inline] -> Maybe (B.Inline, [B.Inline])
+    mkHtmlInline oTag rest = case span (not . isClosingTag oTag) rest of
+      -- Collect the inner element until a closing tag
+      (inner, (closing : is))
+        | -- Verify we did find a matching tag
+          isClosingTag oTag closing ->
+            let inner' = oTag <> plainify inner <> "</" <> T.drop 1 oTag
+             in Just (B.RawInline (B.Format "html") inner', is)
+      _ -> Nothing
+
+    isClosingTag :: Text -> B.Inline -> Bool
+    isClosingTag oTag = \case
+      B.RawInline (B.Format "html") eTag ->
+        "<" `T.isPrefixOf` oTag && "</" `T.isPrefixOf` eTag && T.drop 1 oTag == T.drop 2 eTag
+      _ -> False
 
 -- | Like rpInline', but supports task checkbox in the given inlines.
 rpInlineWithTasks :: RenderCtx -> [B.Inline] -> HI.Splice Identity
