diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
 # Revision history for djot
 
+## 0.1.1.0 -- 2024-02-29
+
+* Add Data instances to everything in the AST [API change].
+
+* Ensure that block attributes are indented on subsequent lines (#2).
+
+* Djot.Blocks: use ByteString directly in `toIdentifier` (#1,
+  Vaibhav Sagar).
+
 ## 0.1.0.0 -- 2024-02-14
 
 * Initial release.
diff --git a/djot.cabal b/djot.cabal
--- a/djot.cabal
+++ b/djot.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               djot
-version:            0.1.0.0
+version:            0.1.1.0
 synopsis:           Parser and renderer for djot light markup syntax.
 description:        Djot (<https://djot.net>) is a light markup language.
                     This package provides a data structure to represent
diff --git a/src/Djot/AST.hs b/src/Djot/AST.hs
--- a/src/Djot/AST.hs
+++ b/src/Djot/AST.hs
@@ -91,7 +91,7 @@
 -- import Debug.Trace
 
 newtype Attr = Attr [(ByteString, ByteString)]
-  deriving (Show, Eq, Ord, Typeable, Generic, Data)
+  deriving (Show, Eq, Ord, Typeable, Data, Generic)
 
 instance Semigroup Attr where
   Attr as <> Attr bs =
@@ -112,7 +112,7 @@
       | otherwise -> kvs
 
 data Pos = NoPos | Pos Int Int Int Int -- start line, start col, end line, end col
-  deriving (Show, Eq, Ord, Typeable, Generic)
+  deriving (Show, Eq, Ord, Typeable, Data, Generic)
 
 instance Semigroup Pos where
   Pos sl1 sc1 _ _ <> Pos _ _ el2 ec2 =
@@ -125,7 +125,7 @@
   mempty = NoPos
 
 data Node a = Node Pos Attr a
-  deriving (Show, Eq, Ord, Functor, Traversable, Foldable, Typeable, Generic)
+  deriving (Show, Eq, Ord, Functor, Traversable, Foldable, Typeable, Data, Generic)
 
 {-# INLINE addAttr #-}
 addAttr :: Attr -> Node a -> Node a
@@ -136,18 +136,18 @@
 addPos pos (Node _ attr bs) = Node pos attr bs
 
 newtype Format = Format { unFormat :: ByteString }
-  deriving (Show, Eq, Ord, Typeable, Generic)
+  deriving (Show, Eq, Ord, Typeable, Data, Generic)
 
 data MathStyle = DisplayMath | InlineMath
-  deriving (Show, Ord, Eq, Typeable, Generic)
+  deriving (Show, Ord, Eq, Typeable, Data, Generic)
 
 data Target =
     Direct ByteString
   | Reference ByteString
-  deriving (Show, Ord, Eq, Typeable, Generic)
+  deriving (Show, Ord, Eq, Typeable, Data, Generic)
 
 data QuoteType = SingleQuotes | DoubleQuotes
-  deriving (Show, Ord, Eq, Typeable, Generic)
+  deriving (Show, Ord, Eq, Typeable, Data, Generic)
 
 data Inline =
       Str ByteString
@@ -172,10 +172,10 @@
     | Quoted QuoteType Inlines
     | SoftBreak
     | HardBreak
-    deriving (Show, Ord, Eq, Typeable, Generic)
+    deriving (Show, Ord, Eq, Typeable, Data, Generic)
 
 newtype Many a = Many { unMany :: Seq a }
-  deriving (Show, Ord, Eq, Functor, Traversable, Foldable, Typeable, Generic)
+  deriving (Show, Ord, Eq, Functor, Traversable, Foldable, Typeable, Data, Generic)
 
 type Inlines = Many (Node Inline)
 
@@ -216,37 +216,37 @@
   mempty = Many mempty
 
 data ListSpacing = Tight | Loose
-  deriving (Show, Ord, Eq, Typeable, Generic)
+  deriving (Show, Ord, Eq, Typeable, Data, Generic)
 
 data OrderedListStyle =
   Decimal | LetterUpper | LetterLower | RomanUpper | RomanLower
-  deriving (Show, Ord, Eq, Typeable, Generic)
+  deriving (Show, Ord, Eq, Typeable, Data, Generic)
 
 data OrderedListDelim =
   RightPeriod | RightParen | LeftRightParen
-  deriving (Show, Ord, Eq, Typeable, Generic)
+  deriving (Show, Ord, Eq, Typeable, Data, Generic)
 
 data OrderedListAttributes =
   OrderedListAttributes
   { orderedListStyle :: OrderedListStyle
   , orderedListDelim :: OrderedListDelim
   , orderedListStart :: Int }
-  deriving (Show, Ord, Eq, Typeable, Generic)
+  deriving (Show, Ord, Eq, Typeable, Data, Generic)
 
 data TaskStatus = Complete | Incomplete
-  deriving (Show, Ord, Eq, Typeable, Generic)
+  deriving (Show, Ord, Eq, Typeable, Data, Generic)
 
 newtype Caption = Caption Blocks
-  deriving (Show, Ord, Eq, Typeable, Generic)
+  deriving (Show, Ord, Eq, Typeable, Data, Generic)
 
 data Align = AlignLeft | AlignRight | AlignCenter | AlignDefault
-  deriving (Show, Ord, Eq, Typeable, Generic)
+  deriving (Show, Ord, Eq, Typeable, Data, Generic)
 
 data CellType = HeadCell | BodyCell
-  deriving (Show, Ord, Eq, Typeable, Generic)
+  deriving (Show, Ord, Eq, Typeable, Data, Generic)
 
 data Cell = Cell CellType Align Inlines
-  deriving (Show, Ord, Eq, Typeable, Generic)
+  deriving (Show, Ord, Eq, Typeable, Data, Generic)
 
 data Block =
     Para Inlines
@@ -262,7 +262,7 @@
   | ThematicBreak
   | Table (Maybe Caption) [[Cell]]
   | RawBlock Format ByteString
-  deriving (Show, Ord, Eq, Typeable, Generic)
+  deriving (Show, Ord, Eq, Typeable, Data, Generic)
 
 type Blocks = Many (Node Block)
 
@@ -279,7 +279,7 @@
      , docReferences :: ReferenceMap
      , docAutoReferences :: ReferenceMap
      , docAutoIdentifiers :: Set ByteString
-     } deriving (Show, Ord, Eq, Typeable, Generic)
+     } deriving (Show, Ord, Eq, Typeable, Data, Generic)
 
 instance Semigroup Doc where
   Doc bs ns rs ar ai <> Doc bs' ns' rs' ar' ai' =
@@ -291,7 +291,7 @@
 
 -- | A map from labels to contents.
 newtype NoteMap = NoteMap { unNoteMap :: M.Map ByteString Blocks }
-  deriving (Show, Ord, Eq, Semigroup, Monoid, Typeable, Generic)
+  deriving (Show, Ord, Eq, Semigroup, Monoid, Typeable, Data, Generic)
 
 insertNote :: ByteString -> Blocks -> NoteMap -> NoteMap
 insertNote label ref (NoteMap m) =
@@ -303,7 +303,7 @@
 
 newtype ReferenceMap =
   ReferenceMap { unReferenceMap :: M.Map ByteString (ByteString, Attr) }
-  deriving (Show, Ord, Eq, Semigroup, Monoid, Typeable, Generic)
+  deriving (Show, Ord, Eq, Semigroup, Monoid, Typeable, Data, Generic)
 
 normalizeLabel :: ByteString -> ByteString
 normalizeLabel = B8.unwords . B8.words
diff --git a/src/Djot/Blocks.hs b/src/Djot/Blocks.hs
--- a/src/Djot/Blocks.hs
+++ b/src/Djot/Blocks.hs
@@ -26,7 +26,6 @@
 import Data.ByteString (ByteString)
 import Control.Monad (replicateM_, void, mzero, unless, when, guard, foldM)
 import Data.List.NonEmpty (NonEmpty(..))
-import Data.List (intercalate)
 import qualified Data.List.NonEmpty as NonEmpty
 import Data.Set (Set)
 import qualified Data.Set as Set
@@ -1127,12 +1126,11 @@
       closeContainingSections lev
     _ -> pure ()
 
--- TODO avoid detour through String
 toIdentifier :: ByteString -> ByteString
 toIdentifier bs =
   if null parts
      then "sec"
-     else strToUtf8 $ intercalate "-" parts
+     else B8.intercalate (B8.singleton '-') parts
  where
    isSym = (`elem` ("][~!@#$%^&*(){}`,.<>\\|=+/" :: [Char]))
-   parts = words $ map (\c -> if isSym c then ' ' else c) $ utf8ToStr bs
+   parts = B8.words $ B8.map (\c -> if isSym c then ' ' else c) bs
diff --git a/src/Djot/Djot.hs b/src/Djot/Djot.hs
--- a/src/Djot/Djot.hs
+++ b/src/Djot/Djot.hs
@@ -131,6 +131,15 @@
     | escapable c = '\\' : c : go cs
     | otherwise = c : go cs
 
+newtype BlockAttr = BlockAttr Attr
+
+formatAttrPart :: (ByteString, ByteString) -> Layout.Doc Text
+formatAttrPart ("id",ident) = literal ("#" <> fromUtf8 ident)
+formatAttrPart ("class", classes') = hsep $ map (("." <>) . literal)
+                                         $ T.words $ fromUtf8 classes'
+formatAttrPart (k,v) = literal (fromUtf8 k) <> "=" <>
+                       doubleQuotes (literal (escapeDjot Normal v))
+
 {-# SPECIALIZE toLayout :: Blocks -> State BState (Layout.Doc Text) #-}
 {-# SPECIALIZE toLayout :: Inlines -> State BState (Layout.Doc Text) #-}
 {-# SPECIALIZE toLayout :: Attr -> State BState (Layout.Doc Text) #-}
@@ -149,13 +158,16 @@
                 then mempty
                 else "{" <> contents <> "}"
        where
-         contents = hsep (map go kvs)
-         go ("id",ident) = literal ("#" <> fromUtf8 ident)
-         go ("class", classes') = hsep $ map (("." <>) . literal)
-                                       $ T.words $ fromUtf8 classes'
-         go (k,v) = literal (fromUtf8 k) <> "=" <>
-                       doubleQuotes (literal (escapeDjot Normal v))
+         contents = hsep (map formatAttrPart kvs)
 
+instance ToLayout BlockAttr where
+  toLayout (BlockAttr (Attr kvs))
+    = pure $ if isEmpty contents
+                then mempty
+                else hang 1 "{" (contents <> "}")
+       where
+         contents = hsep (map formatAttrPart kvs)
+
 instance ToLayout (Node Block) where
   toLayout (Node _pos attr bl) =
     ($$) <$> (case bl of
@@ -163,15 +175,17 @@
                 Heading{} -> do
                   autoids <- gets autoIds
                   let Attr as = attr
-                  toLayout $ Attr [(k,v) | (k,v) <- as
+                  toLayout $ BlockAttr
+                           $ Attr [(k,v) | (k,v) <- as
                                   , not (k == "id" && v `Set.member` autoids)]
                 Section{} -> do
                   autoids <- gets autoIds
                   let Attr as = attr
-                  toLayout $ Attr [(k,v) | (k,v) <- as
+                  toLayout $ BlockAttr
+                           $ Attr [(k,v) | (k,v) <- as
                                   , not (k == "id" &&
                                          v `Set.member` autoids)]
-                _ -> toLayout attr)
+                _ -> toLayout (BlockAttr attr))
          <*> (($$ blankline) <$> case bl of
                Para ils -> toLayout ils
                Heading lev ils -> do
