diff --git a/lib/GEODE/Metadata/ParagraphRecord.hs b/lib/GEODE/Metadata/ParagraphRecord.hs
new file mode 100644
index 0000000000000000000000000000000000000000..f37aaf0384d5f1358a1a4bd590c499c21ef834a7
--- /dev/null
+++ b/lib/GEODE/Metadata/ParagraphRecord.hs
@@ -0,0 +1,40 @@
+{-# LANGUAGE DeriveGeneric, ExplicitNamespaces, NamedFieldPuns, OverloadedStrings #-}
+module GEODE.Metadata.ParagraphRecord
+  ( Paragraph(..)
+  , ParagraphRecord ) where
+
+import Data.Aeson ((.=), FromJSON(..), ToJSON(..))
+import Data.Aeson.KeyMap as KeyMap (singleton)
+import Data.Csv
+  ( (.:), FromNamedRecord(..), ToNamedRecord(..), namedField, namedRecord )
+import GEODE.Metadata as Article
+  ( type (@)(..), ArticleRecord, DefaultFields(..), HasDefaultHeader(..)
+  , Record(..), ToJSONObject(..), uid, relativePath)
+import GHC.Generics (Generic)
+import System.FilePath ((<.>))
+
+newtype Paragraph = Paragraph { paragraph :: Int } deriving (Generic, Show)
+
+instance FromJSON Paragraph
+
+instance Record Paragraph where
+  uid = show . paragraph
+
+  relativePath (Paragraph {paragraph}) extension = show paragraph <.> extension
+
+type ParagraphRecord = ArticleRecord @ Paragraph
+
+instance FromNamedRecord Paragraph where
+  parseNamedRecord nr = Paragraph <$> nr .: "paragraph"
+
+instance ToNamedRecord Paragraph where
+  toNamedRecord (Paragraph {paragraph}) =
+    namedRecord [ namedField "paragraph" paragraph ]
+
+instance ToJSONObject Paragraph where
+  toJSONObject (Paragraph {paragraph}) =
+    KeyMap.singleton "paragraph" (toJSON paragraph)
+  toJSONPairs (Paragraph {paragraph}) = "paragraph" .= paragraph
+
+instance HasDefaultHeader Paragraph where
+  defaultFields = DefaultFields ["paragraph"]
diff --git a/lib/GEODE/Metadata/ProdigyMeta.hs b/lib/GEODE/Metadata/ProdigyMeta.hs
new file mode 100644
index 0000000000000000000000000000000000000000..c63ec937049b0ff37a838a12f3df48f49afd8549
--- /dev/null
+++ b/lib/GEODE/Metadata/ProdigyMeta.hs
@@ -0,0 +1,33 @@
+{-# LANGUAGE DeriveGeneric, ExplicitNamespaces, OverloadedStrings #-}
+module GEODE.Metadata.ProdigyMeta
+  ( ParagraphMeta
+  , ProdigyMeta(..) ) where
+
+import Data.Aeson ((.=), FromJSON(..), ToJSON(..))
+import Data.Aeson.KeyMap as KeyMap (fromList)
+import Data.Csv (FromNamedRecord(..), ToNamedRecord(..))
+import Data.Text (Text)
+import GEODE.Metadata (type (@), DefaultFields(..), HasDefaultHeader(..), ToJSONObject(..))
+import GEODE.Metadata.ParagraphRecord (ParagraphRecord)
+import GHC.Generics (Generic)
+
+data ProdigyMeta = ProdigyMeta
+  { totalParagraphs :: Int
+  , headword :: Text } deriving (Generic, Show)
+
+instance FromJSON ProdigyMeta
+instance ToJSONObject ProdigyMeta where
+  toJSONObject (ProdigyMeta {totalParagraphs, headword}) = KeyMap.fromList
+    [ ("totalParagraphs", toJSON totalParagraphs)
+    , ("headword", toJSON headword) ]
+  toJSONPairs (ProdigyMeta {totalParagraphs, headword}) =
+      "totalParagraphs" .= totalParagraphs
+    <> "headword" .= headword
+
+instance ToNamedRecord ProdigyMeta
+instance FromNamedRecord ProdigyMeta
+
+instance HasDefaultHeader ProdigyMeta where
+  defaultFields = DefaultFields ["totalParagraphs", "headword"]
+
+type ParagraphMeta = ParagraphRecord @ ProdigyMeta