diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,29 +1,49 @@
-# microformats2-parser [![Hackage](https://img.shields.io/hackage/v/microformats2-parser.svg?style=flat)](https://hackage.haskell.org/package/microformats2-parser) [![Build Status](https://img.shields.io/travis/myfreeweb/microformats2-parser.svg?style=flat)](https://travis-ci.org/myfreeweb/microformats2-parser) [![Coverage Status](https://img.shields.io/coveralls/myfreeweb/microformats2-parser.svg?style=flat)](https://coveralls.io/r/myfreeweb/microformats2-parser) [![unlicense](https://img.shields.io/badge/un-license-green.svg?style=flat)](http://unlicense.org)
+# microformats2-parser [![Hackage](https://img.shields.io/hackage/v/microformats2-parser.svg?style=flat)](https://hackage.haskell.org/package/microformats2-parser) [![Build Status](https://img.shields.io/travis/myfreeweb/microformats2-parser.svg?style=flat)](https://travis-ci.org/myfreeweb/microformats2-parser) [![unlicense](https://img.shields.io/badge/un-license-green.svg?style=flat)](http://unlicense.org)
 
-[Microformats 2] parser for Haskell!
+[Microformats 2] parser for Haskell! [#IndieWeb]
 
-Originally created for [sweetroll] :-)
+Originally created for [sweetroll].
 
-The types are located in a separate package called [microformats2-types].
+- parses `items`, `rels`, `rel-urls`
+- resolves relative URLs (with support for the `<base>` tag)
+- handles malformed HTML (the actual HTML parser is [tagstream-conduit])
+- high performance
+- extensively tested
 
+Also check out [http-link-header] because you often need to read links from the Link header!
+
 [Microformats 2]: http://microformats.org/wiki/microformats2
+[#IndieWeb]: http://indiewebcamp.com
 [sweetroll]: https://github.com/myfreeweb/sweetroll
-[microformats2-types]: https://github.com/myfreeweb/microformats2-types
+[tagstream-conduit]: https://hackage.haskell.org/package/tagstream-conduit
+[http-link-header]: https://github.com/myfreeweb/http-link-header
 
 ## [DEMO PAGE](https://unrelenting.technology/mf2/)
 
 ## Usage
 
+Look at the API docs [on Hackage](https://hackage.haskell.org/package/microformats2-parser) for more info, here's a quick overview:
+
 ```haskell
 {-# LANGUAGE OverloadedStrings #-}
+
 import Data.Microformats2.Parser
-import Data.Microformats2.Types
+import Data.Default
+import Network.URI
 
-parseEntry Sanitize $ documentRoot $ parseLBS "<body><p class=h-entry><h1 class=p-name>Yay!</h1></p></body>"
--- [ Entry { entryName = [ "Yay!" ], ... } ]
+parseMf2 def $ documentRoot $ parseLBS "<body><p class=h-entry><h1 class=p-name>Yay!</h1></p></body>"
+
+parseMf2 (def { baseUri = parseURI "https://where.i.got/that/page/from/" }) $ documentRoot $ parseLBS "<body><base href=\"base/\"><link rel=micropub href='micropub'><p class=h-entry><h1 class=p-name>Yay!</h1></p></body>"
 ```
 
-Look at the API docs [on Hackage](https://hackage.haskell.org/package/microformats2-parser) for more info.
+The `def` is the [default](https://hackage.haskell.org/package/data-default-class-0.0.1/docs/Data-Default-Class.html) configuration.
+
+The configuration includes:
+- `htmlMode`, an HTML parsing mode (`Unsafe` | `Escape` | **`Sanitize`**)
+- `baseUri`, the `Maybe URI` that represents the address you retrieved the HTML from, used for resolving relative addresses -- you should set it
+
+`parseMf2` will return an Aeson [Value](https://hackage.haskell.org/package/aeson-0.8.0.2/docs/Data-Aeson-Types.html#t:Value) structured like [canonical microformats2 JSON](http://microformats.org/wiki/microformats2).
+[lens-aeson](https://hackage.haskell.org/package/lens-aeson) is a good way to navigate it.
 
 ## Development
 
diff --git a/executable/Main.hs b/executable/Main.hs
--- a/executable/Main.hs
+++ b/executable/Main.hs
@@ -7,14 +7,19 @@
 import           Data.Microformats2.Parser
 import           Data.List
 import           Data.Maybe (fromMaybe)
+import           Data.Aeson.Encode.Pretty
+import           Data.Aeson.Types (object)
+import           Data.Default
 import qualified Data.Text.Lazy as TL
 import           Data.Streaming.Network (bindPath)
 import qualified Data.Stringable as S
 import           Network.Wai.Handler.Warp
 import qualified Network.Wai.Handler.CGI as CGI
+import           Network.Wai.Middleware.Autohead
 import qualified Network.Socket as S
+import           Network.URI (parseURI)
 import           Web.Scotty
-import           Text.Blaze.Html5 as H
+import           Text.Blaze.Html5 as H hiding (main, param, object)
 import           Text.Blaze.Html5.Attributes as A
 import           Text.Blaze.Html.Renderer.Utf8 (renderHtml)
 import qualified Options as O
@@ -33,52 +38,56 @@
 
 exampleValue = "<body> <p class='h-adr'>   <span class='p-street-address'>17 Austerstræti</span>   <span class='p-locality'>Reykjavík</span>   <span class='p-country-name'>Iceland</span>   <span class='p-postal-code'>107</span> </p> <div class='h-card'>   <a class='p-name u-url'      href='http://blog.lizardwrangler.com/'      >Mitchell Baker</a>    (<a class='p-org h-card'        href='http://mozilla.org/'      >Mozilla Foundation</a>) </div> <article class='h-entry'>   <h1 class='p-name'>Microformats are amazing</h1>   <p>Published by <a class='p-author h-card' href='http://example.com'>W. Developer</a>      on <time class='dt-published' datetime='2013-06-13 12:00:00'>13<sup>th</sup> June 2013</time>     <p class='p-summary'>In which I extoll the virtues of using microformats.</p>     <div class='e-content'>     <p>Blah blah blah</p>   </div> </article> <span class='h-cite'>   <time class='dt-published'>YYYY-MM-DD</time>    <span class='p-author h-card'>AUTHOR</span>:    <cite><a class='u-url p-name' href='URL'>TITLE</a></cite> </span> </body>"
 
-homePage ∷ TL.Text → Html → Html
-homePage v result = docTypeHtml $ do
+homePage ∷ TL.Text → Html
+homePage v = docTypeHtml $ do
   H.head $ do
     H.meta ! charset "utf-8"
     H.title "microformats2-parser"
-    H.style "body { font-family: 'Helvetica Neue', sans-serif; max-width: 900px; margin: 0 auto; } a { color: #ba2323; } a:hover { color: #da4343; } pre, textarea, button { width: 100%; } textarea { resize: vertical; min-height: 10em; margin-bottom: 1em; } pre { white-space: pre-wrap; } footer { margin: 2em 0; }"
+    H.style "body { font-family: 'Helvetica Neue', sans-serif; max-width: 900px; margin: 0 auto; } a { color: #ba2323; } a:hover { color: #da4343; } pre, input, textarea, button { width: 100%; } input, textarea { margin-bottom: 1em; } textarea { resize: vertical; min-height: 15em; } pre { white-space: pre-wrap; } footer { margin: 2em 0; }"
   H.body $ do
-    h1 $ do
-      a ! href "https://github.com/myfreeweb/microformats2-parser" $ "microformats2-parser"
+    H.header $ do
+      h1 $ do
+        a ! href "https://github.com/myfreeweb/microformats2-parser" $ "microformats2-parser"
+      a ! href "https://github.com/myfreeweb/microformats2-parser" $ img ! alt "GitHub" ! src "https://img.shields.io/badge/git-hub-gray.svg?style=flat"
+      " "
+      a ! href "https://hackage.haskell.org/package/microformats2-parser" $ img ! alt "Hackage" ! src "https://img.shields.io/hackage/v/microformats2-parser.svg?style=flat"
+      " "
+      a ! href "https://travis-ci.org/myfreeweb/microformats2-parser" $ img ! alt "Build Status" ! src "https://img.shields.io/travis/myfreeweb/microformats2-parser.svg?style=flat"
+      " "
+      a ! href "http://unlicense.org" $ img ! alt "unlicense" ! src "https://img.shields.io/badge/un-license-green.svg?style=flat"
     p "This is a test page for the Microformats 2 Haskell parser."
-    p "Note:"
+    p "Notes:"
     ul $ do
-      li "this demo page uses the Sanitize mode for e-content in h-cite and h-entry"
-    H.form ! method "post" ! action "" $ do
+      li "this demo page uses the Sanitize mode for e-*"
+      li $ do
+        a ! href "http://enable-cors.org" $ "CORS is enabled"
+        " on the endpoint (POST parse.json, form-urlencoded, 'html' and 'base' parameter)"
+      li $ a ! href "http://jsonview.com" $ "JSONView is awesome"
+    H.form ! method "post" ! action "parse.json" $ do
       textarea ! name "html" $ toHtml v
+      input ! name "base" ! type_ "url" ! placeholder "https://example.com/base/url/for/resolving/relative/urls"
       button "Parse!"
-    result
     footer $ do
-      a ! href "https://unrelenting.technology" $ "unrelenting.technology"
-
-display ∷ Show α ⇒ α → Html
-display = pre . toHtml . S.toLazyText . show
-
-parseResult ∷ TL.Text → Html
-parseResult h = section $ do
-  let root = documentRoot $ parseLBS $ S.toLazyByteString h
-  h2 "h-geo"
-  display $ parseGeo root
-  h2 "h-adr"
-  display $ parseAdr root
-  h2 "h-card"
-  display $ parseCard root
-  h2 "h-cite"
-  display $ parseCite Sanitize root
-  h2 "h-entry"
-  display $ parseEntry Sanitize root
+      a ! href "https://unrelenting.technology" ! rel "author" $ "unrelenting.technology"
 
 app = scottyApp $ do
-  matchAny "/" $ do
-    ps ← params
-    let h' = lookup "html" ps
-        result = case h' of
-                   Just h → parseResult h
-                   Nothing → toHtml ("" ∷ TL.Text)
+  middleware autohead
+
+  get "/" $ do
     setHeader "Content-Type" "text/html; charset=utf-8"
-    raw $ renderHtml $ homePage (fromMaybe exampleValue h') result
+    raw $ renderHtml $ homePage exampleValue
+
+  get "/parse.json" $ do
+    setHeader "Access-Control-Allow-Origin" "*"
+    json $ object []
+
+  post "/parse.json" $ do
+    html ← param "html"
+    base ← param "base" `rescue` (\_ → return "")
+    setHeader "Content-Type" "application/json; charset=utf-8"
+    setHeader "Access-Control-Allow-Origin" "*"
+    let root = documentRoot $ parseLBS html
+    raw $ encodePretty $ parseMf2 (def { baseUri = parseURI base }) root
 
 main = O.runCommand $ \opts args → do
   let warpSettings = setPort (port opts) defaultSettings
diff --git a/library/Data/Microformats2/Parser.hs b/library/Data/Microformats2/Parser.hs
--- a/library/Data/Microformats2/Parser.hs
+++ b/library/Data/Microformats2/Parser.hs
@@ -1,7 +1,9 @@
 {-# LANGUAGE UnicodeSyntax, OverloadedStrings, CPP #-}
 
 module Data.Microformats2.Parser (
-  module Data.Microformats2.Parser
+  Mf2ParserSettings (..)
+, HtmlContentMode (..)
+, parseMf2
 
   -- * HTML parsing stuff (from html-conduit, xml-lens)
 , documentRoot
@@ -9,164 +11,129 @@
 , sinkDoc
 ) where
 
+import           Text.HTML.DOM
+import           Text.XML.Lens hiding ((.=))
 #if __GLASGOW_HASKELL__ < 709
 import           Control.Applicative
 #endif
-import           Control.Monad
-import           Data.Microformats2
-import           Data.Microformats2.Parser.Internal
+import           Data.Microformats2.Parser.Property
+import           Data.Microformats2.Parser.HtmlUtil
+import           Data.Microformats2.Parser.Util
 import           Data.Default
--- import qualified Data.Text as T
-import qualified Data.Text.Lazy as LT
-import           Text.HTML.DOM
-import           Text.XML.Lens
-
-data HtmlContentMode = Unsafe | Strip | Escape | Sanitize
-
-(.&&.) ∷ Monad μ ⇒ μ Bool → μ Bool → μ Bool
-(.&&.) = liftM2 (&&)
-
-findMicroformat ∷ String → Element → [Element]
-findMicroformat n e = e ^.. entire . hasClass n
-
--- | Parses all h-geo entries inside of an element.
-parseGeo ∷ Element → [Geo]
-parseGeo = map extractGeo . findGeo
-
--- | Finds all h-geo elements inside of an element.
-findGeo ∷ Element → [Element]
-findGeo = findMicroformat "h-geo"
-
--- | Parses an element as h-geo.
-extractGeo ∷ Element → Geo
-extractGeo e = def { geoLatitude  = extractPropertyR P "latitude"  e
-                   , geoLongitude = extractPropertyR P "longitude" e
-                   , geoAltitude  = extractPropertyR P "altitude"  e }
-
-
--- | Parses all h-adr entries inside of an element.
-parseAdr ∷ Element → [Adr]
-parseAdr = map extractAdr . findAdr
-
--- | Finds all h-adr elements inside of an element.
-findAdr ∷ Element → [Element]
-findAdr = findMicroformat "h-adr"
+import           Data.Aeson
+import           Data.Aeson.Types
+import           Data.Aeson.Lens
+import           Data.Char (isSpace)
+import qualified Data.HashMap.Strict as HMS
+import qualified Data.Vector as V
+import           Data.Maybe
+import qualified Data.Text as T
+import           Network.URI
+import           Safe (headMay)
 
--- | Parses an element as h-adr.
-extractAdr ∷ Element → Adr
-extractAdr e = def { adrStreetAddress   = extractPropertyL P "street-address"   e
-                   , adrExtendedAddress = extractPropertyL P "extended-address" e
-                   , adrPostOfficeBox   = extractPropertyL P "post-office-box"  e
-                   , adrLocality        = extractPropertyL P "locality"         e
-                   , adrRegion          = extractPropertyL P "region"           e
-                   , adrPostalCode      = extractPropertyL P "postal-code"      e
-                   , adrCountryName     = extractPropertyL P "country-name"     e
-                   , adrLabel           = extractPropertyL P "label"            e
-                   , adrGeo             = filter (/= GeoGeo def) $
-                                            (GeoGeo . extractGeo $ e) :
-                                                 (TextGeo <$> extractPropertyL P "geo" e)
-                                              ++ (GeoGeo . extractGeo <$> findPropertyMicroformat e "p-geo" "h-geo") }
+data Mf2ParserSettings = Mf2ParserSettings
+  { htmlMode ∷ HtmlContentMode
+  , baseUri  ∷ Maybe URI }
+  deriving (Show, Eq)
 
+instance Default Mf2ParserSettings where
+  def = Mf2ParserSettings { htmlMode = Sanitize
+                          , baseUri  = Nothing }
 
--- | Parses all h-card entries inside of an element.
-parseCard ∷ Element → [Card]
-parseCard = map extractCard . findCard
+mf2Elements ∷ Traversal' Element Element
+mf2Elements = attributeSatisfies "class" $ any isMf2Class . T.split isSpace
 
--- | Finds all h-card elements inside of an element.
-findCard ∷ Element → [Element]
-findCard = findMicroformat "h-card"
+readPropertyName ∷ T.Text → (T.Text, T.Text)
+readPropertyName x = (fromMaybe "p" $ headMay ps, T.intercalate "-" $ drop 1 ps)
+  where ps = T.splitOn "-" x
 
--- | Parses an element as h-card.
-extractCard ∷ Element → Card
-extractCard e = def { cardName              = extractPropertyL P "name" e
-                    , cardHonorificPrefix   = extractPropertyL P "honorific-prefix" e
-                    , cardGivenName         = extractPropertyL P "given-name" e
-                    , cardAdditionalName    = extractPropertyL P "additional-name" e
-                    , cardFamilyName        = extractPropertyL P "family-name" e
-                    , cardSortString        = extractPropertyL P "sort-string" e
-                    , cardHonorificSuffix   = extractPropertyL P "honorific-suffix" e
-                    , cardNickname          = extractPropertyL P "nickname" e
-                    , cardEmail             = extractPropertyL U "email" e
-                    , cardLogo              = extractPropertyL U "logo" e
-                    , cardPhoto             = extractPropertyL U "photo" e
-                    , cardUrl               = extractPropertyL U "url" e
-                    , cardUid               = extractPropertyL U "uid" e
-                    , cardCategory          = extractPropertyL P "category" e
-                    , cardAdr               = filter (/= AdrAdr def) $
-                                                (AdrAdr . extractAdr $ e) :
-                                                     (TextAdr <$> extractPropertyL P "adr" e)
-                                                  ++ ((AdrAdr . extractAdr) <$> findPropertyMicroformat e "p-adr" "h-adr")
-                    , cardTel               = extractPropertyL P "tel" e
-                    , cardNote              = extractPropertyL P "note" e
-                    , cardBday              = extractPropertyDt "bday" e
-                    , cardKey               = extractPropertyL U "key" e
-                    , cardOrg               = filter (/= CardCard def) $
-                                                   (TextCard <$> extractPropertyL P "org" e)
-                                                ++ ((CardCard . extractCard) <$> findPropertyMicroformat e "p-org" "h-card")
-                    , cardJobTitle          = extractPropertyL P "job-title" e
-                    , cardRole              = extractPropertyL P "role" e
-                    , cardImpp              = extractPropertyL U "impp" e
-                    , cardSex               = extractPropertyL P "sex" e
-                    , cardGenderIdentity    = extractPropertyL P "gender-identity" e
-                    , cardAnniversary       = extractPropertyDt "anniversary" e }
+extractProperty ∷ Mf2ParserSettings → T.Text → Element → Value
+extractProperty _ "p"  e = fromMaybe Null $ String <$> extractP e
+extractProperty s "u"  e = fromMaybe Null $ String <$> case extractU e of
+                                                         Just (u, True) → Just $ resolveUrl s u
+                                                         Just (t, False) → Just t
+                                                         Nothing → Nothing
+extractProperty _ "dt" e = fromMaybe Null $ String <$> extractDt e
+extractProperty s "e"  e = object [ "html" .= getProcessedInnerHtml (htmlMode s) e, "value" .= getInnerTextRaw e ]
+extractProperty _ _    _ = Null
 
+-- lens-aeson's 'key' doesn't add new keys :-(
 
--- | Parses all h-cite entries inside of an element.
-parseCite ∷ HtmlContentMode → Element → [Cite]
-parseCite m = map (extractCite m) . findCite
+addValue ∷ T.Text → Value → Value → Value
+addValue "p" v@(Object o) f = Object $ HMS.insert "value" (fromMaybe f $ v ^? key "properties" . key "name" . nth 0) o
+addValue "e" (Object o)   f = Object $ HMS.insert "value" (fromMaybe Null $ f ^? key "value") $ HMS.insert "html" (fromMaybe Null $ f ^? key "html") o
+addValue "u" v@(Object o) f = Object $ HMS.insert "value" (fromMaybe f $ v ^? key "properties" . key "url" . nth 0) o
+addValue _   (Object o)   f = Object $ HMS.insert "value" f o
+addValue _   x            _ = x
 
--- | Finds all h-cite elements inside of an element.
-findCite ∷ Element → [Element]
-findCite = findMicroformat "h-cite"
+addImpliedProperties ∷ Mf2ParserSettings → Element → Value → Value
+addImpliedProperties settings e v@(Object o) = Object $ addIfNull "photo" "photo" resolveUrl' $ addIfNull "url" "url" resolveUrl' $ addIfNull "name" "name" id o
+  where addIfNull nameJ nameH f obj = if null $ v ^? key nameJ then HMS.insert nameJ (singleton $ f <$> implyProperty nameH e) obj else obj
+        singleton x = fromMaybe Null $ (Array . V.singleton . String) <$> x
+        resolveUrl' = resolveUrl settings
+addImpliedProperties _ _ v = v
 
--- | Parses an element as h-cite.
-extractCite ∷ HtmlContentMode → Element → Cite
-extractCite m e = def { citeName        = extractPropertyL P "name" e
-                      , citePublished   = extractPropertyDt "published" e
-                      , citeAuthor      = filter (/= CardCard def) $
-                                               (TextCard <$> extractPropertyL P "author" e)
-                                            ++ (CardCard . extractCard <$> findPropertyMicroformat e "p-author" "h-card")
-                      , citeUrl         = extractPropertyL U "url" e
-                      , citeUid         = extractPropertyL U "uid" e
-                      , citePublication = extractPropertyL P "publication" e
-                      , citeAccessed    = extractPropertyDt "accessed" e
-                      , citeContent     = TextContent <$> processContent m e }
+removePropertiesOfNestedMicroformats ∷ [Element] → [Element] → [Element]
+removePropertiesOfNestedMicroformats nmf2s = filter (not . isNested)
+    where isNested e = any (\e' → e `elem` filter (/= e') (e' ^.. entire)) nmf2s
 
+parseProperty ∷ Mf2ParserSettings → Element → [Pair]
+parseProperty settings e =
+  let propNames = groupBy' snd $ map readPropertyName $ filter isPropertyClass $ classes e
+      extractPropertyValue (t, _) = extractProperty settings t e in
+  if any isMf2Class $ classes e
+     then map (\(n, ts) → n .= [ addValue (fst $ head ts) (parseH settings e) (extractPropertyValue $ head ts) ]) propNames
+     else map (\(n, ts) → n .= map extractPropertyValue ts) propNames
 
--- | Parses all h-entry entries inside of an element.
-parseEntry ∷ HtmlContentMode → Element → [Entry]
-parseEntry m = map (extractEntry m) . findEntry
+parseH ∷ Mf2ParserSettings → Element → Value
+parseH settings e =
+  object $ filter (\(n, v) → not (emptyVal v) || n == "properties") [
+      "type"       .= filter isMf2Class (classes e)
+    , "properties" .= properties
+    , "children"   .= childrenMf2
+    , "shape"      .= fromMaybe Null (String <$> e ^. el "area" . attribute "shape")
+    , "coords"     .= fromMaybe Null (String <$> e ^. el "area" . attribute "coords") ]
+  where childrenMf2 = map ((\x → addValue "p" x Null) . parseH settings) $ filter (not . isProperty) $ deduplicateElements allMf2Descendants
+        allMf2Descendants = filter (/= e) $ e ^.. entire . mf2Elements
+        -- we have to do all of this because multiple elements can become multiple properties (with overlap)
+        properties = Object $ HMS.filter (not . emptyVal) properties'
+        (Object properties') = addImpliedProperties settings e $ object $ map mergeProps $ groupBy' fst properties''
+        properties'' = concatMap (parseProperty settings) $ removePropertiesOfNestedMicroformats allMf2Descendants $ filter (/= e) $ e ^.. entire . propertyElements
+        mergeProps (n, vs) = (n, Array $ V.concat $ reverse $ map (extractVector . snd) vs)
+        extractVector (Array v) = v
+        extractVector _ = V.empty
 
--- | Finds all h-entry elements inside of an element.
-findEntry ∷ Element → [Element]
-findEntry = findMicroformat "h-entry"
+-- | Parses Microformats 2 from an HTML Element into a JSON Value.
+parseMf2 ∷ Mf2ParserSettings → Element → Value
+parseMf2 settings rootEl = object [ "items" .= items, "rels" .= rels, "rel-urls" .= relUrls ]
+  where items = map (parseH settings') $ deduplicateElements $ rootEl' ^.. entire . mf2Elements
+        rels = object $ map (\(r, es) → r .= map snd es) $ groupBy' fst $ expandSnd $ map (\e → (T.split isSpace (e ^. attr "rel"), resolveUrl settings' $ e ^. attr "href")) linkEls
+        relUrls = object $ map relUrlObject linkEls
+        relUrlObject e = resolveUrl settings' (e ^. attr "href") .= object (filter (not . emptyVal . snd) [
+            "rels" .= T.split isSpace (e ^. attr "rel")
+          , "text" .= fromMaybe Null (String <$> getInnerTextWithImgs e)
+          , linkAttr "type" "type" e
+          , linkAttr "media" "media" e
+          , linkAttr "hreflang" "hreflang" e ])
+        linkAttr nameJ nameH e = nameJ .= fromMaybe Null (String <$> e ^. attribute nameH)
+        linkEls = filter (not . null . (^. attribute "href")) $ filter (not . null . (^. attribute "rel")) $ rootEl' ^.. entire . els [ "a", "link" ]
+        -- Obligatory WTF comment about base[href] being relative to the URI the page was requested from! <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base>
+        settings' = settings { baseUri = case (baseUri settings, parseURIReference =<< T.unpack <$> (rootEl' ^. entire . el "base" . attribute "href")) of
+                                           (Just sU, Just tU) → Just (tU `relativeTo` sU)
+                                           (Just sU, Nothing) → Just sU
+                                           (Nothing, Just tU) → Just tU
+                                           (Nothing, Nothing) → Nothing }
+        rootEl' = preprocessHtml rootEl
 
--- | Parses an element as h-entry.
-extractEntry ∷ HtmlContentMode → Element → Entry
-extractEntry m e = def { entryName        = extractPropertyL P "name" e
-                       , entrySummary     = extractPropertyL P "summary" e
-                       , entryContent     = TextContent <$> processContent m e 
-                       , entryPublished   = extractPropertyDt "published" e
-                       , entryUpdated     = extractPropertyDt "updated" e
-                       , entryAuthor      = filter (/= CardCard def) $
-                                                (TextCard <$> extractPropertyL P "author" e)
-                                             ++ (CardCard . extractCard <$> findPropertyMicroformat e "p-author" "h-card")
-                       , entryCategory    = extractPropertyL P "category" e
-                       , entryUrl         = extractPropertyL U "url" e
-                       , entryUid         = extractPropertyL U "uid" e
-                       , entryLocation    = filter ((/= CardLoc def) .&&. (/= AdrLoc def) .&&. (/= GeoLoc def)) $
-                                                 (CardLoc . extractCard <$> findPropertyMicroformat e "p-location" "h-card")
-                                              ++ (AdrLoc  . extractAdr <$> findPropertyMicroformat e "p-location" "h-adr")
-                                              ++ (GeoLoc  . extractGeo <$> findPropertyMicroformat e "p-location" "h-geo")
-                       , entryComments    = filter (/= CiteEntry def) $
-                                             (CiteEntry . extractCite m <$> findPropertyMicroformat e "p-comment" "h-cite")
-                       , entrySyndication = extractPropertyL U "syndication" e
-                       , entryInReplyTo   = UrlEntry <$> extractPropertyL U "in-reply-to" e
-                       , entryLikeOf      = UrlEntry <$> extractPropertyL U "like-of" e
-                       , entryRepostOf    = UrlEntry <$> extractPropertyL U "repost-of" e }
+resolveUrl ∷ Mf2ParserSettings → T.Text → T.Text
+resolveUrl settings t = case parseURIReference $ T.unpack t of
+                          Just u → T.pack $ uriToString id (u `relativeTo` fromMaybe nullURI (baseUri settings)) ""
+                          Nothing → t
 
-processContent ∷ HtmlContentMode → Element → [LT.Text]
-processContent Unsafe   = extractPropertyContent getAllHtml E "content"
-processContent Strip    = extractPropertyContent getAllText E "content"
-processContent Escape   = map (LT.replace "<" "&lt;" . LT.replace ">" "&gt;" . LT.replace "&" "&amp;") . extractPropertyContent getAllHtml E "content"
-processContent Sanitize = extractPropertyContent getAllHtmlSanitized E "content"
+preprocessHtml ∷ Element → Element
+preprocessHtml (Element n as ns) = Element (lowerName n) as $ map preprocessChildren $ filter (not . isTemplate) ns
+  where isTemplate (NodeElement (Element (Name "template" _ _) _ _)) = True
+        isTemplate _ = False
+        preprocessChildren (NodeElement e) = NodeElement $ preprocessHtml e
+        preprocessChildren x = x
+        lowerName (Name txt x y) = Name (T.toLower txt) x y
diff --git a/library/Data/Microformats2/Parser/HtmlUtil.hs b/library/Data/Microformats2/Parser/HtmlUtil.hs
new file mode 100644
--- /dev/null
+++ b/library/Data/Microformats2/Parser/HtmlUtil.hs
@@ -0,0 +1,89 @@
+{-# LANGUAGE OverloadedStrings, UnicodeSyntax #-}
+{-# LANGUAGE CPP, RankNTypes #-}
+
+module Data.Microformats2.Parser.HtmlUtil (
+  HtmlContentMode (..)
+, getInnerHtml
+, getInnerHtmlSanitized
+, getInnerTextRaw
+, getInnerTextWithImgs
+, getProcessedInnerHtml
+, deduplicateElements
+) where
+
+#if __GLASGOW_HASKELL__ < 709
+import           Control.Applicative
+#endif
+import qualified Data.Map as M
+import qualified Data.Text as T
+import qualified Data.Text.Lazy as TL
+import           Data.Text (Text)
+import           Data.Foldable (asum)
+import           Data.Maybe
+import           Text.Blaze
+import           Text.Blaze.Renderer.Text
+import           Text.HTML.SanitizeXSS
+import           Text.XML.Lens hiding (re)
+import           Data.Microformats2.Parser.Util
+
+getPrism ∷ Prism' Node Text → Element → Maybe Text
+getPrism t e = Just . T.strip <$> T.concat $ e ^.. nodes . traverse . t
+
+_InnerHtml ∷ Prism' Node Text
+_InnerHtml = prism' NodeContent $ \s → case s of
+  NodeContent c → Just $ collapseWhitespace c
+  NodeElement e → Just . TL.toStrict . renderMarkup . toMarkup $ e
+  _ → Nothing
+
+getInnerHtml ∷ Element → Maybe Text
+getInnerHtml = getPrism _InnerHtml
+
+sanitizeAttrs ∷ Element → Element
+sanitizeAttrs e = e { elementAttributes = M.fromList $ map wrapName $ mapMaybe modify $ M.toList $ elementAttributes e }
+  where modify (Name n _ _, val) = sanitizeAttribute (n, val)
+        wrapName (n, val) = (Name n Nothing Nothing, val)
+
+_InnerHtmlSanitized ∷ Prism' Node Text
+_InnerHtmlSanitized = prism' NodeContent $ \s → case s of
+  NodeContent c → Just $ collapseWhitespace c
+  NodeElement e → if' (safeTagName $ nameLocalName (elementName e)) $
+                    Just . TL.toStrict . renderMarkup . toMarkup $ sanitizeAttrs e
+  _ → Nothing
+
+getInnerHtmlSanitized ∷ Element → Maybe Text
+getInnerHtmlSanitized = getPrism _InnerHtmlSanitized
+
+_InnerTextRaw ∷ Prism' Node Text
+_InnerTextRaw = prism' NodeContent $ \s → case s of
+  NodeContent c → Just . collapseWhitespace $ c
+  NodeElement e → Just . collapseWhitespace . TL.toStrict . renderMarkup . contents . toMarkup $ e
+  _ → Nothing
+
+_InnerTextWithImgs ∷ Prism' Node Text
+_InnerTextWithImgs = prism' NodeContent $ \s → case s of
+  NodeContent c → Just $ collapseWhitespace c
+  NodeElement e → if nameLocalName (elementName e) == "img"
+                    then asum [ e ^. attribute "alt", e ^. attribute "src" ]
+                    else Just . collapseWhitespace . TL.toStrict . renderMarkup . contents . toMarkup $ e
+  _ → Nothing
+
+getInnerTextRaw ∷ Element → Maybe Text
+getInnerTextRaw e = unless' (txt == Just "") txt
+  where txt = getPrism _InnerTextRaw e
+
+getInnerTextWithImgs ∷ Element → Maybe Text
+getInnerTextWithImgs e = unless' (txt == Just "") txt
+  where txt = getPrism _InnerTextWithImgs e
+
+data HtmlContentMode = Unsafe | Escape | Sanitize
+  deriving (Show, Eq)
+
+getProcessedInnerHtml ∷ HtmlContentMode → Element → Maybe Text
+getProcessedInnerHtml Unsafe   e = getInnerHtml e
+getProcessedInnerHtml Escape   e = (T.replace "<" "&lt;" . T.replace ">" "&gt;" . T.replace "&" "&amp;") <$> getInnerHtml e
+getProcessedInnerHtml Sanitize e = getInnerHtmlSanitized e
+
+deduplicateElements ∷ [Element] → [Element]
+deduplicateElements es = filter (not . isNested) es
+  where isNested e = any (\e' → e `elem` filter (/= e') (e' ^.. entire)) es
+        -- not the fastest function I guess...
diff --git a/library/Data/Microformats2/Parser/Internal.hs b/library/Data/Microformats2/Parser/Internal.hs
deleted file mode 100644
--- a/library/Data/Microformats2/Parser/Internal.hs
+++ /dev/null
@@ -1,222 +0,0 @@
-{-# LANGUAGE OverloadedStrings, QuasiQuotes, UnicodeSyntax #-}
-{-# LANGUAGE CPP, RankNTypes #-}
-
-module Data.Microformats2.Parser.Internal where
-
-#if __GLASGOW_HASKELL__ < 709
-import           Control.Applicative
-#endif
-import           Control.Monad (liftM)
-import qualified Data.Text as T
-import qualified Data.Text.Lazy as TL
-import           Data.Text (Text)
-import           Data.Foldable (asum)
-import qualified Data.Set as S
-import qualified Data.Map as M
-import           Data.Maybe
-import           Data.Time.Format
-import           Text.XML.Lens hiding (re)
-import           Text.HTML.SanitizeXSS
-import           Text.Blaze
-import           Text.Blaze.Renderer.Text
-import           Text.Regex.PCRE.Heavy
-import           Safe (readMay)
-
-if' ∷ Bool → Maybe a → Maybe a
-if' c x = if c then x else Nothing
-
-entireNotMicroformat ∷ Traversal' Element Element
-entireNotMicroformat f e@(Element _ _ ns) = com <$> f e <*> traverse (_Element (entireNotMicroformat f)) (filter notMf ns)
-  where com (Element n a _) = Element n a
-        notMf (NodeElement (Element _ a _)) = not $ (fromMaybe "" $ lookup "class" $ map unwrapName $ M.toList a) ≈ [re|h-\w+|]
-        notMf _ = True
-        unwrapName (Name n _ _, val) = (n, val)
-
-hasOneClass ∷ [String] → Traversal' Element Element
-hasOneClass ns = attributeSatisfies "class" $ \a → any (\x → T.isInfixOf (T.pack x) a) ns
-
-hasClass ∷ String →  Traversal' Element Element
-hasClass n = attributeSatisfies "class" $ T.isInfixOf . T.pack $ n
-
-getOnlyChildren ∷ Element → [Element]
-getOnlyChildren e = if lengthOf plate e == 1 then e ^.. plate else []
-
-getOnlyChild ∷ Name → Element → Maybe Element
-getOnlyChild n e = if' (lengthOf plate e == 1) $ e ^? plate . el n
-
-getOnlyOfType ∷ Name → Element → Maybe Element
-getOnlyOfType n e = if' (lengthOf (plate . el n) e == 1) $ e ^? plate . el n
-
-els ∷ [Name] → Traversal' Element Element
-els ns f s = if elementName s `elem` ns then f s else pure s
-
-removeWhitespace ∷ Text → Text
-removeWhitespace = gsub [re|(\s+|&nbsp;)|] (" " ∷ String) -- lol vim |||||||
-
-getPrism ∷ Prism' Node Text → Element → Maybe Text
-getPrism t e = Just . T.strip <$> T.concat $ e ^.. nodes . traverse . t
-
-_InnerHtml ∷ Prism' Node Text
-_InnerHtml = prism' NodeContent $ \s → case s of
-  NodeContent c → Just $ removeWhitespace c
-  NodeElement e → Just . TL.toStrict . renderMarkup . toMarkup $ e
-  _ → Nothing
-
-getAllHtml ∷ Element → Maybe Text
-getAllHtml = getPrism _InnerHtml
-
--- XXX: https://github.com/yesodweb/haskell-xss-sanitize/issues/11
-safeTagName ∷ Text → Bool
-safeTagName = (`S.member` S.fromList [ "a", "b", "abbr", "acronym", "br", "ul", "li", "ol", "span", "strong", "em",
-                                       "i", "q", "img", "time", "strike", "kbd", "dl", "dt", "pre", "p", "blockquote",
-                                       "code", "cite", "figure", "figcaption", "big", "dfn" ])
-
-sanitizeAttrs ∷ Element → Element
-sanitizeAttrs e = e { elementAttributes = M.fromList $ map wrapName $ mapMaybe modify $ M.toList $ elementAttributes e }
-  where modify (Name n _ _, val) = sanitizeAttribute (n, val)
-        wrapName (n, val) = (Name n Nothing Nothing, val)
-
-_InnerHtmlSanitized ∷ Prism' Node Text
-_InnerHtmlSanitized = prism' NodeContent $ \s → case s of
-  NodeContent c → Just $ removeWhitespace c
-  NodeElement e → if not $ safeTagName $ nameLocalName (elementName e)
-                       then Nothing
-                       else Just . TL.toStrict . renderMarkup . toMarkup $ sanitizeAttrs e
-  _ → Nothing
-
-getAllHtmlSanitized ∷ Element → Maybe Text
-getAllHtmlSanitized = getPrism _InnerHtmlSanitized
-
-_InnerText ∷ Prism' Node Text
-_InnerText = prism' NodeContent $ \s → case s of
-  NodeContent c → Just $ removeWhitespace c
-  NodeElement e → if nameLocalName (elementName e) == "img"
-                       then e ^. el "img" . attribute "alt"
-                       else Just . removeWhitespace . TL.toStrict . renderMarkup . contents . toMarkup $ e
-  _ → Nothing
-
-getAllText ∷ Element → Maybe Text
-getAllText = getPrism _InnerText
-
-getText ∷ Element → Maybe Text
-getText e = if T.null $ fromMaybe "" txt then Nothing else txt
-  where txt = listToMaybe $ T.strip <$> e ^.. entire . nodes . traverse . _Content
-
-getAbbrTitle ∷ Element → Maybe Text
-getAbbrTitle e = e ^. el "abbr" . attribute "title"
-
-getDataInputValue ∷ Element → Maybe Text
-getDataInputValue e = e ^. els ["data", "input"] . attribute "value"
-
-getImgSrc ∷ Element → Maybe Text
-getImgSrc e = e ^. el "img" . attribute "src"
-
-getObjectData ∷ Element → Maybe Text
-getObjectData e = e ^. el "object" . attribute "data"
-
-getImgAreaAlt ∷ Element → Maybe Text
-getImgAreaAlt e = e ^. els ["img", "area"] . attribute "alt"
-
-getAAreaHref ∷ Element → Maybe Text
-getAAreaHref e = e ^. els ["a", "area"] . attribute "href"
-
-getImgAudioVideoSourceSrc ∷ Element → Maybe Text
-getImgAudioVideoSourceSrc e = e ^. els ["img", "audio", "video", "source"] . attribute "src"
-
-getTimeInsDelDatetime ∷ Element → Maybe Text
-getTimeInsDelDatetime e = e ^. els ["time", "ins", "del"] . attribute "datetime"
-
-getOnlyChildImgAreaAlt ∷ Element → Maybe Text
-getOnlyChildImgAreaAlt e = (^. attribute "alt") =<< asum (getOnlyChild <$> [ "img", "area" ] <*> pure e)
-
-getOnlyChildAbbrTitle ∷ Element → Maybe Text
-getOnlyChildAbbrTitle e = (^. attribute "title") =<< getOnlyChild "abbr" e
-
-getOnlyOfTypeImgSrc ∷ Element → Maybe Text
-getOnlyOfTypeImgSrc e = (^. attribute "src") =<< getOnlyOfType "img" e
-
-getOnlyOfTypeObjectData ∷ Element → Maybe Text
-getOnlyOfTypeObjectData e = (^. attribute "data") =<< getOnlyOfType "object" e
-
-getOnlyOfTypeAAreaHref ∷ Element → Maybe Text
-getOnlyOfTypeAAreaHref e = (^. attribute "href") =<< asum (getOnlyOfType <$> [ "a", "area" ] <*> pure e)
-
-extractValue ∷ Element → Maybe Text
-extractValue e = asum $ [ getAbbrTitle, getDataInputValue, getImgAreaAlt, getAllText ] <*> pure e
-
-extractValueTitle ∷ Element → Maybe Text
-extractValueTitle e = if' (isJust $ e ^? hasClass "value-title") $ e ^. attribute "title"
-
-extractValueClassPattern ∷ [Element → Maybe Text] → Element → Maybe Text
-extractValueClassPattern fs e = if' (isJust $ e ^? valueParts) extractValueParts
-  where extractValueParts   = Just . T.concat . catMaybes $ e ^.. valueParts . to extractValuePart
-        extractValuePart e' = asum $ fs <*> pure e'
-        valueParts          ∷ Applicative f => (Element → f Element) → Element → f Element
-        valueParts          = entire . hasOneClass ["value", "value-title"]
-
-className ∷ PropType → String → String
-className P  n = "p-" ++ n
-className U  n = "u-" ++ n
-className Dt n = "dt-" ++ n
-className E  n = "e-" ++ n
-
-findProperty ∷ Element → String → [Element]
-findProperty e n = filter (/= e) $ e ^.. entireNotMicroformat . hasClass n
-
-findPropertyMicroformat ∷ Element → String → String → [Element]
-findPropertyMicroformat e n s = filter (/= e) $ e ^.. entire . hasClass n . hasClass s
-
-data PropType = P | U | Dt | E
-
-extract ∷ [Element → Maybe Text] → Element → [Text]
-extract ps e = catMaybes $ [ \x -> asum $ ps <*> pure x ] <*> pure e
-
-extractProperty ∷ PropType → String → Element → [Text]
-extractProperty P n e' =
-  findProperty e' (className P n) >>=
-  extract [ extractValueClassPattern [extractValueTitle, extractValue]
-          , extractValue ]
-extractProperty U n e' =
-  findProperty e' (className U n) >>=
-  extract [ getAAreaHref, getImgAudioVideoSourceSrc
-          , extractValueClassPattern [extractValueTitle, extractValue]
-          , getAbbrTitle, getDataInputValue, getAllText ]
-extractProperty Dt n e' =
-  findProperty e' (className Dt n) >>=
-  extract (extractValueClassPattern ms : ms ++ [getAllText])
-  where ms = [ getTimeInsDelDatetime, getAbbrTitle, getDataInputValue ]
-extractProperty E n e' = findProperty e' (className E n) >>= liftM maybeToList getAllHtml
-
-extractPropertyL ∷ PropType → String → Element → [TL.Text]
-extractPropertyL t n e = TL.fromStrict <$> if null extracted then maybeToList $ implyProperty t n e else extracted
-  where extracted = extractProperty t n e
-
-extractPropertyR ∷ Read α ⇒ PropType → String → Element → [α]
-extractPropertyR t n e = catMaybes $ readMay <$> T.unpack <$> extractProperty t n e
-
-extractPropertyDt ∷ ParseTime α ⇒ String → Element → [α]
-extractPropertyDt n e = catMaybes $ readISO <$> T.unpack <$> extractProperty Dt n e
-  where readISO x = asum $ map ($ x) [ isoParse $ Just "%H:%M:%S%z"
-                                     , isoParse $ Just "%H:%M:%SZ"
-                                     , isoParse $ Just "%H:%M:%S"
-                                     , isoParse $ Just "%H:%M"
-                                     , parseTimeD "%G-W%V-%u"
-                                     , parseTimeD "%G-W%V"
-                                     , isoParse Nothing ]
-        isoParse = parseTimeD . iso8601DateFormat
-        parseTimeD = parseTimeM True defaultTimeLocale 
-
-extractPropertyContent ∷ (Element → Maybe Text) → PropType → String → Element → [TL.Text]
-extractPropertyContent ex t n e = findProperty e (className t n) >>= extract [ ex ] >>= return . TL.fromStrict
-
-implyProperty ∷ PropType → String → Element → Maybe Text
-implyProperty P "name"  e = asum $ [ getImgAreaAlt, getAbbrTitle
-                                   , getOnlyChildImgAreaAlt, getOnlyChildAbbrTitle
-                                   , \e' -> asum $ [ getOnlyChildImgAreaAlt, getOnlyChildAbbrTitle ] <*> getOnlyChildren e'
-                                   , getText ] <*> pure e
-implyProperty U "photo" e = asum $ [ getImgSrc, getObjectData
-                                   , getOnlyOfTypeImgSrc, getOnlyOfTypeObjectData
-                                   , \e' -> asum $ [ getOnlyOfTypeImgSrc, getOnlyOfTypeObjectData ] <*> getOnlyChildren e'
-                                   ] <*> pure e
-implyProperty U "url"   e = asum $ [ getAAreaHref, getOnlyOfTypeAAreaHref ] <*> pure e
-implyProperty _ _ _ = Nothing
diff --git a/library/Data/Microformats2/Parser/Property.hs b/library/Data/Microformats2/Parser/Property.hs
new file mode 100644
--- /dev/null
+++ b/library/Data/Microformats2/Parser/Property.hs
@@ -0,0 +1,113 @@
+{-# LANGUAGE OverloadedStrings, QuasiQuotes, UnicodeSyntax #-}
+{-# LANGUAGE CPP, RankNTypes, TupleSections #-}
+
+module Data.Microformats2.Parser.Property where
+
+#if __GLASGOW_HASKELL__ < 709
+import           Control.Applicative
+#endif
+import qualified Data.Text as T
+import           Data.Text (Text)
+import           Data.Char (isSpace)
+import           Data.Foldable (asum)
+import qualified Data.Map as M
+import           Data.Maybe
+import           Text.XML.Lens hiding (re)
+import           Data.Microformats2.Parser.HtmlUtil
+import           Data.Microformats2.Parser.Util
+
+unwrapName ∷ (Name, a) → (Text, a)
+unwrapName (Name n _ _, val) = (n, val)
+
+classes ∷ Element → [Text]
+classes (Element _ as _) = T.split isSpace . fromMaybe "" . lookup "class" . map unwrapName . M.toList $ as
+
+isPClass, isUClass, isEClass, isDtClass, isPropertyClass, isMf2Class ∷ Text → Bool
+isPClass          = T.isPrefixOf "p-"
+isUClass          = T.isPrefixOf "u-"
+isEClass          = T.isPrefixOf "e-"
+isDtClass         = T.isPrefixOf "dt-"
+isPropertyClass x = isPClass x || isUClass x || isEClass x || isDtClass x
+isMf2Class        = T.isPrefixOf "h-"
+
+isProperty ∷ Element → Bool
+isProperty = any isPropertyClass . classes
+
+propertyElements ∷ Traversal' Element Element
+propertyElements = attributeSatisfies "class" $ any isPropertyClass . T.split isSpace
+
+hasOneClass ∷ [String] → Traversal' Element Element
+hasOneClass ns = attributeSatisfies "class" $ \a → any (\x → (T.pack x) `elem` (T.split isSpace a)) ns
+
+hasClass ∷ String → Traversal' Element Element
+hasClass n = attributeSatisfies "class" $ \a → (T.pack n) `elem` (T.split isSpace a)
+
+getOnlyChildren ∷ Element → [Element]
+getOnlyChildren e = if lengthOf plate e == 1 then e ^.. plate else []
+
+getOnlyChild, getOnlyOfType ∷ Name → Element → Maybe Element
+getOnlyChild n e = if' (fromMaybe False $ not <$> isProperty <$> r) $ r
+  where r = if' (lengthOf plate e == 1) $ e ^? plate . el n
+getOnlyOfType n e = if' (fromMaybe False $ not <$> isProperty <$> r) $ r
+  where r = if' (lengthOf (plate . el n) e == 1) $ e ^? plate . el n
+
+els ∷ [Name] → Traversal' Element Element
+els ns f s = if elementName s `elem` ns then f s else pure s
+
+getAbbrTitle, getDataInputValue, getImgSrc, getObjectData, getImgAreaAlt, getAAreaHref, getImgAudioVideoSourceSrc, getTimeInsDelDatetime, getOnlyChildImgAreaAlt, \
+getOnlyChildAbbrTitle, getOnlyOfTypeImgSrc, getOnlyOfTypeObjectData, getOnlyOfTypeAAreaHref, extractValue, extractValueTitle ∷ Element → Maybe Text
+getAbbrTitle              e = e ^. el "abbr" . attribute "title"
+getDataInputValue         e = e ^. els ["data", "input"] . attribute "value"
+getImgSrc                 e = e ^. el "img" . attribute "src"
+getObjectData             e = e ^. el "object" . attribute "data"
+getImgAreaAlt             e = e ^. els ["img", "area"] . attribute "alt"
+getAAreaHref              e = e ^. els ["a", "area"] . attribute "href"
+getImgAudioVideoSourceSrc e = e ^. els ["img", "audio", "video", "source"] . attribute "src"
+getTimeInsDelDatetime     e = e ^. els ["time", "ins", "del"] . attribute "datetime"
+getOnlyChildImgAreaAlt    e = (^. attribute "alt") =<< asum (getOnlyChild <$> [ "img", "area" ] <*> pure e)
+getOnlyChildAbbrTitle     e = (^. attribute "title") =<< getOnlyChild "abbr" e
+getOnlyOfTypeImgSrc       e = (^. attribute "src") =<< getOnlyOfType "img" e
+getOnlyOfTypeObjectData   e = (^. attribute "data") =<< getOnlyOfType "object" e
+getOnlyOfTypeAAreaHref    e = (^. attribute "href") =<< asum (getOnlyOfType <$> [ "a", "area" ] <*> pure e)
+extractValue              e = asum $ [ getAbbrTitle, getDataInputValue, getImgAreaAlt, getInnerTextRaw ] <*> pure e
+extractValueTitle         e = if' (isJust $ e ^? hasClass "value-title") $ e ^. attribute "title"
+
+extractValueClassPattern ∷ [Element → Maybe Text] → Element → Maybe Text
+extractValueClassPattern fs e = if' (isJust $ e ^? valueParts) extractValueParts
+  where extractValueParts   = Just . T.concat . catMaybes $ e ^.. valueParts . to extractValuePart
+        extractValuePart e' = asum $ fs <*> pure e'
+        valueParts          ∷ Applicative f => (Element → f Element) → Element → f Element
+        valueParts          = entire . hasOneClass ["value", "value-title"]
+
+extractP ∷ Element → Maybe Text
+extractP e =
+  asum $ [ extractValueClassPattern [extractValueTitle, extractValue]
+         , getAbbrTitle, getDataInputValue, getImgAreaAlt, getInnerTextWithImgs ] <*> pure e
+
+extractU ∷ Element
+         → Maybe (Text, Bool) -- ^ The Microformats 2 spec requires URL resolution only in some cases. The Bool here is whether you should resolve the result.
+extractU e =
+  asum $ [ (, True) <$> getAAreaHref e
+         , (, True) <$> getImgAudioVideoSourceSrc e
+         , (, True) <$> getObjectData e
+         , (, False) <$> extractValueClassPattern [extractValueTitle, extractValue] e
+         , (, False) <$> getAbbrTitle e
+         , (, False) <$> getDataInputValue e
+         , (, False) <$> getInnerTextRaw e ]
+
+extractDt ∷ Element → Maybe Text
+extractDt e =
+  asum $ (extractValueClassPattern ms : ms ++ [getInnerTextRaw]) <*> pure e
+  where ms = [ getTimeInsDelDatetime, getAbbrTitle, getDataInputValue ]
+
+implyProperty ∷ String → Element → Maybe Text
+implyProperty "name"  e = asum $ [ getImgAreaAlt, getAbbrTitle
+                                 , getOnlyChildImgAreaAlt, getOnlyChildAbbrTitle
+                                 , \e' -> asum $ [ getOnlyChildImgAreaAlt, getOnlyChildAbbrTitle ] <*> getOnlyChildren e'
+                                 , getInnerTextRaw ] <*> pure e
+implyProperty "photo" e = asum $ [ getImgSrc, getObjectData
+                                 , getOnlyOfTypeImgSrc, getOnlyOfTypeObjectData
+                                 , \e' -> asum $ [ getOnlyOfTypeImgSrc, getOnlyOfTypeObjectData ] <*> getOnlyChildren e'
+                                 ] <*> pure e
+implyProperty "url"   e = asum $ [ getAAreaHref, getOnlyOfTypeAAreaHref ] <*> pure e
+implyProperty _ _ = Nothing
diff --git a/library/Data/Microformats2/Parser/Util.hs b/library/Data/Microformats2/Parser/Util.hs
new file mode 100644
--- /dev/null
+++ b/library/Data/Microformats2/Parser/Util.hs
@@ -0,0 +1,45 @@
+{-# LANGUAGE OverloadedStrings, QuasiQuotes, UnicodeSyntax #-}
+{-# LANGUAGE CPP, TupleSections #-}
+
+module Data.Microformats2.Parser.Util where
+
+#if __GLASGOW_HASKELL__ < 709
+import           Control.Applicative
+#endif
+import           Data.Aeson
+import qualified Data.Text as T
+import qualified Data.Text.Lazy as TL
+import qualified Data.HashMap.Strict as HMS
+import qualified Data.Map as M
+import qualified Data.Vector as V
+import qualified Data.Foldable as F
+import           Text.Regex.PCRE.Heavy
+
+if' ∷ Bool → Maybe a → Maybe a
+if' c x = if c then x else Nothing
+
+unless' ∷ Bool → Maybe a → Maybe a
+unless' c x = if c then Nothing else x
+
+listToMaybeList ∷ [α] → Maybe [α]
+listToMaybeList l = unless' (null l) $ Just l
+
+stripQueryString ∷ TL.Text → TL.Text
+stripQueryString = TL.intercalate "" . take 1 . TL.splitOn "?" . TL.strip
+
+collapseWhitespace ∷ T.Text → T.Text
+collapseWhitespace = gsub [re|(\s+|&nbsp;)|] (" " ∷ String)
+
+emptyVal ∷ Value → Bool
+emptyVal (Object o) = HMS.null o
+emptyVal (Array v) = V.null v
+emptyVal (String s) = T.null s
+emptyVal Null = True
+emptyVal _ = False
+
+groupBy' ∷ (Ord β) ⇒ (α → β) → [α] → [(β, [α])]
+groupBy' f = M.toAscList . M.fromListWith (++) . map (\a → (f a, [a]))
+
+-- https://hackage.haskell.org/package/liquid-fixpoint-0.4.0.0/docs/src/Language-Fixpoint-Misc.html#expandSnd
+expandSnd ∷ Foldable φ ⇒ φ ([α], β) → [(α, β)]
+expandSnd = F.concatMap (\(xs, y) → (, y) <$> xs)
diff --git a/microformats2-parser.cabal b/microformats2-parser.cabal
--- a/microformats2-parser.cabal
+++ b/microformats2-parser.cabal
@@ -1,5 +1,5 @@
 name:            microformats2-parser
-version:         0.1.1
+version:         1.0.0
 synopsis:        A Microformats 2 parser.
 category:        Web
 homepage:        https://github.com/myfreeweb/microformats2-parser
@@ -22,22 +22,31 @@
 library
     build-depends:
         base >= 4.0.0.0 && < 5
+      , transformers
       , text
+      , bytestring
       , time
       , either
       , safe
       , containers
+      , unordered-containers
+      , vector
+      , aeson
+      , aeson-qq
+      , lens-aeson
       , data-default
-      , microformats2-types
       , html-conduit
       , xml-lens
+      , network-uri
       , blaze-markup
       , xss-sanitize
       , pcre-heavy
     default-language: Haskell2010
     exposed-modules:
         Data.Microformats2.Parser
-        Data.Microformats2.Parser.Internal
+        Data.Microformats2.Parser.Property
+        Data.Microformats2.Parser.HtmlUtil
+        Data.Microformats2.Parser.Util
     ghc-options: -Wall
     hs-source-dirs: library
 
@@ -48,14 +57,17 @@
       , warp
       , wai-extra
       , network
+      , network-uri
       , streaming-commons
       , stringable
+      , data-default
       , text
       , scotty
+      , aeson
+      , aeson-pretty
       , blaze-html
       , blaze-markup
       , microformats2-parser
-      , microformats2-types
     default-language: Haskell2010
     ghc-prof-options: -auto-all -prof
     ghc-options: -threaded -rtsopts -with-rtsopts=-N
@@ -65,12 +77,17 @@
 test-suite tests
     build-depends:
         base >= 4.0.0.0 && < 5
+      , mtl
       , time
+      , text
+      , bytestring
+      , network-uri
       , hspec
+      , hspec-expectations-pretty-diff
       , template-haskell
       , microformats2-parser
-      , microformats2-types
       , raw-strings-qq
+      , aeson-qq
       , data-default
       , html-conduit
       , xml-lens
@@ -80,5 +97,6 @@
     main-is: Spec.hs
     other-modules:
         Data.Microformats2.ParserSpec
-        Data.Microformats2.Parser.InternalSpec
+        Data.Microformats2.Parser.PropertySpec
+        Data.Microformats2.Parser.HtmlUtilSpec
     type: exitcode-stdio-1.0
diff --git a/test-suite/Data/Microformats2/Parser/HtmlUtilSpec.hs b/test-suite/Data/Microformats2/Parser/HtmlUtilSpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Data/Microformats2/Parser/HtmlUtilSpec.hs
@@ -0,0 +1,21 @@
+{-# LANGUAGE OverloadedStrings, UnicodeSyntax, QuasiQuotes #-}
+
+module Data.Microformats2.Parser.HtmlUtilSpec (spec) where
+
+import           Test.Hspec
+import           TestCommon
+import           Data.Microformats2.Parser.HtmlUtil
+
+spec ∷ Spec
+spec = do
+  describe "getInnerTextRaw" $ do
+    it "returns textContent without handling imgs" $ do
+      let txtraw = getInnerTextRaw . documentRoot . parseLBS
+      txtraw [xml|<div>This is <a href="">text content</a> <img src="/yo" alt="NOPE"> without any stuff.
+  	</div>|] `shouldBe` Just "This is text content  without any stuff."
+
+  describe "getInnerTextWithImgs" $ do
+    it "returns textContent with handling imgs" $ do
+      let txtraw = getInnerTextWithImgs . documentRoot . parseLBS
+      txtraw [xml|<div>This is <a href="">text content</a> <img src="/yo" alt="with an alt"> <img src="and-src">.
+    </div>|] `shouldBe` Just "This is text content with an alt and-src."
diff --git a/test-suite/Data/Microformats2/Parser/InternalSpec.hs b/test-suite/Data/Microformats2/Parser/InternalSpec.hs
deleted file mode 100644
--- a/test-suite/Data/Microformats2/Parser/InternalSpec.hs
+++ /dev/null
@@ -1,97 +0,0 @@
-{-# LANGUAGE OverloadedStrings, UnicodeSyntax, QuasiQuotes, CPP #-}
-
-module Data.Microformats2.Parser.InternalSpec (spec) where
-
-import           Test.Hspec
-import           TestCommon
-import           Text.HTML.DOM
-import           Text.XML.Lens (documentRoot)
-import           Data.Microformats2.Parser.Internal
-#if __GLASGOW_HASKELL__ < 709
-import           Control.Applicative
-#endif
-
-{-# ANN module ("HLint: ignore Redundant do"::String) #-}
-
-spec ∷ Spec
-spec = do
-  describe "extractProperty" $ do
-    it "parses p- properties" $ do
-      let nm = extractProperty P "name" . documentRoot . parseLBS
-      nm [xml|<div><span class="p-name">Hello Basic</span>|] `shouldBe` pure "Hello Basic"
-      nm [xml|<div><p><span class="p-name">Hello Nested</span>|] `shouldBe` pure "Hello Nested"
-      nm [xml|<div><abbr class="p-name" title="Hello Abbr">HA</abbr>|] `shouldBe` pure "Hello Abbr"
-      nm [xml|<div><abbr class="p-name">HA</abbr>|] `shouldBe` pure "HA"
-      nm [xml|<div><data class="p-name" value="Hello Data" />|] `shouldBe` pure "Hello Data"
-      nm [xml|<div><input class="p-name" value="Hello Input" />|] `shouldBe` pure "Hello Input"
-      nm [xml|<div><img class="p-name" alt="Hello Img" />|] `shouldBe` pure "Hello Img"
-      nm [xml|<div><area class="p-name" alt="Hello Area" />|] `shouldBe` pure "Hello Area"
-      nm [xml|<div><span class="p-name"> ignore <i class="value">Hello</i>  <img class="value" alt="ValuePattern" src="x.png"> </span>|] `shouldBe` pure "HelloValuePattern"
-      nm [xml|<div><span class="p-name"> ignore <em class="value-title" title="Hello">Hi</em>  <span class="value">Value-Title</span></span>|] `shouldBe` pure "HelloValue-Title"
-      nm [xml|<div><span class="p-name">  Hello <img alt="Span With Img" src="x.png"> </span>|] `shouldBe` pure "Hello Span With Img"
-      nm [xml|<div><span class="p-name"> <span class="value"> 	Hello 	
-  <img alt="Span With Img" src="x.png"> </span> 	<em class="value-title" title="&& Value Title">nope</em> </span>|] `shouldBe` pure "Hello Span With Img&& Value Title"
-
-    it "parses u- properties" $ do
-      let ur = extractProperty U "url" . documentRoot . parseLBS
-      ur [xml|<div><a class="u-url" href="/yo/a">link</a>|] `shouldBe` pure "/yo/a"
-      ur [xml|<div><area class="u-url" href="/yo/area"/>|] `shouldBe` pure "/yo/area"
-      ur [xml|<div><img class="u-url" src="/yo/img"/>|] `shouldBe` pure "/yo/img"
-      ur [xml|<div><audio class="u-url" src="/yo/audio"/>|] `shouldBe` pure "/yo/audio"
-      ur [xml|<div><video class="u-url" src="/yo/video"/>|] `shouldBe` pure "/yo/video"
-      ur [xml|<div><source class="u-url" src="/yo/source"/>|] `shouldBe` pure "/yo/source"
-      ur [xml|<div><span class="u-url"><b class=value>/yo</b><em class="value">/vcp</span>|] `shouldBe` pure "/yo/vcp"
-      ur [xml|<div><abbr class="u-url" title="/yo/abbr"/>|] `shouldBe` pure "/yo/abbr"
-      ur [xml|<div><data class="u-url" value="/yo/data"/>|] `shouldBe` pure "/yo/data"
-      ur [xml|<div><input class="u-url" value="/yo/input"/>|] `shouldBe` pure "/yo/input"
-      ur [xml|<div><span class="u-url">/yo/span</span>|] `shouldBe` pure "/yo/span"
-
-    it "parses dt- properties" $ do
-      let dt = extractProperty Dt "updated" . documentRoot . parseLBS
-      dt [xml|<div><time class="dt-updated" datetime="ti.me">someday</time>|] `shouldBe` pure "ti.me"
-      dt [xml|<div><ins class="dt-updated" datetime="i.ns">someday</ins>|] `shouldBe` pure "i.ns"
-      dt [xml|<div><del class="dt-updated" datetime="d.el">someday</del>|] `shouldBe` pure "d.el"
-      dt [xml|<div><abbr class="dt-updated" title="ab.br">AB</abbr>|] `shouldBe` pure "ab.br"
-      dt [xml|<div><data class="dt-updated" value="da.ta"/>|] `shouldBe` pure "da.ta"
-      dt [xml|<div><input class="dt-updated" value="i.np.ut"/>|] `shouldBe` pure "i.np.ut"
-      dt [xml|<div><span class="dt-updated">
-            <abbr class="value" title="vcp">VCP</abbr>
-            <time class="value" datetime="ti">TIME</time>
-            <ins class="value" datetime="me">lol</time>
-          </span>|] `shouldBe` pure "vcptime"
-      dt [xml|<div><span class="dt-updated">date</span>|] `shouldBe` pure "date"
-
-    it "parses e- properties" $ do
-      let ct = extractProperty E "content" . documentRoot . parseLBS
-      ct [xml|<div><div class="e-content"><em>hello html</em>!</div>|] `shouldBe` pure "<em>hello html</em>!"
-
-  describe "implyProperty" $ do
-    it "parses implied p-name" $ do
-      let nm = implyProperty P "name" . documentRoot . parseLBS
-      nm [xml|<img class="h-blah" alt="Hello Img!">|] `shouldBe` pure "Hello Img!"
-      nm [xml|<abbr class="h-blah" title="Hello Abbr!">HA</abbr>|] `shouldBe` pure "Hello Abbr!"
-      nm [xml|<p class="h-blah"><img alt="Hello Only Img!"></p>|] `shouldBe` pure "Hello Only Img!"
-      nm [xml|<p class="h-blah"><img alt="DOING"><img alt="IT"><img alt="WRONG">Goodbye Img!</p>|] `shouldBe` pure "Goodbye Img!"
-      nm [xml|<p class="h-blah"><abbr title="Hello Only Abbr!">HOA</abbr></p>|] `shouldBe` pure "Hello Only Abbr!"
-      nm [xml|<p class="h-blah"><abbr title="DOING"/><abbr title="IT"/><abbr title="WRONG"/>Goodbye Abbr!</p>|] `shouldBe` pure "Goodbye Abbr!"
-      nm [xml|<p class="h-blah"><em><img alt="Hello Only Nested Img!"></p>|] `shouldBe` pure "Hello Only Nested Img!"
-      nm [xml|<p class="h-blah"><em><img alt="DOING"><img alt="WRONG">Goodbye Nested Img!</p>|] `shouldBe` pure "Goodbye Nested Img!"
-      nm [xml|<p class="h-blah"><em><abbr title="Hello Only Nested Abbr!">HOA</abbr></p>|] `shouldBe` pure "Hello Only Nested Abbr!"
-      nm [xml|<p class="h-blah"><em><abbr title="DOING"/><abbr title="WRONG"/>Goodbye Nested Abbr!</p>|] `shouldBe` pure "Goodbye Nested Abbr!"
-      nm [xml|<p class="h-blah">Hello Text!</p>|] `shouldBe` pure "Hello Text!"
-
-    it "parses implied u-photo" $ do
-      let ph = implyProperty U "photo" . documentRoot . parseLBS
-      ph [xml|<img class="h-blah" src="selfie.png">|] `shouldBe` pure "selfie.png"
-      ph [xml|<object class="h-blah" data="art.svg">|] `shouldBe` pure "art.svg"
-      ph [xml|<div class="h-blah"><img src="selfie.png"/><em>yo</em>|] `shouldBe` pure "selfie.png"
-      ph [xml|<div class="h-blah"><object data="art.svg"/><em>yo</em>|] `shouldBe` pure "art.svg"
-      ph [xml|<div class="h-blah"><p class="onlychild"><img src="selfie.png"/><em>yo</em>|] `shouldBe` pure "selfie.png"
-      ph [xml|<div class="h-blah"><p class="onlychild"><object data="art.svg"/><em>yo</em>|] `shouldBe` pure "art.svg"
-
-    it "parses implied u-url" $ do
-      let ur = implyProperty U "url" . documentRoot . parseLBS
-      ur [xml|<a class="h-blah" href="/hello/a">|] `shouldBe` pure "/hello/a"
-      ur [xml|<area class="h-blah" href="/hello/area">|] `shouldBe` pure "/hello/area"
-      ur [xml|<div class="h-blah"><em>what</em><a href="/hello/n/a">|] `shouldBe` pure "/hello/n/a"
-      ur [xml|<div class="h-blah"><em>what</em><area href="/hello/n/area">|] `shouldBe` pure "/hello/n/area"
diff --git a/test-suite/Data/Microformats2/Parser/PropertySpec.hs b/test-suite/Data/Microformats2/Parser/PropertySpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Data/Microformats2/Parser/PropertySpec.hs
@@ -0,0 +1,94 @@
+{-# LANGUAGE OverloadedStrings, UnicodeSyntax, QuasiQuotes, CPP #-}
+
+module Data.Microformats2.Parser.PropertySpec (spec) where
+
+import           Test.Hspec
+import           TestCommon
+import           Data.Microformats2.Parser.Property
+#if __GLASGOW_HASKELL__ < 709
+import           Control.Applicative
+#endif
+
+{-# ANN module ("HLint: ignore Redundant do"::String) #-}
+
+spec ∷ Spec
+spec = do
+  describe "extractProperty" $ do
+    it "parses p- properties" $ do
+      let nm = extractP . documentRoot . parseLBS
+      nm [xml|<span class="p-name">Hello Basic</span>|] `shouldBe` pure "Hello Basic"
+      nm [xml|<abbr class="p-name" title="Hello Abbr">HA</abbr>|] `shouldBe` pure "Hello Abbr"
+      nm [xml|<abbr class="p-name">HA</abbr>|] `shouldBe` pure "HA"
+      nm [xml|<data class="p-name" value="Hello Data" />|] `shouldBe` pure "Hello Data"
+      nm [xml|<input class="p-name" value="Hello Input" />|] `shouldBe` pure "Hello Input"
+      nm [xml|<img class="p-name" alt="Hello Img" />|] `shouldBe` pure "Hello Img"
+      nm [xml|<area class="p-name" alt="Hello Area" />|] `shouldBe` pure "Hello Area"
+      nm [xml|<span class="p-name"> ignore <i class="value">Hello</i>  <img class="value" alt="ValuePattern" src="x.png"> </span>|] `shouldBe` pure "HelloValuePattern"
+      nm [xml|<span class="p-name"> ignore <em class="value-title" title="Hello">Hi</em>  <span class="value">Value-Title</span></span>|] `shouldBe` pure "HelloValue-Title"
+      nm [xml|<span class="p-name">  Hello <img alt="Span With Img" src="x.png"> </span>|] `shouldBe` pure "Hello Span With Img"
+      nm [xml|<span class="p-name">	Hello <img src="span-with.png"> </span>|] `shouldBe` pure "Hello span-with.png"
+      nm [xml|<span class="p-name"> <span class="value"> 	Hello 	
+  <img alt="Span With Img" src="x.png"> </span> 	<em class="value-title" title="&& Value Title">nope</em> </span>|] `shouldBe` pure "Hello&& Value Title"
+
+    it "parses u- properties" $ do
+      let ur = extractU . documentRoot . parseLBS
+      ur [xml|<a class="u-url" href="/yo/a">link</a>|] `shouldBe` pure ("/yo/a", True)
+      ur [xml|<area class="u-url" href="/yo/area"/>|] `shouldBe` pure ("/yo/area", True)
+      ur [xml|<img class="u-url" src="/yo/img"/>|] `shouldBe` pure ("/yo/img", True)
+      ur [xml|<audio class="u-url" src="/yo/audio"/>|] `shouldBe` pure ("/yo/audio", True)
+      ur [xml|<video class="u-url" src="/yo/video"/>|] `shouldBe` pure ("/yo/video", True)
+      ur [xml|<source class="u-url" src="/yo/source"/>|] `shouldBe` pure ("/yo/source", True)
+      ur [xml|<object class="u-url" data="/yo/data"/>|] `shouldBe` pure ("/yo/data", True)
+      ur [xml|<span class="u-url"><b class=value>/yo</b><em class="value">/vcp</span>|] `shouldBe` pure ("/yo/vcp", False)
+      ur [xml|<abbr class="u-url" title="/yo/abbr"/>|] `shouldBe` pure ("/yo/abbr", False)
+      ur [xml|<data class="u-url" value="/yo/data"/>|] `shouldBe` pure ("/yo/data", False)
+      ur [xml|<input class="u-url" value="/yo/input"/>|] `shouldBe` pure ("/yo/input", False)
+      ur [xml|<span class="u-url">/yo/span</span>|] `shouldBe` pure ("/yo/span", False)
+
+    it "parses dt- properties" $ do
+      let dt = extractDt . documentRoot . parseLBS
+      dt [xml|<time class="dt-updated" datetime="ti.me">someday</time>|] `shouldBe` pure "ti.me"
+      dt [xml|<ins class="dt-updated" datetime="i.ns">someday</ins>|] `shouldBe` pure "i.ns"
+      dt [xml|<del class="dt-updated" datetime="d.el">someday</del>|] `shouldBe` pure "d.el"
+      dt [xml|<abbr class="dt-updated" title="ab.br">AB</abbr>|] `shouldBe` pure "ab.br"
+      dt [xml|<data class="dt-updated" value="da.ta"/>|] `shouldBe` pure "da.ta"
+      dt [xml|<input class="dt-updated" value="i.np.ut"/>|] `shouldBe` pure "i.np.ut"
+      dt [xml|<span class="dt-updated">
+            <abbr class="value" title="vcp">VCP</abbr>
+            <time class="value" datetime="ti">TIME</time>
+            <ins class="value" datetime="me">lol</time>
+          </span>|] `shouldBe` pure "vcptime"
+      dt [xml|<span class="dt-updated">date</span>|] `shouldBe` pure "date"
+
+  describe "implyProperty" $ do
+    it "parses implied p-name" $ do
+      let nm = implyProperty "name" . documentRoot . parseLBS
+      nm [xml|<img class="h-blah" alt="Hello Img!">|] `shouldBe` pure "Hello Img!"
+      nm [xml|<abbr class="h-blah" title="Hello Abbr!">HA</abbr>|] `shouldBe` pure "Hello Abbr!"
+      nm [xml|<p class="h-blah"><img alt="Hello Only Img!"></p>|] `shouldBe` pure "Hello Only Img!"
+      nm [xml|<p class="h-blah"><img alt="DOING"><img alt="IT"><img alt="WRONG">Goodbye Img!</p>|] `shouldBe` pure "Goodbye Img!"
+      nm [xml|<p class="h-blah"><abbr title="Hello Only Abbr!">HOA</abbr></p>|] `shouldBe` pure "Hello Only Abbr!"
+      nm [xml|<p class="h-blah"><abbr title="DOING"/><abbr title="IT"/><abbr title="WRONG"/>Goodbye Abbr!</p>|] `shouldBe` pure "Goodbye Abbr!"
+      nm [xml|<p class="h-blah"><em><img alt="Hello Only Nested Img!"></p>|] `shouldBe` pure "Hello Only Nested Img!"
+      nm [xml|<p class="h-blah"><em><img alt="DOING"><img alt="WRONG">Goodbye Nested Img!</p>|] `shouldBe` pure "Goodbye Nested Img!"
+      nm [xml|<p class="h-blah"><em><abbr title="Hello Only Nested Abbr!">HOA</abbr></p>|] `shouldBe` pure "Hello Only Nested Abbr!"
+      nm [xml|<p class="h-blah"><em><abbr title="DOING"/><abbr title="WRONG"/>Goodbye Nested Abbr!</p>|] `shouldBe` pure "Goodbye Nested Abbr!"
+      nm [xml|<p class="h-blah">Hello Text!</p>|] `shouldBe` pure "Hello Text!"
+
+    it "parses implied u-photo" $ do
+      let ph = implyProperty "photo" . documentRoot . parseLBS
+      ph [xml|<img class="h-blah" src="selfie.png">|] `shouldBe` pure "selfie.png"
+      ph [xml|<object class="h-blah" data="art.svg">|] `shouldBe` pure "art.svg"
+      ph [xml|<div class="h-blah"><img src="selfie.png"/><em>yo</em>|] `shouldBe` pure "selfie.png"
+      ph [xml|<div class="h-blah"><object data="art.svg"/><em>yo</em>|] `shouldBe` pure "art.svg"
+      ph [xml|<div class="h-blah"><p class="onlychild"><img src="selfie.png"/><em>yo</em>|] `shouldBe` pure "selfie.png"
+      ph [xml|<div class="h-blah"><p class="onlychild"><object data="art.svg"/><em>yo</em>|] `shouldBe` pure "art.svg"
+
+    it "parses implied u-url" $ do
+      let ur = implyProperty "url" . documentRoot . parseLBS
+      ur [xml|<a class="h-blah" href="/hello/a">|] `shouldBe` pure "/hello/a"
+      ur [xml|<area class="h-blah" href="/hello/area">|] `shouldBe` pure "/hello/area"
+      ur [xml|<div class="h-blah"><em>what</em><a href="/hello/n/a">|] `shouldBe` pure "/hello/n/a"
+      ur [xml|<div class="h-blah"><em>what</em><area href="/hello/n/area">|] `shouldBe` pure "/hello/n/area"
+      ur [xml|<div class="h-blah"><div class="p-nope"><a href="/nope"></div>|] `shouldBe` Nothing
+      ur [xml|<div class="h-blah"><a href="/nope" class="p-nope">n0pe</a>|] `shouldBe` Nothing
diff --git a/test-suite/Data/Microformats2/ParserSpec.hs b/test-suite/Data/Microformats2/ParserSpec.hs
--- a/test-suite/Data/Microformats2/ParserSpec.hs
+++ b/test-suite/Data/Microformats2/ParserSpec.hs
@@ -1,257 +1,240 @@
-{-# LANGUAGE QuasiQuotes, OverloadedStrings, UnicodeSyntax, CPP #-}
+{-# LANGUAGE QuasiQuotes, OverloadedStrings, UnicodeSyntax #-}
 
 module Data.Microformats2.ParserSpec (spec) where
 
-import           Test.Hspec
+import           Test.Hspec hiding (shouldBe)
+import           Test.Hspec.Expectations.Pretty (shouldBe)
 import           TestCommon
-import           Data.Default
-import           Data.Time.Clock
-import           Data.Time.Calendar
-import           Data.Microformats2
+import           Network.URI (parseURI)
 import           Data.Microformats2.Parser
-#if __GLASGOW_HASKELL__ < 709
-import           Control.Applicative
-#endif
 
-{-# ANN module ("HLint: ignore Redundant do"::String) #-}
-
 spec ∷ Spec
 spec = do
-
-  describe "parseGeo" $ do
-    let parseGeo' = parseGeo . documentRoot . parseLBS
-
-    it "parses valid h-geo" $ do
-      parseGeo' [xml|<div>
-        <p class="h-geo">
-          <span class="p-latitude">37.33168</span>
-          <span class="p-longitude">-122.03016</span>
-          <span class="p-altitude">1.2345</span>
-        </p>
-        <p class="h-geo">
-          <data class="p-latitude" value="123.45">
-          <input class="p-latitude" value="678.9">
-        </p>
-      </div>|] `shouldBe` [ def { geoLatitude = pure 37.33168, geoLongitude = pure (-122.03016), geoAltitude = pure 1.2345 }
-                          , def { geoLatitude = [123.45, 678.9] } ]
-
-    it "ignores invalid properties" $ do
-      parseGeo' [xml|<p class="h-geo">
-          <span class="p-latitude">HELLO WORLD!!</span>
-          <span class="p-altitude">1.2345</span>
-        </p>|] `shouldBe` [ def { geoAltitude = pure 1.2345 } ]
-
-  describe "parseAdr" $ do
-    let parseAdr' = parseAdr . documentRoot . parseLBS
-
-    it "parses valid h-adr" $ do
-      parseAdr' [xml|<div>
-          <article class="h-adr">
-            <span class="p-street-address">SA</span>
-            <p class="p-extended-address">EA</p>
-            <abbr class="p-post-office-box" title="PO">_</abbr>
-            <span class="p-locality">L</span>
-            <span class="p-region">R</span>
-            <span class="p-postal-code">PC</span>
-            <span class="p-country-name">C</span>
-            <span class="p-label">LB</span>
-            <span class="p-geo">G</span>
-          </article>
-        </div>|] `shouldBe` [ def { adrStreetAddress = pure "SA", adrExtendedAddress = pure "EA"
-                                  , adrPostOfficeBox = pure "PO", adrLocality = pure "L"
-                                  , adrRegion = pure "R", adrPostalCode = pure "PC"
-                                  , adrCountryName = pure "C", adrLabel = pure "LB"
-                                  , adrGeo = pure $ TextGeo "G" } ]
-
-    it "parses p-geo" $ do
-      parseAdr' [xml|<div>
-          <span class="h-adr">
-            <span class="p-geo h-geo">
-              <span class="p-latitude">37.33168</span>
-              <span class="p-longitude">-122.03016</span>
-              <span class="p-altitude">1.2345</span>
-            </span>
-          </span>
-        </div>|] `shouldBe` [ def { adrGeo = [GeoGeo $ def { geoLatitude = pure 37.33168, geoLongitude = pure (-122.03016), geoAltitude = pure 1.2345  }] } ]
-
-    it "ignores nested h-geo not marked as p-geo" $ do
-      parseAdr' [xml|<div>
-          <span class="h-adr">
-            <span class="h-geo"><span class="p-altitude">1.2345</span></span>
-          </span>
-        </div>|] `shouldBe` [ def ]
-
-    it "parses geo properties into p-geo" $ do
-      parseAdr' [xml|<div>
-          <span class="h-adr">
-            <span class="p-latitude">37.33168</span>
-            <span class="p-longitude">-122.03016</span>
-            <span class="p-altitude">1.2345</span>
-          </span>
-        </div>|] `shouldBe` [ def { adrGeo = [GeoGeo $ def { geoLatitude = pure 37.33168, geoLongitude = pure (-122.03016), geoAltitude = pure 1.2345  }] } ]
-
-  describe "parseCard" $ do
-    let parseCard' = parseCard . documentRoot . parseLBS
-
-    it "parses valid h-card" $ do
-      parseCard' [xml|<div>
-          <p class="h-card">
-            <img class="u-photo" src="photo.png" alt="" />
-            <a class="p-name u-url" href="http://example.org">Joe Bloggs</a>
-            <a class="u-email" href="mailto:joebloggs@example.com">joebloggs@example.com</a>,
-          </p>
-        </div>|] `shouldBe` [ def { cardPhoto = pure "photo.png"
-                                  , cardUrl = pure "http://example.org"
-                                  , cardName = pure "Joe Bloggs"
-                                  , cardEmail = pure "mailto:joebloggs@example.com" } ]
-
-    it "parses valid implied h-card" $ do
-      parseCard' [xml|<div>
-          <a class="h-card" href="http://example.org">Joe Bloggs</a>
-          <img class="h-card" src="http://example.org/photo.jpg" />
-        </div>|] `shouldBe` [ def { cardUrl = pure "http://example.org"
-                                  , cardName = pure "Joe Bloggs" }
-                            , def { cardPhoto = pure "http://example.org/photo.jpg" }]
-
-    it "parses p-adr" $ do
-      parseCard' [xml|<div>
-          <section class="h-card">
-            <p class="p-adr h-adr">
-              <span class="p-street-address">17</span>
-              <span class="p-locality">Reykjavik</span>
-              <span class="p-country-name">Iceland</span>
-            </p>
-          </section>
-        </div>|] `shouldBe` [ def { cardAdr = pure (AdrAdr $ def { adrStreetAddress = pure "17"
-                                                                 , adrLocality = pure "Reykjavik"
-                                                                 , adrCountryName = pure "Iceland" }) } ]
-
-    it "ignores nested h-adr not marked as p-adr" $ do
-      parseCard' [xml|<div>
-          <section class="h-card">
-            <p class="h-adr"> <span class="p-country-name">Iceland</span> </p>
-          </section>
-        </div>|] `shouldBe` [ def ]
-
-    it "parses adr and geo properties into p-adr" $ do
-      parseCard' [xml|<div>
-          <p class="h-card">
-            <span class="p-street-address">17</span>
-            <span class="p-locality">Reykjavik</span>
-            <span class="p-country-name">Iceland</span>
-            <span class="p-longitude">-122.03016</span>
-          </p>
-        </div>|] `shouldBe` [ def { cardAdr = pure (AdrAdr $ def { adrStreetAddress = pure "17"
-                                                                 , adrLocality = pure "Reykjavik"
-                                                                 , adrCountryName = pure "Iceland"
-                                                                 , adrGeo = [ GeoGeo $ def { geoLongitude = pure (-122.03016) } ] }) } ]
-
-    it "parses h-geo into p-adr" $ do
-      parseCard' [xml|<div>
-          <section class="h-card">
-            <p class="p-geo h-geo"> <span class="p-longitude">-122.03016</span> </p>
-          </section>
-        </div>|] `shouldBe` [ def { cardAdr = pure (AdrAdr $ def { adrGeo = [ GeoGeo $ def { geoLongitude = pure (-122.03016) } ] }) } ]
+  describe "parseItems" $ do
+    let parseMf2'' c = parseMf2 c . documentRoot . parseLBS
+    let parseMf2' = parseMf2'' def
+    it "parses items" $ do
+      parseMf2' [xml|<body>
+  <div class="h-something aaa h-something-else">
+    <h1 class="someclass p-name eeee">Name</h1>
+    <header><A class="p-name u-url" href="http://main.url">other name</a></header>
+    <span class="aaaaaap-nothingaaaa">---</span>
+    <section class="p-org h-card">
+      <a class="p-name">Card</a>
+    </section>
+    <template>
+      <a href="http://ignore.this" class="p-org h-card">IGNORED</a>
+    </template>
+    <a href="http://card.url" class="p-org h-card">org</a>
+    <time class="dt-published p-published dt-updated" datetime="2015-07-17T21:05:13+00:00">17<sup>th</sup> of July 2015 at 21:05</time>
+    <Div class="h-nested-something">
+      haz name
+    </Div>
+    <map name="primary">
+      <area shape="circle" coords="200,250,25" class="h-area" />
+      <area class="h-area" />
+    </map>
+  </div>
+</body>|] `shouldBe` [json|{
+    "items": [
+        {
+            "type": [ "h-something", "h-something-else" ],
+            "properties": {
+                "org": [
+                    {
+                        "type": [ "h-card" ],
+                        "properties": {
+                            "name": [ "Card" ]
+                        },
+                        "value": "Card"
+                    },
+                    {
+                        "type": [ "h-card" ],
+                        "properties": {
+                            "name": [ "org" ],
+                            "url": [ "http:\/\/card.url" ]
+                        },
+                        "value": "org"
+                    }
+                ],
+                "url": [ "http:\/\/main.url" ],
+                "name": [ "Name", "other name" ],
+                "published": [ "17th of July 2015 at 21:05", "2015-07-17T21:05:13+00:00" ],
+                "updated": [ "2015-07-17T21:05:13+00:00" ]
+            },
+            "children": [
+                {
+                    "type": [ "h-nested-something" ],
+                    "properties": {
+                        "name": [ "haz name" ]
+                    },
+                    "value": "haz name"
+                },
+                {
+                    "type": [ "h-area" ],
+                    "properties": {},
+                    "shape": "circle",
+                    "coords": "200,250,25",
+                    "value": null
+                },
+                {
+                    "type": [ "h-area" ],
+                    "properties": {},
+                    "value": null
+                }
+            ]
+        }
+    ],
+    "rels": {},
+    "rel-urls": {}
 
-    it "ignores nested h-geo not marked as p-geo" $ do
-      parseCard' [xml|<div>
-          <section class="h-card">
-            <p class="h-geo"> <span class="p-longitude">-122.03016</span> </p>
-          </section>
-        </div>|] `shouldBe` [ def ]
+}|]
 
-    it "parses multiple things into p-adr" $ do
-      parseCard' [xml|<div>
-          <section class="h-card">
-            <p class="p-geo h-geo"> <span class="p-longitude">-122.03016</span> </p>
-            <span class="p-altitude">-122.03016</span>
-            <span class="p-country-name">Iceland</span>
-            <p class="p-adr h-adr"> <span class="p-locality">Reykjavik</span> </p>
-          </section>
-        </div>|] `shouldBe` [ def { cardAdr = [
-                                      (AdrAdr $ def { adrCountryName = pure "Iceland" 
-                                                    , adrGeo = [ GeoGeo $ def { geoAltitude = pure (-122.03016) }
-                                                               , GeoGeo $ def { geoLongitude = pure (-122.03016) } ] })
-                                    , (AdrAdr $ def { adrLocality = pure "Reykjavik" }) ]} ]
+    it "inserts value and html into e-* h-* properties" $ do
+      parseMf2' [xml|<body class=h-parent><div class="h-child e-prop">some <abbr>html</abbr> and <span class="p-name">props|] `shouldBe` [json|{
+    "items": [
+        {
+            "type": [ "h-parent" ],
+            "properties": {
+                "name": [ "some html and props" ],
+                "prop": [
+                    {
+                        "type": [ "h-child" ],
+                        "properties": {
+                            "name": [ "props" ]
+                        },
+                        "value": "some html and props",
+                        "html": "some <abbr>html</abbr> and <span class=\"p-name\">props</span>"
+                    }
+                ]
+            }
+        }
+    ],
+    "rels": {},
+    "rel-urls": {}
+}|]
 
+    it "parses nested properties but doesn't parse properties of nested microformats into the parent" $ do
+      parseMf2' [xml|<body class=h-parent>
+                <span class="p-outer"><span class="p-inner">some</span>thing</span>
+                <div class="p-outer"><div class="h-child p-prop"><span class="p-aaa">a</span></div></div>
+                <div class="h-child"><span class="p-bbb">b</span></div>|] `shouldBe` [json|{
+    "items": [
+        {
+            "type": [ "h-parent" ],
+            "properties": {
+                "name": [ "something a b" ],
+                "outer": [ "something", "a" ],
+                "inner": [ "some" ],
+                "prop": [ {
+                    "type": [ "h-child" ],
+                    "properties": {
+                        "name": [ "a" ],
+                        "aaa": [ "a" ]
+                    },
+                    "value": "a"
+                } ]
+            },
+            "children": [ {
+                "type": [ "h-child" ],
+                "properties": {
+                    "name": [ "b" ],
+                    "bbb": [ "b" ]
+                },
+                "value": "b"
+            } ]
+        }
+    ],
+    "rels": {},
+    "rel-urls": {}
+}|]
 
-    it "parses p-org" $ do
-      parseCard' [xml|<div>
-          <section class="h-card">
-            <p class="p-org h-card"> <span class="p-name">IndieWebCamp</span> </p>
-            <span class="p-org">Microformats</span>
-          </section>
-        </div>|] `shouldBe` [ def { cardOrg = [ TextCard "Microformats"
-                                                , (CardCard $ def { cardName = pure "IndieWebCamp" }) ] }
-                            , def { cardName = pure "IndieWebCamp" }]
+    it "parses rels" $ do
+      parseMf2' [xml|<html>
+  <head><link rel="alternate feed me" href="/atom.xml" type="application/atom+xml"></head>
+  <body>
+    <a href="//example.com">test</a>
+    <a href="https://twitter.com/myfreeweb" rel=me>twitter</a>
+    <p><span><a href="https://github.com/myfreeweb" rel=me media=handheld hreflang=en>github</a></span></p>
+    <footer><a href="/-1" rel="prev">-1</a></footer>
+  </body>
+</html>|] `shouldBe` [json|{
+    "items": [],
+    "rels": {
+        "feed": [ "/atom.xml" ],
+        "me": [ "https:\/\/github.com\/myfreeweb", "https:\/\/twitter.com\/myfreeweb", "/atom.xml" ],
+        "alternate": [ "/atom.xml" ],
+        "prev": [ "/-1" ]
+    },
+    "rel-urls": {
+        "/atom.xml": { 
+            "type": "application/atom+xml",
+            "rels": [ "alternate", "feed", "me" ]
+        },
+        "https://twitter.com/myfreeweb": { 
+            "text": "twitter",
+            "rels": [ "me" ]
+        },
+        "https://github.com/myfreeweb": { 
+            "text": "github",
+            "rels": [ "me" ],
+            "media": "handheld",
+            "hreflang": "en"
+        },
+        "/-1": {
+            "text": "-1",
+            "rels": [ "prev" ]
+        }
+    }
+}|]
 
-  describe "parseCite" $ do
-    let parseCite' = parseCite Strip . documentRoot . parseLBS
+    it "resolves relative URIs" $ do
 
-    it "parses valid h-cite" $ do
-      parseCite' [xml|<div>
-          <article class="h-cite">
-            <a class="p-name u-url u-uid" href="https://youtu.be/E99FnoYqoII">Rails is Omakase</a>
-            <span class="p-author h-card"><span class="p-name">DHH</span></span>
-            <time class="dt-published">2013-01-25</time>
-            <p class="e-content">Rails is not that. Rails is omakase...</p>
-          </article>
-        </div>|] `shouldBe` [ def { citeName = pure "Rails is Omakase"
-                                  , citeUrl = pure "https://youtu.be/E99FnoYqoII"
-                                  , citeUid = pure "https://youtu.be/E99FnoYqoII"
-                                  , citeAuthor = pure $ CardCard $ def { cardName = pure "DHH" }
-                                  , citeContent = pure $ TextContent "Rails is not that. Rails is omakase..."
-                                  , citePublished = pure $ UTCTime (fromGregorian 2013 1 25) (secondsToDiffTime 0) } ]
+      parseMf2' [xml|<html> <base href="http://example.com"> <a href="/atom.xml" rel=me>feed</a> </html>|] `shouldBe` [json|{
+    "items": [],
+    "rels": { "me": [ "http://example.com/atom.xml" ] },
+    "rel-urls": { "http://example.com/atom.xml": { "text": "feed", "rels": [ "me" ] } } }|]
 
+      parseMf2'' (def { baseUri = parseURI "http://com.example" }) [xml|<html> <a href="/atom.xml" rel=me>feed</a> </html>|] `shouldBe` [json|{
+    "items": [],
+    "rels": { "me": [ "http://com.example/atom.xml" ] },
+    "rel-urls": { "http://com.example/atom.xml": { "text": "feed", "rels": [ "me" ] } } }|]
 
-  describe "parseEntry" $ do
-    let parseEntry' m = parseEntry m . documentRoot . parseLBS
+      parseMf2'' (def { baseUri = parseURI "http://com.example" }) [xml|<html> <base href="http://example.com"> <a href="/atom.xml" rel=me>feed</a> </html>|] `shouldBe` [json|{
+    "items": [],
+    "rels": { "me": [ "http://example.com/atom.xml" ] },
+    "rel-urls": { "http://example.com/atom.xml": { "text": "feed", "rels": [ "me" ] } } }|]
 
-    it "parses valid h-entry" $ do
-      parseEntry' Strip [xml|<div>
-          <article class="h-entry">
-            <a class="p-name u-url u-uid" href="https://youtu.be/E99FnoYqoII">Rails is Omakase</a>
-            <span class="p-author h-card"><span class="p-name">DHH</span></span>
-            <a href="http://david.heinemeierhansson.com" class="p-author h-card">DHH</a>
-            <a href="http://david.heinemeierhansson.com" class="p-author">David</a>
-            <time class="dt-published">2013-01-25</time>
-            <time class="dt-updated">2013-01-25T01:23</time>
-            <p class="e-content">Rails is not that. Rails is omakase...</p>
-          </article>
-        </div>|] `shouldBe` [ def { entryName = pure "Rails is Omakase"
-                                  , entryUrl = pure "https://youtu.be/E99FnoYqoII"
-                                  , entryUid = pure "https://youtu.be/E99FnoYqoII"
-                                  , entryAuthor = [ TextCard "David"
-                                                  , CardCard $ def { cardName = pure "DHH" }
-                                                  , CardCard $ def { cardName = pure "DHH", cardUrl = pure "http://david.heinemeierhansson.com" } ]
-                                  , entryContent = pure $ TextContent "Rails is not that. Rails is omakase..."
-                                  , entryPublished = pure $ UTCTime (fromGregorian 2013 1 25) (secondsToDiffTime 0)
-                                  , entryUpdated = pure $ UTCTime (fromGregorian 2013 1 25) (secondsToDiffTime 4980) } ]
+      parseMf2'' (def { baseUri = parseURI "http://com.example" }) [xml|<html> <base href="/base/"> <a href="atom.xml" rel=me>feed</a> </html>|] `shouldBe` [json|{
+    "items": [],
+    "rels": { "me": [ "http://com.example/base/atom.xml" ] },
+    "rel-urls": { "http://com.example/base/atom.xml": { "text": "feed", "rels": [ "me" ] } } }|]
 
-    it "supports different html content modes" $ do
-      let src = [xml|<div>
-          <article class="h-entry">
-            <p class="e-content"><script>alert('XSS')</script><a href="http://rubyonrails.org" onclick="alert()">Rails</a> is not that. Rails is omakase...</p>
-          </article>
-        </div>|]
-      parseEntry' Unsafe   src `shouldBe` [ def { entryContent = pure $ TextContent "<script>alert('XSS')</script><a href=\"http://rubyonrails.org\" onclick=\"alert()\">Rails</a> is not that. Rails is omakase..." } ]
-      parseEntry' Strip    src `shouldBe` [ def { entryContent = pure $ TextContent "alert('XSS')Rails is not that. Rails is omakase..." } ]
-      parseEntry' Escape   src `shouldBe` [ def { entryContent = pure $ TextContent "&lt;script&gt;alert('XSS')&lt;/script&gt;&lt;a href=\"http://rubyonrails.org\" onclick=\"alert()\"&gt;Rails&lt;/a&gt; is not that. Rails is omakase..." } ]
-      parseEntry' Sanitize src `shouldBe` [ def { entryContent = pure $ TextContent "<a href=\"http://rubyonrails.org\">Rails</a> is not that. Rails is omakase..." } ]
+      parseMf2' [xml|<html> <base href="/base/"> <a href="atom.xml" rel=me>feed</a> </html>|] `shouldBe` [json|{
+    "items": [],
+    "rels": { "me": [ "/base/atom.xml" ] },
+    "rel-urls": { "/base/atom.xml": { "text": "feed", "rels": [ "me" ] } } }|]
 
-    it "parses p-location" $ do
-      parseEntry' Strip [xml|<div>
-          <article class="h-entry">
-            <p class="p-location h-card">
-              <a class="p-name u-url" href="http://penisland.net">Pen Island</a>
-            </p>
-            <p class="p-location h-adr">
-              <span class="p-country-name">USA</span>
-            </p>
-            <p class="p-location h-geo">
-              <data class="p-latitude" value="123.45">
-            </p>
-          </article>
-        </div>|] `shouldBe` [ def { entryLocation = [ CardLoc $ def { cardName = pure "Pen Island", cardUrl = pure "http://penisland.net" }
-                                                    , AdrLoc $ def { adrCountryName = pure "USA" }
-                                                    , GeoLoc $ def { geoLatitude = pure 123.45 } ] } ]
+      parseMf2'' (def { baseUri = parseURI "http://com.example" }) [xml|<html>
+  <base href="/base/">
+  <a href="atom.xml" rel=me>feed</a>
+  <div class=h-card>
+    <h1 class=p-name>card</h1>
+    <a href="url" class=u-url>url</a>
+    <span href="url" class=u-url><span class=value>/not</span>!!!<em class=value>/resolved</em></span>
+    <img src="photo.webp" alt="photo of me"> <!-- implied by :only-of-type -->
+  </div>
+</html>|] `shouldBe` [json|{
+    "items": [
+        {
+            "type": [ "h-card" ],
+            "properties": {
+                "photo": [ "http://com.example/base/photo.webp" ],
+                "url": [ "http://com.example/base/url", "/not/resolved" ],
+                "name": [ "card" ]
+            }
+        }
+    ],
+    "rels": { "me": [ "http://com.example/base/atom.xml" ] },
+    "rel-urls": { "http://com.example/base/atom.xml": { "text": "feed", "rels": [ "me" ] } }
+}|]
